...

Source file src/edge-infra.dev/pkg/edge/bsl/bsl_client.go

Documentation: edge-infra.dev/pkg/edge/bsl

     1  package bsl
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  	"time"
     9  
    10  	"edge-infra.dev/pkg/edge/api/apierror/bsl"
    11  	bslerror "edge-infra.dev/pkg/edge/api/apierror/bsl"
    12  	"edge-infra.dev/pkg/edge/api/bsl/types"
    13  	"edge-infra.dev/pkg/edge/api/middleware"
    14  
    15  	"github.com/NCR-Corporation/ncr-bsp-hmac/go/sign"
    16  	"github.com/go-resty/resty/v2"
    17  	"github.com/google/uuid"
    18  	"github.com/rs/zerolog/log"
    19  )
    20  
    21  const (
    22  	AccessTokenPrefix = "AccessToken"
    23  	NepCorrelationID  = "Nep-Correlation-Id"
    24  	jsonType          = "application/json"
    25  )
    26  
    27  var (
    28  	defaultTimeOut = time.Second * 10
    29  )
    30  
    31  // AccessKey access keys to sign bsl request
    32  type AccessKey struct {
    33  	SharedKey string `json:"sharedKey"`
    34  	SecretKey string `json:"secretKey"`
    35  }
    36  
    37  type SyncConfig struct {
    38  	EntityTypes       []string `json:"entityTypes"`
    39  	EnterpriseUnitIDs []string `json:"enterpriseUnitIds"`
    40  }
    41  
    42  func (a AccessKey) Valid() bool {
    43  	return a.SharedKey != "" && a.SecretKey != ""
    44  }
    45  
    46  type contextKey struct{}
    47  
    48  var reqContextKey = &contextKey{}
    49  
    50  type Client struct {
    51  	*resty.Client
    52  	config           types.BSPConfig
    53  	cache            map[string]sign.HTTPSigner
    54  	GetAccessKey     AccessKeySecret
    55  	DefaultAccessKey *AccessKey
    56  }
    57  
    58  type Request struct {
    59  	*resty.Request
    60  	rootOrg string
    61  }
    62  
    63  func NewBSLClient(config types.BSPConfig, fn ...AccessKeySecret) *Client {
    64  	var getAccessKey AccessKeySecret
    65  	if len(fn) == 1 {
    66  		getAccessKey = fn[0]
    67  	}
    68  	client := resty.New()
    69  	client.SetBaseURL(config.Endpoint)
    70  	client.SetHeader("Access-Control-Allow-Credentials", "true")
    71  	client.SetHeader("Content-Type", jsonType)
    72  	client.SetHeader("Accept", jsonType)
    73  	client.SetTimeout(defaultTimeOut)
    74  	client.SetPreRequestHook(preRequestHook)
    75  	return &Client{
    76  		Client:       client,
    77  		GetAccessKey: getAccessKey,
    78  		config:       config,
    79  		cache:        map[string]sign.HTTPSigner{},
    80  	}
    81  }
    82  
    83  // SetDefaultAccessKey use the provided access key for this client when using `WithBackendOrgAccessKey`
    84  func (c *Client) SetDefaultAccessKey(sharedKey, secretKey string) *Client {
    85  	c.DefaultAccessKey = &AccessKey{
    86  		SharedKey: sharedKey,
    87  		SecretKey: secretKey,
    88  	}
    89  	return c
    90  }
    91  
    92  func (c *Client) request(ctx context.Context) *Request {
    93  	req := c.R().SetContext(ctx).ForceContentType(jsonType)
    94  	return &Request{Request: req, rootOrg: c.config.Root}
    95  }
    96  
    97  func (c *Client) WithAuthentication(ctx context.Context, organization, username, password string) (*Request, *types.SecurityTokenData, error) {
    98  	if organization == "" || username == "" || password == "" {
    99  		return nil, nil, bsl.New("invalid credentials")
   100  	}
   101  	data := &types.SecurityTokenData{}
   102  	req := c.request(ctx).SetOrg(organization)
   103  	resp, err := req.SetBasicAuth(username, password).SetResult(data).Post("security/authentication/login")
   104  	if err = ValidateResponse(req.Request, resp, err); err != nil {
   105  		return nil, nil, err
   106  	}
   107  	return c.WithAccessToken(ctx, data.Token).SetOrg(organization), data, nil
   108  }
   109  
   110  func (c *Client) WithUserTokenCredentials(ctx context.Context) *Request {
   111  	user := middleware.ForContext(ctx)
   112  	return c.WithTokenCredentials(ctx, user)
   113  }
   114  
   115  func (c *Client) WithTokenCredentials(ctx context.Context, user *types.AuthUser) *Request {
   116  	return c.WithAccessToken(ctx, user.Token).SetOrg(user.Organization)
   117  }
   118  
   119  func (c *Client) WithOktaToken(ctx context.Context, oktaToken string) *Request {
   120  	return c.WithAccessToken(ctx, oktaToken)
   121  }
   122  
   123  func (c *Client) WithAccessToken(ctx context.Context, token string) *Request {
   124  	r := c.request(ctx)
   125  	r.SetAuthScheme(AccessTokenPrefix).SetAuthToken(token)
   126  	return r
   127  }
   128  
   129  // WithBackendOrgAccessKey use an access key when making the request, cache the key on the client
   130  func (c *Client) WithBackendOrgAccessKey(ctx context.Context, organization string) (*Request, error) {
   131  	var err error
   132  	if signer := c.cache[organization]; signer != nil {
   133  		return c.request(context.WithValue(ctx, reqContextKey, signer)).SetOrg(organization), nil
   134  	}
   135  	var accessKey *AccessKey
   136  	if c.DefaultAccessKey != nil {
   137  		accessKey = c.DefaultAccessKey
   138  	} else {
   139  		accessKey, err = c.GetAccessKey(ctx, organization)
   140  		if err != nil {
   141  			return nil, fmt.Errorf("organization \"%s\" not found", organization)
   142  		}
   143  	}
   144  
   145  	if !accessKey.Valid() {
   146  		return nil, fmt.Errorf("invalid access key: shared and secret keys must not be empty for organization: %s", organization)
   147  	}
   148  
   149  	signer, err := sign.NewAccessKeyHTTPSigner(accessKey.SharedKey, accessKey.SecretKey)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	c.cache[organization] = signer
   155  
   156  	return c.request(context.WithValue(ctx, reqContextKey, c.cache[organization])).SetOrg(organization), nil
   157  }
   158  
   159  // WithRootOrgAccessKey use an access key and root org when making the request, cache the key on the client
   160  func (c *Client) WithRootOrgAccessKey(ctx context.Context) (*Request, error) {
   161  	req, err := c.WithBackendOrgAccessKey(ctx, c.config.Root)
   162  	if err != nil {
   163  		return nil, fmt.Errorf("fail to build bsl req from access and secret keys: %w", err)
   164  	}
   165  
   166  	data := &types.SecurityTokenData{}
   167  	resp, err := req.SetResult(data).Post("security/authentication/login")
   168  	if err = ValidateResponse(req.Request, resp, err); err != nil {
   169  		return nil, fmt.Errorf("fail to get token from bsl auth: %w", err)
   170  	}
   171  	return c.WithAccessToken(ctx, data.Token).SetOrg(c.config.Root), nil
   172  }
   173  
   174  func (r *Request) SetPayload(body interface{}) *Request {
   175  	r.SetBody(body)
   176  	return r
   177  }
   178  
   179  func (r *Request) SetOrg(organization string) *Request {
   180  	r.SetHeader(NepOrganization, WithRootOrg(r.rootOrg, organization))
   181  	return r
   182  }
   183  
   184  func (r *Request) SetExactOrg(organization string) *Request {
   185  	r.SetHeader(NepOrganization, organization)
   186  	return r
   187  }
   188  
   189  func (r *Request) SetOrgID(organization string) *Request {
   190  	r.SetHeader(NepOrganization, organization)
   191  	return r
   192  }
   193  
   194  // JSON execute json requests and validate response
   195  func (r *Request) JSON(method, path string, v interface{}) error {
   196  	r.SetResult(v)
   197  	resp, err := r.Execute(strings.ToUpper(method), path)
   198  	return ValidateResponse(r.Request, resp, err)
   199  }
   200  
   201  // SyncCatalogBSLData catalog data-sync full
   202  func (r *Request) SyncCatalogBSLData(cfg SyncConfig) error {
   203  	r.SetBody(cfg)
   204  	return r.Post("catalog/data-sync/full")
   205  }
   206  
   207  // SyncProvisioningBSLData provisioning data-sync full
   208  func (r *Request) SyncProvisioningBSLData(cfg SyncConfig) error {
   209  	r.SetBody(cfg)
   210  	return r.Post("provisioning/data-sync/full")
   211  }
   212  
   213  // Get execute get request and validate response
   214  func (r *Request) Get(path string) error {
   215  	resp, err := r.Request.Get(path)
   216  	return ValidateResponse(r.Request, resp, err)
   217  }
   218  
   219  // Post execute post request and validate response
   220  func (r *Request) Post(path string) error {
   221  	resp, err := r.Request.Post(path)
   222  	return ValidateResponse(r.Request, resp, err)
   223  }
   224  
   225  // Put execute put request and validate response
   226  func (r *Request) Put(path string) error {
   227  	resp, err := r.Request.Put(path)
   228  	return ValidateResponse(r.Request, resp, err)
   229  }
   230  
   231  // Patch execute patch request and validate response
   232  func (r *Request) Patch(path string) error {
   233  	resp, err := r.Request.Patch(path)
   234  	return ValidateResponse(r.Request, resp, err)
   235  }
   236  
   237  // Delete execute delete request and validate response
   238  func (r *Request) Delete(path string) error {
   239  	resp, err := r.Request.Delete(path)
   240  	return ValidateResponse(r.Request, resp, err)
   241  }
   242  
   243  func preRequestHook(_ *resty.Client, request *http.Request) error {
   244  	request.Header.Set(NepCorrelationID, "edge-backend-"+uuid.New().String())
   245  	request.Header.Set(sign.DateHeader, time.Now().UTC().Format(http.TimeFormat))
   246  	val := request.Context().Value(reqContextKey)
   247  	if val != nil {
   248  		signer := val.(sign.HTTPSigner)
   249  		request.Header.Del("Authorization") // need to remove Authorization header for request to be signed.
   250  		if _, err := signer.Sign(request); err != nil {
   251  			log.Ctx(request.Context()).Err(err).Msg("fail to sign http request with access token")
   252  			return err
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  func ValidateResponse(r *resty.Request, res *resty.Response, err error) error {
   259  	ok := res.StatusCode() == 200 || res.StatusCode() == 204
   260  	if err == nil && ok {
   261  		return nil
   262  	}
   263  	if err == nil {
   264  		err = fmt.Errorf("status code: %v", res.StatusCode())
   265  	}
   266  	if res.StatusCode() == http.StatusBadRequest {
   267  		err = fmt.Errorf("the request failed validation, status code: %v", res.StatusCode())
   268  	}
   269  	return bslerror.Wrap(err).
   270  		SetStatusCode(res.StatusCode()).
   271  		SetMethod(res.Request.Method).
   272  		SetPath(r.RawRequest.URL.Path).
   273  		SetURL(r.RawRequest.Host).
   274  		UnmarshalErrorResponse(res.Body())
   275  }
   276  

View as plain text