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
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
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
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
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
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
202 func (r *Request) SyncCatalogBSLData(cfg SyncConfig) error {
203 r.SetBody(cfg)
204 return r.Post("catalog/data-sync/full")
205 }
206
207
208 func (r *Request) SyncProvisioningBSLData(cfg SyncConfig) error {
209 r.SetBody(cfg)
210 return r.Post("provisioning/data-sync/full")
211 }
212
213
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
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
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
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
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")
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