package bsl import ( "context" "fmt" "net/http" "strings" "time" "edge-infra.dev/pkg/edge/api/apierror/bsl" bslerror "edge-infra.dev/pkg/edge/api/apierror/bsl" "edge-infra.dev/pkg/edge/api/bsl/types" "edge-infra.dev/pkg/edge/api/middleware" "github.com/NCR-Corporation/ncr-bsp-hmac/go/sign" "github.com/go-resty/resty/v2" "github.com/google/uuid" "github.com/rs/zerolog/log" ) const ( AccessTokenPrefix = "AccessToken" NepCorrelationID = "Nep-Correlation-Id" jsonType = "application/json" ) var ( defaultTimeOut = time.Second * 10 ) // AccessKey access keys to sign bsl request type AccessKey struct { SharedKey string `json:"sharedKey"` SecretKey string `json:"secretKey"` } type SyncConfig struct { EntityTypes []string `json:"entityTypes"` EnterpriseUnitIDs []string `json:"enterpriseUnitIds"` } func (a AccessKey) Valid() bool { return a.SharedKey != "" && a.SecretKey != "" } type contextKey struct{} var reqContextKey = &contextKey{} type Client struct { *resty.Client config types.BSPConfig cache map[string]sign.HTTPSigner GetAccessKey AccessKeySecret DefaultAccessKey *AccessKey } type Request struct { *resty.Request rootOrg string } func NewBSLClient(config types.BSPConfig, fn ...AccessKeySecret) *Client { var getAccessKey AccessKeySecret if len(fn) == 1 { getAccessKey = fn[0] } client := resty.New() client.SetBaseURL(config.Endpoint) client.SetHeader("Access-Control-Allow-Credentials", "true") client.SetHeader("Content-Type", jsonType) client.SetHeader("Accept", jsonType) client.SetTimeout(defaultTimeOut) client.SetPreRequestHook(preRequestHook) return &Client{ Client: client, GetAccessKey: getAccessKey, config: config, cache: map[string]sign.HTTPSigner{}, } } // SetDefaultAccessKey use the provided access key for this client when using `WithBackendOrgAccessKey` func (c *Client) SetDefaultAccessKey(sharedKey, secretKey string) *Client { c.DefaultAccessKey = &AccessKey{ SharedKey: sharedKey, SecretKey: secretKey, } return c } func (c *Client) request(ctx context.Context) *Request { req := c.R().SetContext(ctx).ForceContentType(jsonType) return &Request{Request: req, rootOrg: c.config.Root} } func (c *Client) WithAuthentication(ctx context.Context, organization, username, password string) (*Request, *types.SecurityTokenData, error) { if organization == "" || username == "" || password == "" { return nil, nil, bsl.New("invalid credentials") } data := &types.SecurityTokenData{} req := c.request(ctx).SetOrg(organization) resp, err := req.SetBasicAuth(username, password).SetResult(data).Post("security/authentication/login") if err = ValidateResponse(req.Request, resp, err); err != nil { return nil, nil, err } return c.WithAccessToken(ctx, data.Token).SetOrg(organization), data, nil } func (c *Client) WithUserTokenCredentials(ctx context.Context) *Request { user := middleware.ForContext(ctx) return c.WithTokenCredentials(ctx, user) } func (c *Client) WithTokenCredentials(ctx context.Context, user *types.AuthUser) *Request { return c.WithAccessToken(ctx, user.Token).SetOrg(user.Organization) } func (c *Client) WithOktaToken(ctx context.Context, oktaToken string) *Request { return c.WithAccessToken(ctx, oktaToken) } func (c *Client) WithAccessToken(ctx context.Context, token string) *Request { r := c.request(ctx) r.SetAuthScheme(AccessTokenPrefix).SetAuthToken(token) return r } // WithBackendOrgAccessKey use an access key when making the request, cache the key on the client func (c *Client) WithBackendOrgAccessKey(ctx context.Context, organization string) (*Request, error) { var err error if signer := c.cache[organization]; signer != nil { return c.request(context.WithValue(ctx, reqContextKey, signer)).SetOrg(organization), nil } var accessKey *AccessKey if c.DefaultAccessKey != nil { accessKey = c.DefaultAccessKey } else { accessKey, err = c.GetAccessKey(ctx, organization) if err != nil { return nil, fmt.Errorf("organization \"%s\" not found", organization) } } if !accessKey.Valid() { return nil, fmt.Errorf("invalid access key: shared and secret keys must not be empty for organization: %s", organization) } signer, err := sign.NewAccessKeyHTTPSigner(accessKey.SharedKey, accessKey.SecretKey) if err != nil { return nil, err } c.cache[organization] = signer return c.request(context.WithValue(ctx, reqContextKey, c.cache[organization])).SetOrg(organization), nil } // WithRootOrgAccessKey use an access key and root org when making the request, cache the key on the client func (c *Client) WithRootOrgAccessKey(ctx context.Context) (*Request, error) { req, err := c.WithBackendOrgAccessKey(ctx, c.config.Root) if err != nil { return nil, fmt.Errorf("fail to build bsl req from access and secret keys: %w", err) } data := &types.SecurityTokenData{} resp, err := req.SetResult(data).Post("security/authentication/login") if err = ValidateResponse(req.Request, resp, err); err != nil { return nil, fmt.Errorf("fail to get token from bsl auth: %w", err) } return c.WithAccessToken(ctx, data.Token).SetOrg(c.config.Root), nil } func (r *Request) SetPayload(body interface{}) *Request { r.SetBody(body) return r } func (r *Request) SetOrg(organization string) *Request { r.SetHeader(NepOrganization, WithRootOrg(r.rootOrg, organization)) return r } func (r *Request) SetExactOrg(organization string) *Request { r.SetHeader(NepOrganization, organization) return r } func (r *Request) SetOrgID(organization string) *Request { r.SetHeader(NepOrganization, organization) return r } // JSON execute json requests and validate response func (r *Request) JSON(method, path string, v interface{}) error { r.SetResult(v) resp, err := r.Execute(strings.ToUpper(method), path) return ValidateResponse(r.Request, resp, err) } // SyncCatalogBSLData catalog data-sync full func (r *Request) SyncCatalogBSLData(cfg SyncConfig) error { r.SetBody(cfg) return r.Post("catalog/data-sync/full") } // SyncProvisioningBSLData provisioning data-sync full func (r *Request) SyncProvisioningBSLData(cfg SyncConfig) error { r.SetBody(cfg) return r.Post("provisioning/data-sync/full") } // Get execute get request and validate response func (r *Request) Get(path string) error { resp, err := r.Request.Get(path) return ValidateResponse(r.Request, resp, err) } // Post execute post request and validate response func (r *Request) Post(path string) error { resp, err := r.Request.Post(path) return ValidateResponse(r.Request, resp, err) } // Put execute put request and validate response func (r *Request) Put(path string) error { resp, err := r.Request.Put(path) return ValidateResponse(r.Request, resp, err) } // Patch execute patch request and validate response func (r *Request) Patch(path string) error { resp, err := r.Request.Patch(path) return ValidateResponse(r.Request, resp, err) } // Delete execute delete request and validate response func (r *Request) Delete(path string) error { resp, err := r.Request.Delete(path) return ValidateResponse(r.Request, resp, err) } func preRequestHook(_ *resty.Client, request *http.Request) error { request.Header.Set(NepCorrelationID, "edge-backend-"+uuid.New().String()) request.Header.Set(sign.DateHeader, time.Now().UTC().Format(http.TimeFormat)) val := request.Context().Value(reqContextKey) if val != nil { signer := val.(sign.HTTPSigner) request.Header.Del("Authorization") // need to remove Authorization header for request to be signed. if _, err := signer.Sign(request); err != nil { log.Ctx(request.Context()).Err(err).Msg("fail to sign http request with access token") return err } } return nil } func ValidateResponse(r *resty.Request, res *resty.Response, err error) error { ok := res.StatusCode() == 200 || res.StatusCode() == 204 if err == nil && ok { return nil } if err == nil { err = fmt.Errorf("status code: %v", res.StatusCode()) } if res.StatusCode() == http.StatusBadRequest { err = fmt.Errorf("the request failed validation, status code: %v", res.StatusCode()) } return bslerror.Wrap(err). SetStatusCode(res.StatusCode()). SetMethod(res.Request.Method). SetPath(r.RawRequest.URL.Path). SetURL(r.RawRequest.Host). UnmarshalErrorResponse(res.Body()) }