1 package authservice
2
3 import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "net/http"
12 "net/url"
13 "path"
14 "slices"
15
16 "edge-infra.dev/pkg/lib/fog"
17 "edge-infra.dev/pkg/sds/emergencyaccess/apierror"
18 "edge-infra.dev/pkg/sds/emergencyaccess/eaconst"
19 eamiddleware "edge-infra.dev/pkg/sds/emergencyaccess/middleware"
20 "edge-infra.dev/pkg/sds/emergencyaccess/msgdata"
21 "edge-infra.dev/pkg/sds/emergencyaccess/retriever"
22 "edge-infra.dev/pkg/sds/emergencyaccess/types"
23 )
24
25 type Dataset interface {
26 GetProjectAndBannerID(ctx context.Context, banner string) (projectID, bannerID string, err error)
27 GetStoreID(ctx context.Context, store, bannerID string) (storeID string, err error)
28 GetTerminalID(ctx context.Context, terminal, storeID string) (terminalID string, err error)
29 }
30
31 type Retriever interface {
32 Artifact(ctx context.Context, name string, artifactType retriever.ArtifactType) (retriever.Artifact, error)
33 }
34
35 const (
36 defaultValidateComPath = "validatecommand"
37 getEARolesPath = "eaRoles"
38
39
40 httpScheme = "http://"
41 )
42
43 type AuthService struct {
44 ds Dataset
45 retriever Retriever
46
47 rulesEngineHost string
48 rulesEngineURL url.URL
49 validateComPath string
50
51 userServiceHost string
52 userServiceURL url.URL
53 getEARolesPath string
54
55 edgeAPI string
56 }
57
58 func New(config Config, ds Dataset, ret Retriever, opts ...Option) (*AuthService, error) {
59 asOpts := authserviceOpts{}
60 for _, opt := range opts {
61 opt(&asOpts)
62 }
63
64 as := &AuthService{
65 ds: ds,
66 retriever: ret,
67
68 userServiceHost: config.UserServiceHost,
69 getEARolesPath: getEARolesPath,
70
71 rulesEngineHost: config.RulesEngineHost,
72 validateComPath: defaultValidateComPath,
73
74 edgeAPI: config.EdgeAPI,
75 }
76
77 err := as.setHostURLs()
78 if err != nil {
79 return as, err
80 }
81
82 return as, nil
83 }
84
85 func (as *AuthService) setHostURLs() error {
86 addr, err := url.Parse(httpScheme + as.userServiceHost)
87 if err != nil {
88 return err
89 }
90 as.userServiceURL = *addr
91 addr, err = url.Parse(httpScheme + as.rulesEngineHost)
92 if err != nil {
93 return err
94 }
95 as.rulesEngineURL = *addr
96 return nil
97 }
98
99 func (as AuthService) buildEARolesURL(userRoles []string) (eaRolesURL string) {
100 addr := as.userServiceURL
101 addr.Path = path.Join(addr.Path, as.getEARolesPath)
102 values := url.Values{}
103 for _, role := range userRoles {
104 values.Add("role", role)
105 }
106 addr.RawQuery = values.Encode()
107 return addr.String()
108 }
109
110
111
112 func callUserService(ctx context.Context, addr string) (roles []string, err error) {
113 log := fog.FromContext(ctx)
114 req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
115 if err != nil {
116 return nil, err
117 }
118 correlationID := eamiddleware.GetCorrelationID(ctx)
119 req.Header.Set(eamiddleware.CorrelationIDKey, correlationID)
120 log.Info("Invoking userservice", "url", addr)
121 resp, err := http.DefaultClient.Do(req)
122 if err != nil {
123 return nil, err
124 }
125 log.Info("Userservice response received", "url", addr)
126 if resp.StatusCode != http.StatusOK {
127 return nil, fmt.Errorf("user service returned status %s", resp.Status)
128 }
129 defer resp.Body.Close()
130 bytes, err := io.ReadAll(resp.Body)
131 if err != nil {
132 return nil, err
133 } else if len(bytes) == 0 {
134 return nil, nil
135 }
136 err = json.Unmarshal(bytes, &roles)
137 if err != nil {
138 return nil, err
139 }
140 return roles, nil
141 }
142
143 type Validation struct {
144 Valid bool `json:"valid"`
145 }
146
147
148
149
150
151
152
153 func (as AuthService) AuthorizeRequest(ctx context.Context, payload AuthorizeRequestPayload) (msgdata.Request, error) {
154 log := fog.FromContext(ctx)
155
156
157 req, err := msgdata.NewRequest(payload.Request.Data, payload.Request.Attributes)
158 if err != nil {
159 return nil, fmt.Errorf("failed to create structured request from payload: %w", err)
160 }
161
162 user, ok := types.UserFromContext(ctx)
163 if !ok {
164 return nil, errors.New("user struct not found in context")
165 }
166 eaRoles, err := as.getRolesForUser(ctx, user)
167 if err != nil {
168 return nil, err
169 }
170 if err := as.authorizeUser(eaRoles); err != nil {
171 return nil, err
172 }
173
174 err = as.authorizeTarget(user, payload.Target)
175 if err != nil {
176 return nil, err
177 }
178
179 if artifactor, ok := req.(msgdata.Artifactor); ok {
180 if req.RequestType() != eaconst.Executable {
181 return nil, fmt.Errorf("unknown request type when retrieving artifact: %q", req.RequestType())
182 }
183
184 artifact, err := as.retriever.Artifact(ctx, req.CommandToBeAuthorized(), retriever.Executable)
185 if err != nil {
186 return nil, fmt.Errorf("failed to retrieve artifact: %w", err)
187 }
188
189 artifactB64 := base64.StdEncoding.EncodeToString(artifact.Artifact)
190 artifactor.WriteContents(artifactB64)
191 }
192
193
194 addr := as.buildRulesEngineURL(as.validateComPath)
195 commandToBeAuthorized := req.CommandToBeAuthorized()
196 rePayload := RulesEnginePayload{
197 Identity: identity{
198 UserID: user.Username,
199 EAroles: eaRoles,
200 },
201 Target: payload.Target,
202 Command: RulesEngineCommand{
203 Name: commandToBeAuthorized,
204 Type: req.RequestType(),
205 },
206 }
207
208 log.Info("Authorizing request", "commandName", commandToBeAuthorized, "commandType", req.RequestType())
209 val, err := as.callRulesEngine(ctx, addr, rePayload)
210 if err != nil {
211 return nil, err
212 }
213 if !val.Valid {
214 return nil, apierror.E(apierror.ErrUnauthorizedCommand, errors.New("command not authorized for user on target"))
215 }
216
217 return req, nil
218 }
219
220
221
222 func (as AuthService) AuthorizeCommand(ctx context.Context, payload CommandAuthPayload) (Validation, error) {
223 log := fog.FromContext(ctx)
224
225 addr := as.buildRulesEngineURL(as.validateComPath)
226 commandName, err := extractCommandName(payload.Command)
227 if err != nil {
228 return Validation{}, apierror.E(apierror.ErrInvalidCommand, err)
229 }
230 if commandName == "" {
231 return Validation{}, apierror.E(apierror.ErrInvalidCommand, "No command identified")
232 }
233 if payload.AuthDetails.DarkMode {
234 commandName = "dark"
235 }
236
237 log.Info("Extracted command to be authorized", "commandName", commandName)
238
239 user, ok := types.UserFromContext(ctx)
240 if !ok {
241 return Validation{}, fmt.Errorf("user struct not found in context")
242 }
243 eaRoles, err := as.getRolesForUser(ctx, user)
244 if err != nil {
245 return Validation{}, err
246 }
247
248 if err := as.authorizeUser(eaRoles); err != nil {
249 return Validation{}, err
250 }
251
252 err = as.authorizeTarget(user, payload.Target)
253 if err != nil {
254 return Validation{}, err
255 }
256
257 rePayload := RulesEnginePayload{
258 Identity: identity{
259 UserID: user.Username,
260 EAroles: eaRoles,
261 },
262 Target: payload.Target,
263 Command: RulesEngineCommand{
264 Name: commandName,
265 Type: eaconst.Command,
266 },
267 }
268 return as.callRulesEngine(ctx, addr, rePayload)
269 }
270
271 func (as *AuthService) authorizeUser(eaRoles []string) error {
272 if len(eaRoles) == 0 {
273 return apierror.E(apierror.ErrUserMissingRoles, fmt.Errorf("no roles returned from userservice"),
274 "You have no emergency access roles assigned, please contact your administrator.",
275 )
276 }
277
278 return nil
279 }
280
281
282 func (as *AuthService) getRolesForUser(ctx context.Context, user types.User) ([]string, error) {
283 eaRolesURL := as.buildEARolesURL(user.Roles)
284 eaRoles, err := callUserService(ctx, eaRolesURL)
285 if err != nil {
286 return nil, fmt.Errorf("error when getting ea roles: %w", err)
287 }
288 return eaRoles, nil
289 }
290
291 func (as AuthService) buildRulesEngineURL(urlPathElems ...string) string {
292 addr := as.rulesEngineURL
293 for _, p := range urlPathElems {
294 addr.Path = path.Join(addr.Path, p)
295 }
296 return addr.String()
297 }
298
299
300
301 func (as AuthService) callRulesEngine(ctx context.Context, addr string, rePayload RulesEnginePayload) (Validation, error) {
302 log := fog.FromContext(ctx)
303 data, err := json.Marshal(rePayload)
304 if err != nil {
305 return Validation{}, err
306 }
307 req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, bytes.NewBuffer(data))
308 if err != nil {
309 return Validation{}, err
310 }
311
312 correlationID := eamiddleware.GetCorrelationID(ctx)
313 req.Header.Set(eamiddleware.CorrelationIDKey, correlationID)
314 log.Info("Invoking rulesengine", "url", addr)
315
316 resp, err := http.DefaultClient.Do(req)
317 if err != nil {
318 return Validation{}, err
319 }
320 log.Info("Rules Engine response received", "url", addr)
321 if resp.StatusCode != http.StatusOK {
322 err := fmt.Errorf("rules engine returned status %s", resp.Status)
323 return Validation{}, err
324 }
325
326 defer resp.Body.Close()
327 bytes, err := io.ReadAll(resp.Body)
328 if err != nil {
329 return Validation{}, err
330 }
331 var val Validation
332 err = json.Unmarshal(bytes, &val)
333 if err != nil {
334 return Validation{}, err
335 }
336 return val, err
337 }
338
339
340
341 func (as AuthService) AuthorizeTarget(ctx context.Context, target Target) error {
342 user, ok := types.UserFromContext(ctx)
343 if !ok {
344
345 return fmt.Errorf("user struct not in context")
346 }
347 earoles, err := as.getRolesForUser(ctx, user)
348 if err != nil {
349 return err
350 }
351
352 if err := as.authorizeUser(earoles); err != nil {
353 return err
354 }
355
356 return as.authorizeTarget(user, target)
357 }
358
359 func (as AuthService) authorizeTarget(user types.User, target Target) error {
360 if !slices.Contains(user.Banners, target.BannerID) {
361 return apierror.E(apierror.ErrUserNotAuthorized, errors.New("banner not found in user struct"), fmt.Sprintf("User was not assigned access to requested banner: %s", target.BannerID))
362 }
363 return nil
364 }
365
366 func (as AuthService) ResolveTarget(ctx context.Context, payload ResolveTargetPayload) (Target, error) {
367 projectID, bannerID, err := as.ds.GetProjectAndBannerID(ctx, payload.Target.BannerID)
368 if err != nil {
369 return Target{}, err
370 }
371 if bannerID == "" {
372 return Target{}, apierror.E(
373 apierror.ErrInvalidTarget,
374 fmt.Errorf("banner %s not found", payload.Target.BannerID),
375 fmt.Sprintf("Banner %s not found", payload.Target.BannerID),
376 )
377 }
378 if projectID == "" {
379 return Target{}, apierror.E(
380 apierror.ErrInvalidTarget,
381 fmt.Errorf("project not found for banner %s", payload.Target.BannerID),
382 fmt.Sprintf("Project not found for banner %s", payload.Target.BannerID),
383 )
384 }
385
386 storeID, err := as.ds.GetStoreID(ctx, payload.Target.StoreID, bannerID)
387 if err != nil {
388 return Target{}, err
389 }
390 if storeID == "" {
391 return Target{}, apierror.E(
392 apierror.ErrInvalidTarget,
393 fmt.Errorf("store %s not found in given banner %s", payload.Target.StoreID, payload.Target.BannerID),
394 fmt.Sprintf("Store %s not found in given banner %s", payload.Target.StoreID, payload.Target.BannerID),
395 )
396 }
397
398 terminalID, err := as.ds.GetTerminalID(ctx, payload.Target.TerminalID, storeID)
399 if err != nil {
400 return Target{}, err
401 }
402 if terminalID == "" {
403 return Target{}, apierror.E(
404 apierror.ErrInvalidTarget,
405 fmt.Errorf("terminal %s not found in given store %s and banner %s", payload.Target.TerminalID, payload.Target.StoreID, payload.Target.BannerID),
406 fmt.Sprintf("Terminal %s not found in given store %s and banner %s", payload.Target.TerminalID, payload.Target.StoreID, payload.Target.BannerID),
407 )
408 }
409
410 return Target{
411 ProjectID: projectID,
412 BannerID: bannerID,
413 StoreID: storeID,
414 TerminalID: terminalID,
415 }, nil
416 }
417
418 func (as AuthService) AuthorizeUser(ctx context.Context) error {
419 user, ok := types.UserFromContext(ctx)
420 if !ok {
421 return fmt.Errorf("user struct not found in context")
422 }
423
424 eaRoles, err := as.getRolesForUser(ctx, user)
425 if err != nil {
426 return err
427 }
428
429 return as.authorizeUser(eaRoles)
430 }
431
View as plain text