diff --git a/Makefile b/Makefile index 19e9b01..af6069e 100644 --- a/Makefile +++ b/Makefile @@ -10,9 +10,11 @@ oapi-codegen-install: oapi-codegen: oapi-codegen -package insightapi -generate types ./api/insights-v1.yml > ./gen/insightapi/insights.types.go oapi-codegen -package insightapi -generate client ./api/insights-v1.yml > ./gen/insightapi/insights.client.go + oapi-codegen -package controlplane -generate types ./api/cp-v1-trials.yml > ./gen/controlplane/trials.types.go + oapi-codegen -package controlplane -generate client ./api/cp-v1-trials.yml > ./gen/controlplane/trials.client.go setup: - mkdir -p out gen/insightapi + mkdir -p out gen/insightapi gen/controlplane GO_CFLAGS=-X main.commit=$(GITCOMMIT) -X main.version=$(VERSION) GO_LDFLAGS=-ldflags "-w $(GO_CFLAGS)" diff --git a/api/cp-v1-trials.yml b/api/cp-v1-trials.yml new file mode 100644 index 0000000..95d68d5 --- /dev/null +++ b/api/cp-v1-trials.yml @@ -0,0 +1,123 @@ +openapi: 3.0.2 +info: + title: SafeDep Control Plane API for Trials Registration + contact: + name: SafeDep API + url: 'https://safedep.io' + description: | + Trials API provide a way for obtaining an API Key for data plane service access + using an Email Address. Trials is different from Registrations as the later + allows full access to the control plane while Trials is meant to allow access + only to a time bounded (expirable) API key for quick evaluation of tools. + version: 0.0.1 +servers: + - url: 'https://{apiHost}/{apiBase}' + variables: + apiHost: + default: api.safedep.io + apiBase: + default: control-plane/v1 +tags: + - name: Control Plane + description: Control Plane API +paths: + /trials: + post: + description: | + Register a trial user to obtain an expirable API Key. The API key will + be generated and sent to the user over Email to ensure validity and access + to the email by the requester. System defined limits will be applied to + maximum number of trial API keys that can be generated for an email. + operationId: registerTrialUser + tags: + - Control Plane + requestBody: + description: Trial registration request + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TrialRequest' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/TrialResponse' + '403': + description: Access to the API is denied + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + '429': + description: Rate limit block + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + '500': + description: Failed due to internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' +components: + schemas: + ApiError: + type: object + properties: + message: + type: string + description: A descriptive message about the error meant for developer consumption + type: + type: string + description: An optional service or domain specific error group + enum: + - invalid_request + - operation_failed + - internal_error + code: + type: string + description: An error code identifying the error + enum: + - api_guard_invalid_credentials + - api_guard_rate_limit_exceeded + - api_guard_unauthorized + - api_guard_error + - app_generic_error + - app_security_error + - app_insufficient_parameters + - app_feature_not_enabled + - app_package_version_not_found + params: + type: object + description: Optional error specific attributes + additionalProperties: + type: object + properties: + key: + type: string + value: + type: string + TrialRequest: + type: object + properties: + email: + type: string + format: email + required: + - email + TrialResponse: + type: object + properties: + id: + type: string + minLength: 6 + maxLength: 512 + description: The ID of the trial registration request created in the system + expires_at: + type: string + format: date-time + description: The expiry time of the API key diff --git a/auth.go b/auth.go index 5388a8b..e827779 100644 --- a/auth.go +++ b/auth.go @@ -12,7 +12,9 @@ import ( ) var ( - authInsightApiBaseUrl string + authInsightApiBaseUrl string + authControlPlaneApiBaseUrl string + authTrialEmail string ) func newAuthCommand() *cobra.Command { @@ -28,6 +30,7 @@ func newAuthCommand() *cobra.Command { cmd.AddCommand(configureAuthCommand()) cmd.AddCommand(verifyAuthCommand()) + cmd.AddCommand(trialsRegisterCommand()) return cmd } @@ -43,8 +46,9 @@ func configureAuthCommand() *cobra.Command { } err = auth.Configure(auth.Config{ - ApiUrl: authInsightApiBaseUrl, - ApiKey: string(key), + ApiUrl: authInsightApiBaseUrl, + ApiKey: string(key), + ControlPlaneApiUrl: authControlPlaneApiBaseUrl, }) if err != nil { panic(err) @@ -55,8 +59,10 @@ func configureAuthCommand() *cobra.Command { }, } - cmd.Flags().StringVarP(&authInsightApiBaseUrl, "api", "", "https://api.safedep.io/insights/v1", + cmd.Flags().StringVarP(&authInsightApiBaseUrl, "api", "", auth.DefaultApiUrl(), "Base URL of Insights API") + cmd.Flags().StringVarP(&authControlPlaneApiBaseUrl, "control-plane", "", + auth.DefaultControlPlaneApiUrl(), "Base URL of Control Plane API for registrations") return cmd @@ -66,7 +72,7 @@ func verifyAuthCommand() *cobra.Command { cmd := &cobra.Command{ Use: "verify", RunE: func(cmd *cobra.Command, args []string) error { - // Run auth.Verify() + fmt.Printf("Verify auth command is currently work in progress\n") os.Exit(1) return nil }, @@ -74,3 +80,33 @@ func verifyAuthCommand() *cobra.Command { return cmd } + +func trialsRegisterCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "trial", + RunE: func(cmd *cobra.Command, args []string) error { + client := auth.NewTrialRegistrationClient(auth.TrialConfig{ + Email: authTrialEmail, + ControlPlaneApiUrl: authControlPlaneApiBaseUrl, + }) + + res, err := client.Execute() + if err != nil { + return err + } + + fmt.Printf("Trial registration successful with Id:%s\n", res.Id) + fmt.Printf("Check your email (%s) for API key and usage instructions\n", authTrialEmail) + fmt.Printf("The trial API key will expire on %s\n", res.ExpiresAt.String()) + + return nil + }, + } + + cmd.Flags().StringVarP(&authTrialEmail, "email", "", "", + "Email address to use for sending trial API key") + cmd.Flags().StringVarP(&authControlPlaneApiBaseUrl, "control-plane", "", + auth.DefaultControlPlaneApiUrl(), "Base URL of Control Plane API for registrations") + + return cmd +} diff --git a/gen/controlplane/trials.client.go b/gen/controlplane/trials.client.go new file mode 100644 index 0000000..c0947ac --- /dev/null +++ b/gen/controlplane/trials.client.go @@ -0,0 +1,297 @@ +// Package controlplane provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.10.1 DO NOT EDIT. +package controlplane + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // RegisterTrialUser request with any body + RegisterTrialUserWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RegisterTrialUser(ctx context.Context, body RegisterTrialUserJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) RegisterTrialUserWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterTrialUserRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RegisterTrialUser(ctx context.Context, body RegisterTrialUserJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRegisterTrialUserRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewRegisterTrialUserRequest calls the generic RegisterTrialUser builder with application/json body +func NewRegisterTrialUserRequest(server string, body RegisterTrialUserJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRegisterTrialUserRequestWithBody(server, "application/json", bodyReader) +} + +// NewRegisterTrialUserRequestWithBody generates requests for RegisterTrialUser with any type of body +func NewRegisterTrialUserRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trials") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // RegisterTrialUser request with any body + RegisterTrialUserWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterTrialUserResponse, error) + + RegisterTrialUserWithResponse(ctx context.Context, body RegisterTrialUserJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterTrialUserResponse, error) +} + +type RegisterTrialUserResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *TrialResponse + JSON403 *ApiError + JSON429 *ApiError + JSON500 *ApiError +} + +// Status returns HTTPResponse.Status +func (r RegisterTrialUserResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RegisterTrialUserResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// RegisterTrialUserWithBodyWithResponse request with arbitrary body returning *RegisterTrialUserResponse +func (c *ClientWithResponses) RegisterTrialUserWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterTrialUserResponse, error) { + rsp, err := c.RegisterTrialUserWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterTrialUserResponse(rsp) +} + +func (c *ClientWithResponses) RegisterTrialUserWithResponse(ctx context.Context, body RegisterTrialUserJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterTrialUserResponse, error) { + rsp, err := c.RegisterTrialUser(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRegisterTrialUserResponse(rsp) +} + +// ParseRegisterTrialUserResponse parses an HTTP response from a RegisterTrialUserWithResponse call +func ParseRegisterTrialUserResponse(rsp *http.Response) (*RegisterTrialUserResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RegisterTrialUserResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest TrialResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest ApiError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest ApiError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest ApiError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} diff --git a/gen/controlplane/trials.types.go b/gen/controlplane/trials.types.go new file mode 100644 index 0000000..b1b9c8a --- /dev/null +++ b/gen/controlplane/trials.types.go @@ -0,0 +1,159 @@ +// Package controlplane provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.10.1 DO NOT EDIT. +package controlplane + +import ( + "encoding/json" + "fmt" + "time" + + openapi_types "github.com/deepmap/oapi-codegen/pkg/types" +) + +// Defines values for ApiErrorCode. +const ( + ApiErrorCodeApiGuardError ApiErrorCode = "api_guard_error" + + ApiErrorCodeApiGuardInvalidCredentials ApiErrorCode = "api_guard_invalid_credentials" + + ApiErrorCodeApiGuardRateLimitExceeded ApiErrorCode = "api_guard_rate_limit_exceeded" + + ApiErrorCodeApiGuardUnauthorized ApiErrorCode = "api_guard_unauthorized" + + ApiErrorCodeAppFeatureNotEnabled ApiErrorCode = "app_feature_not_enabled" + + ApiErrorCodeAppGenericError ApiErrorCode = "app_generic_error" + + ApiErrorCodeAppInsufficientParameters ApiErrorCode = "app_insufficient_parameters" + + ApiErrorCodeAppPackageVersionNotFound ApiErrorCode = "app_package_version_not_found" + + ApiErrorCodeAppSecurityError ApiErrorCode = "app_security_error" +) + +// Defines values for ApiErrorType. +const ( + ApiErrorTypeInternalError ApiErrorType = "internal_error" + + ApiErrorTypeInvalidRequest ApiErrorType = "invalid_request" + + ApiErrorTypeOperationFailed ApiErrorType = "operation_failed" +) + +// ApiError defines model for ApiError. +type ApiError struct { + // An error code identifying the error + Code *ApiErrorCode `json:"code,omitempty"` + + // A descriptive message about the error meant for developer consumption + Message *string `json:"message,omitempty"` + + // Optional error specific attributes + Params *ApiError_Params `json:"params,omitempty"` + + // An optional service or domain specific error group + Type *ApiErrorType `json:"type,omitempty"` +} + +// An error code identifying the error +type ApiErrorCode string + +// Optional error specific attributes +type ApiError_Params struct { + AdditionalProperties map[string]struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` + } `json:"-"` +} + +// An optional service or domain specific error group +type ApiErrorType string + +// TrialRequest defines model for TrialRequest. +type TrialRequest struct { + Email openapi_types.Email `json:"email"` +} + +// TrialResponse defines model for TrialResponse. +type TrialResponse struct { + // The expiry time of the API key + ExpiresAt *time.Time `json:"expires_at,omitempty"` + + // The ID of the trial registration request created in the system + Id *string `json:"id,omitempty"` +} + +// RegisterTrialUserJSONBody defines parameters for RegisterTrialUser. +type RegisterTrialUserJSONBody TrialRequest + +// RegisterTrialUserJSONRequestBody defines body for RegisterTrialUser for application/json ContentType. +type RegisterTrialUserJSONRequestBody RegisterTrialUserJSONBody + +// Getter for additional properties for ApiError_Params. Returns the specified +// element and whether it was found +func (a ApiError_Params) Get(fieldName string) (value struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for ApiError_Params +func (a *ApiError_Params) Set(fieldName string, value struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` + }) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for ApiError_Params to handle AdditionalProperties +func (a *ApiError_Params) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` + }) + for fieldName, fieldBuf := range object { + var fieldVal struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` + } + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for ApiError_Params to handle AdditionalProperties +func (a ApiError_Params) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index efc160a..6fac033 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -13,14 +13,18 @@ const ( apiUrlEnvKey = "VET_INSIGHTS_API_URL" apiKeyEnvKey = "VET_INSIGHTS_API_KEY" - defaultApiUrl = "https://api.safedep.io/insights/v1" + controlPlaneUrlEnvKey = "VET_CONTROL_PLANE_API_URL" + + defaultApiUrl = "https://api.safedep.io/insights/v1" + defaultControlPlaneApiUrl = "https://api.safedep.io/controlplane/v1" homeRelativeConfigPath = ".safedep/vet-auth.yml" ) type Config struct { - ApiUrl string `yaml:"api_url"` - ApiKey string `yaml:"api_key"` + ApiUrl string `yaml:"api_url"` + ApiKey string `yaml:"api_key"` + ControlPlaneApiUrl string `yaml:"control_plane_api"` } // Global config to be used during runtime @@ -40,6 +44,14 @@ func Verify() error { return nil } +func DefaultApiUrl() string { + return defaultApiUrl +} + +func DefaultControlPlaneApiUrl() string { + return defaultControlPlaneApiUrl +} + func ApiUrl() string { if url, ok := os.LookupEnv(apiUrlEnvKey); ok { return url @@ -64,6 +76,18 @@ func ApiKey() string { return "" } +func ControlPlaneApiUrl() string { + if url, ok := os.LookupEnv(controlPlaneUrlEnvKey); ok { + return url + } + + if globalConfig != nil { + return globalConfig.ControlPlaneApiUrl + } + + return defaultControlPlaneApiUrl +} + func loadConfiguration() error { path, err := os.UserHomeDir() path = filepath.Join(path, homeRelativeConfigPath) diff --git a/internal/auth/trial.go b/internal/auth/trial.go new file mode 100644 index 0000000..c3e8c55 --- /dev/null +++ b/internal/auth/trial.go @@ -0,0 +1,34 @@ +package auth + +import ( + "errors" + "time" + + "github.com/safedep/dry/utils" +) + +type TrialConfig struct { + Email string + ControlPlaneApiUrl string +} + +type trialRegistrationResponse struct { + Id string + ExpiresAt time.Time +} + +type trialRegistrationClient struct { + config TrialConfig +} + +func NewTrialRegistrationClient(config TrialConfig) *trialRegistrationClient { + return &trialRegistrationClient{config: config} +} + +func (client *trialRegistrationClient) Execute() (*trialRegistrationResponse, error) { + if utils.IsEmptyString(client.config.Email) { + return nil, errors.New("email is required") + } + + return &trialRegistrationResponse{}, nil +} diff --git a/main.go b/main.go index 07cca37..698fc39 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ var banner string = ` | | | || | | || | | | | '--------------' || '--------------' || '--------------' | '----------------' '----------------' '----------------' + ` func main() {