• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

goto / guardian / 10369614855

13 Aug 2024 12:20PM UTC coverage: 74.419% (-0.005%) from 74.424%
10369614855

Pull #170

github

anjaliagg9791
feat: add support for expression in request body when fetching meatdata sources fro appeal
Pull Request #170: feat: add support for expression in request body when fetching meatda…

18 of 27 new or added lines in 1 file covered. (66.67%)

1 existing line in 1 file now uncovered.

10316 of 13862 relevant lines covered (74.42%)

4.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.22
/core/appeal/service.go
1
package appeal
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "reflect"
10
        "strings"
11
        "sync"
12
        "time"
13

14
        "github.com/go-playground/validator/v10"
15
        "github.com/goto/guardian/core/comment"
16
        "github.com/goto/guardian/core/event"
17
        "github.com/goto/guardian/core/grant"
18
        "github.com/goto/guardian/core/policy"
19
        "github.com/goto/guardian/domain"
20
        "github.com/goto/guardian/pkg/evaluator"
21
        "github.com/goto/guardian/pkg/http"
22
        "github.com/goto/guardian/pkg/log"
23
        "github.com/goto/guardian/plugins/notifiers"
24
        "github.com/goto/guardian/utils"
25
        "github.com/mitchellh/mapstructure"
26
        "golang.org/x/sync/errgroup"
27
)
28

29
const (
30
        AuditKeyBulkInsert     = "appeal.bulkInsert"
31
        AuditKeyUpdate         = "appeal.update"
32
        AuditKeyCancel         = "appeal.cancel"
33
        AuditKeyApprove        = "appeal.approve"
34
        AuditKeyReject         = "appeal.reject"
35
        AuditKeyRevoke         = "appeal.revoke"
36
        AuditKeyExtend         = "appeal.extend"
37
        AuditKeyAddApprover    = "appeal.addApprover"
38
        AuditKeyDeleteApprover = "appeal.deleteApprover"
39

40
        RevokeReasonForExtension = "Automatically revoked for grant extension"
41
        RevokeReasonForOverride  = "Automatically revoked for grant override"
42
)
43

44
var TimeNow = time.Now
45

46
//go:generate mockery --name=repository --exported --with-expecter
47
type repository interface {
48
        BulkUpsert(context.Context, []*domain.Appeal) error
49
        Find(context.Context, *domain.ListAppealsFilter) ([]*domain.Appeal, error)
50
        GetByID(ctx context.Context, id string) (*domain.Appeal, error)
51
        UpdateByID(context.Context, *domain.Appeal) error
52
        Update(context.Context, *domain.Appeal) error
53
        GetAppealsTotalCount(context.Context, *domain.ListAppealsFilter) (int64, error)
54
}
55

56
//go:generate mockery --name=iamManager --exported --with-expecter
57
type iamManager interface {
58
        domain.IAMManager
59
}
60

61
//go:generate mockery --name=notifier --exported --with-expecter
62
type notifier interface {
63
        notifiers.Client
64
}
65

66
//go:generate mockery --name=policyService --exported --with-expecter
67
type policyService interface {
68
        Find(context.Context) ([]*domain.Policy, error)
69
        GetOne(context.Context, string, uint) (*domain.Policy, error)
70
}
71

72
//go:generate mockery --name=approvalService --exported --with-expecter
73
type approvalService interface {
74
        AddApprover(ctx context.Context, approvalID, email string) error
75
        DeleteApprover(ctx context.Context, approvalID, email string) error
76
}
77

78
//go:generate mockery --name=providerService --exported --with-expecter
79
type providerService interface {
80
        Find(context.Context) ([]*domain.Provider, error)
81
        GrantAccess(context.Context, domain.Grant) error
82
        RevokeAccess(context.Context, domain.Grant) error
83
        ValidateAppeal(context.Context, *domain.Appeal, *domain.Provider, *domain.Policy) error
84
        GetPermissions(context.Context, *domain.ProviderConfig, string, string) ([]interface{}, error)
85
        IsExclusiveRoleAssignment(context.Context, string, string) bool
86
}
87

88
//go:generate mockery --name=resourceService --exported --with-expecter
89
type resourceService interface {
90
        Find(context.Context, domain.ListResourcesFilter) ([]*domain.Resource, error)
91
        Get(context.Context, *domain.ResourceIdentifier) (*domain.Resource, error)
92
}
93

94
//go:generate mockery --name=grantService --exported --with-expecter
95
type grantService interface {
96
        List(context.Context, domain.ListGrantsFilter) ([]domain.Grant, error)
97
        Prepare(context.Context, domain.Appeal) (*domain.Grant, error)
98
        Revoke(ctx context.Context, id, actor, reason string, opts ...grant.Option) (*domain.Grant, error)
99
}
100

101
//go:generate mockery --name=auditLogger --exported --with-expecter
102
type auditLogger interface {
103
        Log(ctx context.Context, action string, data interface{}) error
104
}
105

106
type CreateAppealOption func(*createAppealOptions)
107

108
type createAppealOptions struct {
109
        IsAdditionalAppeal bool
110
}
111

112
func CreateWithAdditionalAppeal() CreateAppealOption {
2✔
113
        return func(opts *createAppealOptions) {
6✔
114
                opts.IsAdditionalAppeal = true
4✔
115
        }
4✔
116
}
117

118
type ServiceDeps struct {
119
        Repository      repository
120
        ApprovalService approvalService
121
        ResourceService resourceService
122
        ProviderService providerService
123
        PolicyService   policyService
124
        GrantService    grantService
125
        CommentService  *comment.Service
126
        EventService    *event.Service
127
        IAMManager      iamManager
128

129
        Notifier    notifier
130
        Validator   *validator.Validate
131
        Logger      log.Logger
132
        AuditLogger auditLogger
133
}
134

135
// Service handling the business logics
136
type Service struct {
137
        repo            repository
138
        approvalService approvalService
139
        resourceService resourceService
140
        providerService providerService
141
        policyService   policyService
142
        grantService    grantService
143
        commentService  *comment.Service
144
        eventService    *event.Service
145
        iam             domain.IAMManager
146

147
        notifier    notifier
148
        validator   *validator.Validate
149
        logger      log.Logger
150
        auditLogger auditLogger
151

152
        TimeNow func() time.Time
153
}
154

155
// NewService returns service struct
156
func NewService(deps ServiceDeps) *Service {
102✔
157
        return &Service{
102✔
158
                deps.Repository,
102✔
159
                deps.ApprovalService,
102✔
160
                deps.ResourceService,
102✔
161
                deps.ProviderService,
102✔
162
                deps.PolicyService,
102✔
163
                deps.GrantService,
102✔
164
                deps.CommentService,
102✔
165
                deps.EventService,
102✔
166
                deps.IAMManager,
102✔
167

102✔
168
                deps.Notifier,
102✔
169
                deps.Validator,
102✔
170
                deps.Logger,
102✔
171
                deps.AuditLogger,
102✔
172
                time.Now,
102✔
173
        }
102✔
174
}
102✔
175

176
// GetByID returns one record by id
177
func (s *Service) GetByID(ctx context.Context, id string) (*domain.Appeal, error) {
53✔
178
        if id == "" {
54✔
179
                return nil, ErrAppealIDEmptyParam
1✔
180
        }
1✔
181

182
        if !utils.IsValidUUID(id) {
52✔
183
                return nil, InvalidError{AppealID: id}
×
184
        }
×
185

186
        return s.repo.GetByID(ctx, id)
52✔
187
}
188

189
// Find appeals by filters
190
func (s *Service) Find(ctx context.Context, filters *domain.ListAppealsFilter) ([]*domain.Appeal, error) {
2✔
191
        return s.repo.Find(ctx, filters)
2✔
192
}
2✔
193

194
// Create record
195
func (s *Service) Create(ctx context.Context, appeals []*domain.Appeal, opts ...CreateAppealOption) error {
30✔
196
        createAppealOpts := &createAppealOptions{}
30✔
197
        for _, opt := range opts {
32✔
198
                opt(createAppealOpts)
2✔
199
        }
2✔
200
        isAdditionalAppealCreation := createAppealOpts.IsAdditionalAppeal
30✔
201

30✔
202
        resourceIDs := []string{}
30✔
203
        accountIDs := []string{}
30✔
204
        for _, a := range appeals {
57✔
205
                resourceIDs = append(resourceIDs, a.ResourceID)
27✔
206
                accountIDs = append(accountIDs, a.AccountID)
27✔
207
        }
27✔
208

209
        eg, egctx := errgroup.WithContext(ctx)
30✔
210
        var (
30✔
211
                resources      map[string]*domain.Resource
30✔
212
                providers      map[string]map[string]*domain.Provider
30✔
213
                policies       map[string]map[uint]*domain.Policy
30✔
214
                pendingAppeals map[string]map[string]map[string]*domain.Appeal
30✔
215
        )
30✔
216

30✔
217
        eg.Go(func() error {
60✔
218
                resourcesData, err := s.getResourcesMap(egctx, resourceIDs)
30✔
219
                if err != nil {
31✔
220
                        return fmt.Errorf("error getting resource map: %w", err)
1✔
221
                }
1✔
222
                resources = resourcesData
29✔
223
                return nil
29✔
224
        })
225

226
        eg.Go(func() error {
60✔
227
                providersData, err := s.getProvidersMap(egctx)
30✔
228
                if err != nil {
31✔
229
                        return fmt.Errorf("error getting providers map: %w", err)
1✔
230
                }
1✔
231
                providers = providersData
29✔
232
                return nil
29✔
233
        })
234

235
        eg.Go(func() error {
60✔
236
                policiesData, err := s.getPoliciesMap(egctx)
30✔
237
                if err != nil {
31✔
238
                        return fmt.Errorf("error getting policies map: %w", err)
1✔
239
                }
1✔
240
                policies = policiesData
29✔
241
                return nil
29✔
242
        })
243

244
        eg.Go(func() error {
60✔
245
                pendingAppealsData, err := s.getAppealsMap(egctx, &domain.ListAppealsFilter{
30✔
246
                        Statuses:   []string{domain.AppealStatusPending},
30✔
247
                        AccountIDs: accountIDs,
30✔
248
                })
30✔
249
                if err != nil {
31✔
250
                        return fmt.Errorf("listing pending appeals: %w", err)
1✔
251
                }
1✔
252
                pendingAppeals = pendingAppealsData
29✔
253
                return nil
29✔
254
        })
255

256
        if err := eg.Wait(); err != nil {
34✔
257
                return err
4✔
258
        }
4✔
259

260
        notifications := []domain.Notification{}
26✔
261

26✔
262
        for _, appeal := range appeals {
53✔
263
                appeal.SetDefaults()
27✔
264

27✔
265
                if err := validateAppeal(appeal, pendingAppeals); err != nil {
28✔
266
                        return err
1✔
267
                }
1✔
268
                if err := addResource(appeal, resources); err != nil {
27✔
269
                        return fmt.Errorf("couldn't find resource with id %q: %w", appeal.ResourceID, err)
1✔
270
                }
1✔
271
                provider, err := getProvider(appeal, providers)
25✔
272
                if err != nil {
27✔
273
                        return err
2✔
274
                }
2✔
275

276
                var policy *domain.Policy
23✔
277
                if isAdditionalAppealCreation && appeal.PolicyID != "" && appeal.PolicyVersion != 0 {
24✔
278
                        policy = policies[appeal.PolicyID][appeal.PolicyVersion]
1✔
279
                } else {
23✔
280
                        policy, err = getPolicy(appeal, provider, policies)
22✔
281
                        if err != nil {
25✔
282
                                return err
3✔
283
                        }
3✔
284
                }
285

286
                activeGrant, err := s.findActiveGrant(ctx, appeal)
20✔
287
                if err != nil && err != ErrGrantNotFound {
20✔
288
                        return err
×
289
                }
×
290

291
                if activeGrant != nil {
31✔
292
                        if err := s.checkExtensionEligibility(appeal, provider, policy, activeGrant); err != nil {
14✔
293
                                return err
3✔
294
                        }
3✔
295
                }
296

297
                if err := s.providerService.ValidateAppeal(ctx, appeal, provider, policy); err != nil {
21✔
298
                        return fmt.Errorf("provider validation: %w", err)
4✔
299
                }
4✔
300

301
                strPermissions, err := s.getPermissions(ctx, provider.Config, appeal.Resource.Type, appeal.Role)
13✔
302
                if err != nil {
13✔
303
                        return fmt.Errorf("getting permissions list: %w", err)
×
304
                }
×
305
                appeal.Permissions = strPermissions
13✔
306

13✔
307
                if err := validateAppealDurationConfig(appeal, policy); err != nil {
14✔
308
                        return err
1✔
309
                }
1✔
310

311
                if err := validateAppealOnBehalf(appeal, policy); err != nil {
13✔
312
                        return err
1✔
313
                }
1✔
314

315
                if err := s.populateAppealMetadata(ctx, appeal, policy); err != nil {
11✔
316
                        return fmt.Errorf("getting appeal metadata: %w", err)
×
317
                }
×
318

319
                if err := s.addCreatorDetails(ctx, appeal, policy); err != nil {
11✔
320
                        return fmt.Errorf("getting creator details: %w", err)
×
321
                }
×
322

323
                appeal.Revision = 0
11✔
324
                if err := appeal.ApplyPolicy(policy); err != nil {
11✔
325
                        return err
×
326
                }
×
327

328
                if err := appeal.AdvanceApproval(policy); err != nil {
11✔
329
                        return fmt.Errorf("initializing approvals: %w", err)
×
330
                }
×
331
                appeal.Policy = nil
11✔
332

11✔
333
                for _, approval := range appeal.Approvals {
30✔
334
                        // TODO: direcly check on appeal.Status==domain.AppealStatusApproved instead of manual looping through approvals
19✔
335
                        if approval.Index == len(appeal.Approvals)-1 && (approval.Status == domain.ApprovalStatusApproved || appeal.Status == domain.AppealStatusApproved) {
23✔
336
                                newGrant, prevGrant, err := s.prepareGrant(ctx, appeal)
4✔
337
                                if err != nil {
4✔
338
                                        return fmt.Errorf("preparing grant: %w", err)
×
339
                                }
×
340
                                newGrant.Resource = appeal.Resource
4✔
341
                                appeal.Grant = newGrant
4✔
342
                                if prevGrant != nil {
5✔
343
                                        if _, err := s.grantService.Revoke(ctx, prevGrant.ID, domain.SystemActorName, prevGrant.RevokeReason,
1✔
344
                                                grant.SkipNotifications(),
1✔
345
                                                grant.SkipRevokeAccessInProvider(),
1✔
346
                                        ); err != nil {
1✔
347
                                                return fmt.Errorf("revoking previous grant: %w", err)
×
348
                                        }
×
349
                                }
350

351
                                if err := s.GrantAccessToProvider(ctx, appeal, opts...); err != nil {
4✔
352
                                        return fmt.Errorf("granting access: %w", err)
×
353
                                }
×
354

355
                                notifications = append(notifications, domain.Notification{
4✔
356
                                        User: appeal.CreatedBy,
4✔
357
                                        Labels: map[string]string{
4✔
358
                                                "appeal_id": appeal.ID,
4✔
359
                                        },
4✔
360
                                        Message: domain.NotificationMessage{
4✔
361
                                                Type: domain.NotificationTypeAppealApproved,
4✔
362
                                                Variables: map[string]interface{}{
4✔
363
                                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
4✔
364
                                                        "role":          appeal.Role,
4✔
365
                                                        "account_id":    appeal.AccountID,
4✔
366
                                                        "appeal_id":     appeal.ID,
4✔
367
                                                        "requestor":     appeal.CreatedBy,
4✔
368
                                                },
4✔
369
                                        },
4✔
370
                                })
4✔
371

4✔
372
                                notifications = addOnBehalfApprovedNotification(appeal, notifications)
4✔
373
                        }
374
                }
375
        }
376

377
        if err := s.repo.BulkUpsert(ctx, appeals); err != nil {
11✔
378
                return fmt.Errorf("inserting appeals into db: %w", err)
1✔
379
        }
1✔
380

381
        go func() {
18✔
382
                ctx := context.WithoutCancel(ctx)
9✔
383
                if err := s.auditLogger.Log(ctx, AuditKeyBulkInsert, appeals); err != nil {
9✔
384
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
385
                }
×
386
        }()
387

388
        for _, a := range appeals {
20✔
389
                if a.Status == domain.AppealStatusRejected {
11✔
390
                        var reason string
×
391
                        for _, approval := range a.Approvals {
×
392
                                if approval.Status == domain.ApprovalStatusRejected {
×
393
                                        reason = approval.Reason
×
394
                                        break
×
395
                                }
396
                        }
397

398
                        notifications = append(notifications, domain.Notification{
×
399
                                User: a.CreatedBy,
×
400
                                Labels: map[string]string{
×
401
                                        "appeal_id": a.ID,
×
402
                                },
×
403
                                Message: domain.NotificationMessage{
×
404
                                        Type: domain.NotificationTypeAppealRejected,
×
405
                                        Variables: map[string]interface{}{
×
406
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", a.Resource.Name, a.Resource.ProviderType, a.Resource.URN),
×
407
                                                "role":          a.Role,
×
408
                                                "account_id":    a.AccountID,
×
409
                                                "appeal_id":     a.ID,
×
410
                                                "requestor":     a.CreatedBy,
×
411
                                                "reason":        reason,
×
412
                                        },
×
413
                                },
×
414
                        })
×
415
                }
416

417
                notifications = append(notifications, s.getApprovalNotifications(ctx, a)...)
11✔
418
        }
419

420
        if len(notifications) > 0 {
18✔
421
                go func() {
18✔
422
                        ctx := context.WithoutCancel(ctx)
9✔
423
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
9✔
424
                                for _, err1 := range errs {
×
425
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
426
                                }
×
427
                        }
428
                }()
429
        }
430

431
        return nil
9✔
432
}
433

434
func (s *Service) findActiveGrant(ctx context.Context, a *domain.Appeal) (*domain.Grant, error) {
34✔
435
        grants, err := s.grantService.List(ctx, domain.ListGrantsFilter{
34✔
436
                Statuses:    []string{string(domain.GrantStatusActive)},
34✔
437
                AccountIDs:  []string{a.AccountID},
34✔
438
                ResourceIDs: []string{a.ResourceID},
34✔
439
                Roles:       []string{a.Role},
34✔
440
                OrderBy:     []string{"updated_at:desc"},
34✔
441
        })
34✔
442

34✔
443
        if err != nil {
34✔
444
                return nil, fmt.Errorf("listing active grants: %w", err)
×
445
        }
×
446

447
        if len(grants) == 0 {
48✔
448
                return nil, ErrGrantNotFound
14✔
449
        }
14✔
450

451
        return &grants[0], nil
20✔
452
}
453

454
func addOnBehalfApprovedNotification(appeal *domain.Appeal, notifications []domain.Notification) []domain.Notification {
6✔
455
        if appeal.AccountType == domain.DefaultAppealAccountType && appeal.AccountID != appeal.CreatedBy {
6✔
456
                notifications = append(notifications, domain.Notification{
×
457
                        User: appeal.AccountID,
×
458
                        Labels: map[string]string{
×
459
                                "appeal_id": appeal.ID,
×
460
                        },
×
461
                        Message: domain.NotificationMessage{
×
462
                                Type: domain.NotificationTypeOnBehalfAppealApproved,
×
463
                                Variables: map[string]interface{}{
×
464
                                        "appeal_id":     appeal.ID,
×
465
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
×
466
                                        "role":          appeal.Role,
×
467
                                        "account_id":    appeal.AccountID,
×
468
                                        "requestor":     appeal.CreatedBy,
×
469
                                },
×
470
                        },
×
471
                })
×
472
        }
×
473
        return notifications
6✔
474
}
475

476
func validateAppealDurationConfig(appeal *domain.Appeal, policy *domain.Policy) error {
22✔
477
        // return nil if duration options are not configured for this policy
22✔
478
        if policy.AppealConfig == nil || policy.AppealConfig.DurationOptions == nil {
42✔
479
                return nil
20✔
480
        }
20✔
481
        for _, durationOption := range policy.AppealConfig.DurationOptions {
8✔
482
                if appeal.Options.Duration == durationOption.Value {
6✔
483
                        return nil
×
484
                }
×
485
        }
486

487
        return fmt.Errorf("invalid duration: %w: %q", ErrDurationNotAllowed, appeal.Options.Duration)
2✔
488
}
489

490
func validateAppealOnBehalf(a *domain.Appeal, policy *domain.Policy) error {
20✔
491
        if a.AccountType == domain.DefaultAppealAccountType {
40✔
492
                if policy.AppealConfig != nil && policy.AppealConfig.AllowOnBehalf {
32✔
493
                        return nil
12✔
494
                }
12✔
495
                if a.AccountID != a.CreatedBy {
10✔
496
                        return ErrCannotCreateAppealForOtherUser
2✔
497
                }
2✔
498
        }
499
        return nil
6✔
500
}
501

502
// Patch record
503
func (s *Service) Patch(ctx context.Context, appeal *domain.Appeal) error {
27✔
504
        existingAppeal, err := s.GetByID(ctx, appeal.ID)
27✔
505
        if err != nil {
28✔
506
                return fmt.Errorf("error getting existing appeal: %w", err)
1✔
507
        }
1✔
508

509
        if existingAppeal.Status != domain.AppealStatusPending {
27✔
510
                return fmt.Errorf("%w: unable to edit appeal in status: %q", ErrAppealStatusInvalid, existingAppeal.Status)
1✔
511
        }
1✔
512

513
        isAppealUpdated, err := validatePatchReq(appeal, existingAppeal)
25✔
514
        if err != nil {
25✔
515
                return err
×
516
        }
×
517

518
        if !isAppealUpdated {
26✔
519
                return ErrNoChanges
1✔
520
        }
1✔
521

522
        eg, egctx := errgroup.WithContext(ctx)
24✔
523
        var (
24✔
524
                providers      map[string]map[string]*domain.Provider
24✔
525
                policies       map[string]map[uint]*domain.Policy
24✔
526
                pendingAppeals map[string]map[string]map[string]*domain.Appeal
24✔
527
        )
24✔
528

24✔
529
        eg.Go(func() error {
48✔
530
                if appeal.Resource == nil {
43✔
531
                        resource, err := s.resourceService.Get(egctx, &domain.ResourceIdentifier{ID: appeal.ResourceID})
19✔
532
                        if err != nil {
20✔
533
                                return fmt.Errorf("error getting resource: %w", err)
1✔
534
                        }
1✔
535
                        appeal.Resource = resource
18✔
536
                }
537
                return nil
23✔
538
        })
539

540
        eg.Go(func() error {
48✔
541
                providersData, err := s.getProvidersMap(egctx)
24✔
542
                if err != nil {
25✔
543
                        return fmt.Errorf("error getting providers map: %w", err)
1✔
544
                }
1✔
545
                providers = providersData
23✔
546
                return nil
23✔
547
        })
548

549
        eg.Go(func() error {
48✔
550
                policiesData, err := s.getPoliciesMap(egctx)
24✔
551
                if err != nil {
25✔
552
                        return fmt.Errorf("error getting policies map: %w", err)
1✔
553
                }
1✔
554
                policies = policiesData
23✔
555
                return nil
23✔
556
        })
557

558
        eg.Go(func() error {
48✔
559
                pendingAppealsData, err := s.getAppealsMap(egctx, &domain.ListAppealsFilter{
24✔
560
                        Statuses:   []string{domain.AppealStatusPending},
24✔
561
                        AccountIDs: []string{appeal.AccountID},
24✔
562
                })
24✔
563
                if err != nil {
25✔
564
                        return fmt.Errorf("error while listing pending appeals: %w", err)
1✔
565
                }
1✔
566
                pendingAppeals = pendingAppealsData
23✔
567
                return nil
23✔
568
        })
569

570
        if err := eg.Wait(); err != nil {
28✔
571
                return err
4✔
572
        }
4✔
573

574
        appeal.SetDefaults()
20✔
575

20✔
576
        if appeal.AccountID != existingAppeal.AccountID || appeal.ResourceID != existingAppeal.ResourceID || appeal.Role != existingAppeal.Role {
33✔
577
                if err := validateAppeal(appeal, pendingAppeals); err != nil {
14✔
578
                        return err
1✔
579
                }
1✔
580
        }
581

582
        provider, err := getProvider(appeal, providers)
19✔
583
        if err != nil {
21✔
584
                return err
2✔
585
        }
2✔
586

587
        policy, err := getPolicy(appeal, provider, policies)
17✔
588
        if err != nil {
20✔
589
                return err
3✔
590
        }
3✔
591

592
        activeGrant, err := s.findActiveGrant(ctx, appeal)
14✔
593
        if err != nil && err != ErrGrantNotFound {
14✔
594
                return err
×
595
        }
×
596

597
        if activeGrant != nil {
23✔
598
                if err := s.checkExtensionEligibility(appeal, provider, policy, activeGrant); err != nil {
12✔
599
                        return err
3✔
600
                }
3✔
601
        }
602

603
        if err := s.providerService.ValidateAppeal(ctx, appeal, provider, policy); err != nil {
13✔
604
                return fmt.Errorf("provider validation: %w", err)
2✔
605
        }
2✔
606

607
        strPermissions, err := s.getPermissions(ctx, provider.Config, appeal.Resource.Type, appeal.Role)
9✔
608
        if err != nil {
9✔
609
                return fmt.Errorf("getting permissions list: %w", err)
×
610
        }
×
611
        appeal.Permissions = strPermissions
9✔
612

9✔
613
        if err := validateAppealDurationConfig(appeal, policy); err != nil {
10✔
614
                return err
1✔
615
        }
1✔
616

617
        if err := validateAppealOnBehalf(appeal, policy); err != nil {
9✔
618
                return err
1✔
619
        }
1✔
620

621
        if err := s.populateAppealMetadata(ctx, appeal, policy); err != nil {
7✔
622
                return fmt.Errorf("getting appeal metadata: %w", err)
×
623
        }
×
624

625
        if err := s.addCreatorDetails(ctx, appeal, policy); err != nil {
7✔
626
                return fmt.Errorf("getting creator details: %w", err)
×
627
        }
×
628

629
        // create new approval
630
        appeal.Revision = existingAppeal.Revision + 1
7✔
631
        if err := appeal.ApplyPolicy(policy); err != nil {
7✔
632
                return err
×
633
        }
×
634

635
        if err := appeal.AdvanceApproval(policy); err != nil {
7✔
636
                return fmt.Errorf("initializing approvals: %w", err)
×
637
        }
×
638
        appeal.Policy = nil
7✔
639

7✔
640
        notifications := []domain.Notification{}
7✔
641
        for _, approval := range appeal.Approvals {
19✔
642
                if approval.Index == len(appeal.Approvals)-1 && (approval.Status == domain.ApprovalStatusApproved || appeal.Status == domain.AppealStatusApproved) {
12✔
643
                        newGrant, revokedGrant, err := s.prepareGrant(ctx, appeal)
×
644
                        if err != nil {
×
645
                                return fmt.Errorf("preparing grant: %w", err)
×
646
                        }
×
647
                        newGrant.Resource = appeal.Resource
×
648
                        appeal.Grant = newGrant
×
649
                        if revokedGrant != nil {
×
650
                                if _, err := s.grantService.Revoke(ctx, revokedGrant.ID, domain.SystemActorName, revokedGrant.RevokeReason,
×
651
                                        grant.SkipNotifications(),
×
652
                                        grant.SkipRevokeAccessInProvider(),
×
653
                                ); err != nil {
×
654
                                        return fmt.Errorf("revoking previous grant: %w", err)
×
655
                                }
×
656
                        } else {
×
657
                                if err := s.GrantAccessToProvider(ctx, appeal); err != nil {
×
658
                                        return fmt.Errorf("granting access: %w", err)
×
659
                                }
×
660
                        }
661

662
                        notifications = append(notifications, domain.Notification{
×
663
                                User: appeal.CreatedBy,
×
664
                                Labels: map[string]string{
×
665
                                        "appeal_id": appeal.ID,
×
666
                                },
×
667
                                Message: domain.NotificationMessage{
×
668
                                        Type: domain.NotificationTypeAppealApproved,
×
669
                                        Variables: map[string]interface{}{
×
670
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
×
671
                                                "role":          appeal.Role,
×
672
                                                "account_id":    appeal.AccountID,
×
673
                                                "appeal_id":     appeal.ID,
×
674
                                                "requestor":     appeal.CreatedBy,
×
675
                                        },
×
676
                                },
×
677
                        })
×
678

×
679
                        notifications = addOnBehalfApprovedNotification(appeal, notifications)
×
680
                }
681
        }
682

683
        newApprovals := appeal.Approvals
7✔
684

7✔
685
        // mark previous approvals as stale
7✔
686
        for _, approval := range existingAppeal.Approvals {
19✔
687
                approval.IsStale = true
12✔
688

12✔
689
                // clear approvers so it won't get inserted to db
12✔
690
                // TODO: change Approvers type to Approver[] instead of string[] to keep each ID
12✔
691
                approval.Approvers = []string{}
12✔
692

12✔
693
                appeal.Approvals = append(appeal.Approvals, approval)
12✔
694
        }
12✔
695

696
        if err := s.repo.UpdateByID(ctx, appeal); err != nil {
8✔
697
                return fmt.Errorf("error saving appeal to db: %w", err)
1✔
698
        }
1✔
699

700
        diff, err := appeal.Compare(existingAppeal, appeal.CreatedBy)
6✔
701
        if err != nil {
6✔
702
                return fmt.Errorf("error comparing appeals: %w", err)
×
703
        }
×
704

705
        auditLog := map[string]interface{}{
6✔
706
                "appeal_id": appeal.ID,
6✔
707
                "revision":  appeal.Revision,
6✔
708
                "diff":      diff,
6✔
709
        }
6✔
710
        go func() {
12✔
711
                ctx := context.WithoutCancel(ctx)
6✔
712
                if err := s.auditLogger.Log(ctx, AuditKeyUpdate, auditLog); err != nil {
6✔
713
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
714
                }
×
715
        }()
716

717
        appeal.Approvals = newApprovals
6✔
718
        if appeal.Status == domain.AppealStatusApproved {
6✔
719
                notifications = append(notifications, domain.Notification{
×
720
                        User: appeal.CreatedBy,
×
721
                        Labels: map[string]string{
×
722
                                "appeal_id": appeal.ID,
×
723
                        },
×
724
                        Message: domain.NotificationMessage{
×
725
                                Type: domain.NotificationTypeAppealApproved,
×
726
                                Variables: map[string]interface{}{
×
727
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
×
728
                                        "role":          appeal.Role,
×
729
                                        "account_id":    appeal.AccountID,
×
730
                                        "appeal_id":     appeal.ID,
×
731
                                        "requestor":     appeal.CreatedBy,
×
732
                                },
×
733
                        },
×
734
                })
×
735
                notifications = addOnBehalfApprovedNotification(appeal, notifications)
×
736
        } else if appeal.Status == domain.AppealStatusRejected {
6✔
737
                var reason string
×
738
                for _, approval := range appeal.Approvals {
×
739
                        if approval.Status == domain.ApprovalStatusRejected {
×
740
                                reason = approval.Reason
×
741
                                break
×
742
                        }
743
                }
744
                notifications = append(notifications, domain.Notification{
×
745
                        User: appeal.CreatedBy,
×
746
                        Labels: map[string]string{
×
747
                                "appeal_id": appeal.ID,
×
748
                        },
×
749
                        Message: domain.NotificationMessage{
×
750
                                Type: domain.NotificationTypeAppealRejected,
×
751
                                Variables: map[string]interface{}{
×
752
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
×
753
                                        "role":          appeal.Role,
×
754
                                        "account_id":    appeal.AccountID,
×
755
                                        "appeal_id":     appeal.ID,
×
756
                                        "requestor":     appeal.CreatedBy,
×
757
                                        "reason":        reason,
×
758
                                },
×
759
                        },
×
760
                })
×
761
        } else {
6✔
762
                notifications = append(notifications, s.getApprovalNotifications(ctx, appeal)...)
6✔
763
        }
6✔
764

765
        if len(notifications) > 0 {
12✔
766
                go func() {
12✔
767
                        ctx := context.WithoutCancel(ctx)
6✔
768
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
6✔
769
                                for _, err1 := range errs {
×
770
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
771
                                }
×
772
                        }
773
                }()
774
        }
775

776
        return nil
6✔
777
}
778

779
func validatePatchReq(appeal, existingAppeal *domain.Appeal) (bool, error) {
25✔
780
        var isAppealUpdated bool
25✔
781

25✔
782
        updateField := func(newVal, existingVal string) string {
175✔
783
                if newVal == "" || newVal == existingVal {
286✔
784
                        return existingVal
136✔
785
                }
136✔
786
                isAppealUpdated = true
14✔
787
                return newVal
14✔
788
        }
789

790
        appeal.AccountID = updateField(appeal.AccountID, existingAppeal.AccountID)
25✔
791
        appeal.AccountType = updateField(appeal.AccountType, existingAppeal.AccountType)
25✔
792
        appeal.Description = updateField(appeal.Description, existingAppeal.Description)
25✔
793
        appeal.Role = updateField(appeal.Role, existingAppeal.Role)
25✔
794
        appeal.ResourceID = updateField(appeal.ResourceID, existingAppeal.ResourceID)
25✔
795
        if appeal.ResourceID == existingAppeal.ResourceID {
40✔
796
                appeal.Resource = existingAppeal.Resource
15✔
797
        }
15✔
798

799
        if appeal.Options == nil || reflect.DeepEqual(appeal.Options, existingAppeal.Options) {
40✔
800
                appeal.Options = existingAppeal.Options
15✔
801
        } else {
25✔
802
                isAppealUpdated = true
10✔
803
        }
10✔
804

805
        if appeal.Details == nil || reflect.DeepEqual(appeal.Details, existingAppeal.Details) {
48✔
806
                appeal.Details = existingAppeal.Details
23✔
807
        } else {
25✔
808
                for key, value := range appeal.Details {
5✔
809
                        if existingValue, found := existingAppeal.Details[key]; !found || !reflect.DeepEqual(existingValue, value) {
4✔
810
                                isAppealUpdated = true
1✔
811
                        }
1✔
812
                }
813
        }
814

815
        if appeal.Labels == nil || reflect.DeepEqual(appeal.Labels, existingAppeal.Labels) {
49✔
816
                appeal.Labels = existingAppeal.Labels
24✔
817
        } else {
25✔
818
                isAppealUpdated = true
1✔
819
        }
1✔
820

821
        appeal.CreatedBy = updateField(appeal.CreatedBy, existingAppeal.CreatedBy)
25✔
822
        if appeal.CreatedBy != existingAppeal.CreatedBy {
25✔
823
                return false, fmt.Errorf("not allowed to update creator")
×
824
        }
×
825

826
        appeal.Creator = existingAppeal.Creator
25✔
827
        appeal.Status = existingAppeal.Status
25✔
828

25✔
829
        return isAppealUpdated, nil
25✔
830
}
831

832
// UpdateApproval Approve an approval step
833
func (s *Service) UpdateApproval(ctx context.Context, approvalAction domain.ApprovalAction) (*domain.Appeal, error) {
26✔
834
        if err := approvalAction.Validate(); err != nil {
31✔
835
                return nil, fmt.Errorf("%w: %v", ErrInvalidUpdateApprovalParameter, err)
5✔
836
        }
5✔
837

838
        appeal, err := s.GetByID(ctx, approvalAction.AppealID)
21✔
839
        if err != nil {
23✔
840
                if errors.Is(err, ErrAppealNotFound) {
3✔
841
                        return nil, fmt.Errorf("%w: %q", ErrAppealNotFound, approvalAction.AppealID)
1✔
842
                }
1✔
843
                return nil, err
1✔
844
        }
845

846
        if err := checkIfAppealStatusStillPending(appeal.Status); err != nil {
23✔
847
                return nil, err
4✔
848
        }
4✔
849

850
        currentApproval := appeal.GetApproval(approvalAction.ApprovalName)
15✔
851
        if currentApproval == nil {
16✔
852
                return nil, fmt.Errorf("%w: %q", ErrApprovalNotFound, approvalAction.ApprovalName)
1✔
853
        }
1✔
854

855
        // validate previous approvals status
856
        for i := 0; i < currentApproval.Index; i++ {
25✔
857
                prevApproval := appeal.GetApprovalByIndex(i)
11✔
858
                if prevApproval == nil {
11✔
859
                        return nil, fmt.Errorf("unable to find approval with index %d", i)
×
860
                }
×
861
                if err := checkPreviousApprovalStatus(prevApproval.Status, prevApproval.Name); err != nil {
14✔
862
                        return nil, err
3✔
863
                }
3✔
864
        }
865

866
        // validate current approval status
867
        if err := checkApprovalStatus(currentApproval.Status); err != nil {
15✔
868
                return nil, err
4✔
869
        }
4✔
870
        if !currentApproval.IsExistingApprover(approvalAction.Actor) {
8✔
871
                return nil, ErrActionForbidden
1✔
872
        }
1✔
873

874
        // update approval
875
        currentApproval.Actor = &approvalAction.Actor
6✔
876
        currentApproval.Reason = approvalAction.Reason
6✔
877
        currentApproval.UpdatedAt = TimeNow()
6✔
878
        if approvalAction.Action == domain.AppealActionNameApprove {
10✔
879
                currentApproval.Approve()
4✔
880

4✔
881
                // mark next approval as pending
4✔
882
                nextApproval := appeal.GetApprovalByIndex(currentApproval.Index + 1)
4✔
883
                if nextApproval != nil {
5✔
884
                        nextApproval.Status = domain.ApprovalStatusPending
1✔
885
                }
1✔
886

887
                if appeal.Policy == nil {
8✔
888
                        appeal.Policy, err = s.policyService.GetOne(ctx, appeal.PolicyID, appeal.PolicyVersion)
4✔
889
                        if err != nil {
5✔
890
                                return nil, err
1✔
891
                        }
1✔
892
                }
893
                if err := appeal.AdvanceApproval(appeal.Policy); err != nil {
3✔
894
                        return nil, err
×
895
                }
×
896
        } else if approvalAction.Action == domain.AppealActionNameReject {
4✔
897
                currentApproval.Reject()
2✔
898
                appeal.Reject()
2✔
899

2✔
900
                // mark the rest of approvals as skipped
2✔
901
                i := currentApproval.Index
2✔
902
                for {
5✔
903
                        nextApproval := appeal.GetApprovalByIndex(i + 1)
3✔
904
                        if nextApproval == nil {
5✔
905
                                break
2✔
906
                        }
907
                        nextApproval.Skip()
1✔
908
                        nextApproval.UpdatedAt = TimeNow()
1✔
909
                        i++
1✔
910
                }
911
        } else {
×
912
                return nil, ErrActionInvalidValue
×
913
        }
×
914

915
        // evaluate final appeal status
916
        if appeal.Status == domain.AppealStatusApproved {
7✔
917
                newGrant, prevGrant, err := s.prepareGrant(ctx, appeal)
2✔
918
                if err != nil {
2✔
919
                        return nil, fmt.Errorf("preparing grant: %w", err)
×
920
                }
×
921
                newGrant.Resource = appeal.Resource
2✔
922
                appeal.Grant = newGrant
2✔
923
                if prevGrant != nil {
3✔
924
                        if _, err := s.grantService.Revoke(ctx, prevGrant.ID, domain.SystemActorName, prevGrant.RevokeReason,
1✔
925
                                grant.SkipNotifications(),
1✔
926
                                grant.SkipRevokeAccessInProvider(),
1✔
927
                        ); err != nil {
1✔
928
                                return nil, fmt.Errorf("revoking previous grant: %w", err)
×
929
                        }
×
930
                }
931

932
                if err := s.GrantAccessToProvider(ctx, appeal); err != nil {
2✔
933
                        return nil, fmt.Errorf("granting access: %w", err)
×
934
                }
×
935
        }
936

937
        if err := s.Update(ctx, appeal); err != nil {
5✔
938
                if !errors.Is(err, domain.ErrDuplicateActiveGrant) {
×
939
                        if err := s.providerService.RevokeAccess(ctx, *appeal.Grant); err != nil {
×
940
                                return nil, fmt.Errorf("revoking access: %w", err)
×
941
                        }
×
942
                }
943
                return nil, fmt.Errorf("updating appeal: %w", err)
×
944
        }
945

946
        notifications := []domain.Notification{}
5✔
947
        if appeal.Status == domain.AppealStatusApproved {
7✔
948
                notifications = append(notifications, domain.Notification{
2✔
949
                        User: appeal.CreatedBy,
2✔
950
                        Labels: map[string]string{
2✔
951
                                "appeal_id": appeal.ID,
2✔
952
                        },
2✔
953
                        Message: domain.NotificationMessage{
2✔
954
                                Type: domain.NotificationTypeAppealApproved,
2✔
955
                                Variables: map[string]interface{}{
2✔
956
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
2✔
957
                                        "role":          appeal.Role,
2✔
958
                                        "account_id":    appeal.AccountID,
2✔
959
                                        "appeal_id":     appeal.ID,
2✔
960
                                        "requestor":     appeal.CreatedBy,
2✔
961
                                },
2✔
962
                        },
2✔
963
                })
2✔
964
                notifications = addOnBehalfApprovedNotification(appeal, notifications)
2✔
965
        } else if appeal.Status == domain.AppealStatusRejected {
7✔
966
                notifications = append(notifications, domain.Notification{
2✔
967
                        User: appeal.CreatedBy,
2✔
968
                        Labels: map[string]string{
2✔
969
                                "appeal_id": appeal.ID,
2✔
970
                        },
2✔
971
                        Message: domain.NotificationMessage{
2✔
972
                                Type: domain.NotificationTypeAppealRejected,
2✔
973
                                Variables: map[string]interface{}{
2✔
974
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
2✔
975
                                        "role":          appeal.Role,
2✔
976
                                        "account_id":    appeal.AccountID,
2✔
977
                                        "appeal_id":     appeal.ID,
2✔
978
                                        "requestor":     appeal.CreatedBy,
2✔
979
                                },
2✔
980
                        },
2✔
981
                })
2✔
982
        } else {
3✔
983
                notifications = append(notifications, s.getApprovalNotifications(ctx, appeal)...)
1✔
984
        }
1✔
985
        if len(notifications) > 0 {
10✔
986
                go func() {
10✔
987
                        ctx := context.WithoutCancel(ctx)
5✔
988
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
5✔
989
                                for _, err1 := range errs {
×
990
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
991
                                }
×
992
                        }
993
                }()
994
        }
995

996
        var auditKey string
5✔
997
        if approvalAction.Action == string(domain.ApprovalActionReject) {
7✔
998
                auditKey = AuditKeyReject
2✔
999
        } else if approvalAction.Action == string(domain.ApprovalActionApprove) {
8✔
1000
                auditKey = AuditKeyApprove
3✔
1001
        }
3✔
1002
        if auditKey != "" {
10✔
1003
                go func() {
10✔
1004
                        ctx := context.WithoutCancel(ctx)
5✔
1005
                        if err := s.auditLogger.Log(ctx, auditKey, approvalAction); err != nil {
5✔
1006
                                s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1007
                        }
×
1008
                }()
1009
        }
1010

1011
        return appeal, nil
5✔
1012
}
1013

1014
func (s *Service) Update(ctx context.Context, appeal *domain.Appeal) error {
5✔
1015
        return s.repo.Update(ctx, appeal)
5✔
1016
}
5✔
1017

1018
func (s *Service) Cancel(ctx context.Context, id string) (*domain.Appeal, error) {
2✔
1019
        if id == "" {
3✔
1020
                return nil, ErrAppealIDEmptyParam
1✔
1021
        }
1✔
1022

1023
        if !utils.IsValidUUID(id) {
2✔
1024
                return nil, InvalidError{AppealID: id}
1✔
1025
        }
1✔
1026

1027
        appeal, err := s.GetByID(ctx, id)
×
1028
        if err != nil {
×
1029
                return nil, err
×
1030
        }
×
1031

1032
        // TODO: check only appeal creator who is allowed to cancel the appeal
1033

1034
        if err := checkIfAppealStatusStillPending(appeal.Status); err != nil {
×
1035
                return nil, err
×
1036
        }
×
1037

1038
        appeal.Cancel()
×
1039
        if err := s.repo.Update(ctx, appeal); err != nil {
×
1040
                return nil, err
×
1041
        }
×
1042

1043
        go func() {
×
1044
                ctx := context.WithoutCancel(ctx)
×
1045
                if err := s.auditLogger.Log(ctx, AuditKeyCancel, map[string]interface{}{
×
1046
                        "appeal_id": id,
×
1047
                }); err != nil {
×
1048
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1049
                }
×
1050
        }()
1051

1052
        return appeal, nil
×
1053
}
1054

1055
func (s *Service) AddApprover(ctx context.Context, appealID, approvalID, email string) (*domain.Appeal, error) {
14✔
1056
        if err := s.validator.Var(email, "email"); err != nil {
16✔
1057
                return nil, fmt.Errorf("%w: %s", ErrApproverEmail, err)
2✔
1058
        }
2✔
1059

1060
        appeal, approval, err := s.getApproval(ctx, appealID, approvalID)
12✔
1061
        if err != nil {
16✔
1062
                return nil, err
4✔
1063
        }
4✔
1064
        if appeal.Status != domain.AppealStatusPending {
9✔
1065
                return nil, fmt.Errorf("%w: can't add new approver to appeal with %q status", ErrUnableToAddApprover, appeal.Status)
1✔
1066
        }
1✔
1067
        if approval.IsStale {
8✔
1068
                return nil, fmt.Errorf("%w: can't add new approver to a stale approval", ErrUnableToAddApprover)
1✔
1069
        }
1✔
1070
        if approval.IsExistingApprover(email) {
7✔
1071
                return nil, fmt.Errorf("%w: approver %q already exists", ErrUnableToAddApprover, email)
1✔
1072
        }
1✔
1073

1074
        switch approval.Status {
5✔
1075
        case domain.ApprovalStatusPending:
3✔
1076
                break
3✔
1077
        case domain.ApprovalStatusBlocked:
1✔
1078
                // check if approval type is auto
1✔
1079
                // this approach is the quickest way to assume that approval is auto, otherwise need to fetch the policy details and lookup the approval type which takes more time
1✔
1080
                if approval.Approvers == nil || len(approval.Approvers) == 0 {
2✔
1081
                        // approval is automatic (strategy: auto) that is still on blocked
1✔
1082
                        return nil, fmt.Errorf("%w: can't modify approvers for approval with strategy auto", ErrUnableToAddApprover)
1✔
1083
                }
1✔
1084
        default:
1✔
1085
                return nil, fmt.Errorf("%w: can't add approver to approval with %q status", ErrUnableToAddApprover, approval.Status)
1✔
1086
        }
1087

1088
        if err := s.approvalService.AddApprover(ctx, approval.ID, email); err != nil {
4✔
1089
                return nil, fmt.Errorf("adding new approver: %w", err)
1✔
1090
        }
1✔
1091
        approval.Approvers = append(approval.Approvers, email)
2✔
1092

2✔
1093
        auditData, err := utils.StructToMap(approval)
2✔
1094
        if err != nil {
2✔
1095
                return nil, fmt.Errorf("converting approval to map: %w", err)
×
1096
        }
×
1097
        auditData["affected_approver"] = email
2✔
1098
        go func() {
4✔
1099
                ctx := context.WithoutCancel(ctx)
2✔
1100
                if err := s.auditLogger.Log(ctx, AuditKeyAddApprover, auditData); err != nil {
2✔
1101
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1102
                }
×
1103
        }()
1104

1105
        duration := domain.PermanentDurationLabel
2✔
1106
        if !appeal.IsDurationEmpty() {
2✔
1107
                duration, err = utils.GetReadableDuration(appeal.Options.Duration)
×
1108
                if err != nil {
×
1109
                        s.logger.Error(ctx, "failed to get readable duration", "error", err, "appeal_id", appeal.ID)
×
1110
                }
×
1111
        }
1112

1113
        go func() {
4✔
1114
                ctx := context.WithoutCancel(ctx)
2✔
1115
                if errs := s.notifier.Notify(ctx, []domain.Notification{
2✔
1116
                        {
2✔
1117
                                User: email,
2✔
1118
                                Labels: map[string]string{
2✔
1119
                                        "appeal_id": appeal.ID,
2✔
1120
                                },
2✔
1121
                                Message: domain.NotificationMessage{
2✔
1122
                                        Type: domain.NotificationTypeApproverNotification,
2✔
1123
                                        Variables: map[string]interface{}{
2✔
1124
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
2✔
1125
                                                "role":          appeal.Role,
2✔
1126
                                                "requestor":     appeal.CreatedBy,
2✔
1127
                                                "appeal_id":     appeal.ID,
2✔
1128
                                                "account_id":    appeal.AccountID,
2✔
1129
                                                "account_type":  appeal.AccountType,
2✔
1130
                                                "provider_type": appeal.Resource.ProviderType,
2✔
1131
                                                "resource_type": appeal.Resource.Type,
2✔
1132
                                                "created_at":    appeal.CreatedAt,
2✔
1133
                                                "approval_step": approval.Name,
2✔
1134
                                                "actor":         email,
2✔
1135
                                                "details":       appeal.Details,
2✔
1136
                                                "duration":      duration,
2✔
1137
                                                "creator":       appeal.Creator,
2✔
1138
                                        },
2✔
1139
                                },
2✔
1140
                        },
2✔
1141
                }); errs != nil {
2✔
1142
                        for _, err1 := range errs {
×
1143
                                s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
1144
                        }
×
1145
                }
1146
        }()
1147

1148
        return appeal, nil
2✔
1149
}
1150

1151
func (s *Service) DeleteApprover(ctx context.Context, appealID, approvalID, email string) (*domain.Appeal, error) {
13✔
1152
        if err := s.validator.Var(email, "email"); err != nil {
15✔
1153
                return nil, fmt.Errorf("%w: %s", ErrApproverEmail, err)
2✔
1154
        }
2✔
1155

1156
        appeal, approval, err := s.getApproval(ctx, appealID, approvalID)
11✔
1157
        if err != nil {
14✔
1158
                return nil, err
3✔
1159
        }
3✔
1160
        if appeal.Status != domain.AppealStatusPending {
9✔
1161
                return nil, fmt.Errorf("%w: can't delete approver to appeal with %q status", ErrUnableToDeleteApprover, appeal.Status)
1✔
1162
        }
1✔
1163
        if approval.IsStale {
8✔
1164
                return nil, fmt.Errorf("%w: can't delete approver in a stale approval", ErrUnableToDeleteApprover)
1✔
1165
        }
1✔
1166

1167
        switch approval.Status {
6✔
1168
        case domain.ApprovalStatusPending:
3✔
1169
                break
3✔
1170
        case domain.ApprovalStatusBlocked:
2✔
1171
                // check if approval type is auto
2✔
1172
                // this approach is the quickest way to assume that approval is auto, otherwise need to fetch the policy details and lookup the approval type which takes more time
2✔
1173
                if approval.Approvers == nil || len(approval.Approvers) == 0 {
3✔
1174
                        // approval is automatic (strategy: auto) that is still on blocked
1✔
1175
                        return nil, fmt.Errorf("%w: can't modify approvers for approval with strategy auto", ErrUnableToDeleteApprover)
1✔
1176
                }
1✔
1177
        default:
1✔
1178
                return nil, fmt.Errorf("%w: can't delete approver to approval with %q status", ErrUnableToDeleteApprover, approval.Status)
1✔
1179
        }
1180

1181
        if len(approval.Approvers) == 1 {
5✔
1182
                return nil, fmt.Errorf("%w: can't delete if there's only one approver", ErrUnableToDeleteApprover)
1✔
1183
        }
1✔
1184

1185
        if err := s.approvalService.DeleteApprover(ctx, approvalID, email); err != nil {
4✔
1186
                return nil, err
1✔
1187
        }
1✔
1188

1189
        var newApprovers []string
2✔
1190
        for _, a := range approval.Approvers {
6✔
1191
                if a != email {
6✔
1192
                        newApprovers = append(newApprovers, a)
2✔
1193
                }
2✔
1194
        }
1195
        approval.Approvers = newApprovers
2✔
1196

2✔
1197
        auditData, err := utils.StructToMap(approval)
2✔
1198
        if err != nil {
2✔
1199
                return nil, fmt.Errorf("converting approval to map: %w", err)
×
1200
        }
×
1201
        auditData["affected_approver"] = email
2✔
1202
        go func() {
4✔
1203
                ctx := context.WithoutCancel(ctx)
2✔
1204
                if err := s.auditLogger.Log(ctx, AuditKeyDeleteApprover, auditData); err != nil {
2✔
1205
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1206
                }
×
1207
        }()
1208

1209
        return appeal, nil
2✔
1210
}
1211

1212
func (s *Service) getApproval(ctx context.Context, appealID, approvalID string) (*domain.Appeal, *domain.Approval, error) {
23✔
1213
        if appealID == "" {
25✔
1214
                return nil, nil, ErrAppealIDEmptyParam
2✔
1215
        }
2✔
1216
        if approvalID == "" {
23✔
1217
                return nil, nil, ErrApprovalIDEmptyParam
2✔
1218
        }
2✔
1219

1220
        appeal, err := s.repo.GetByID(ctx, appealID)
19✔
1221
        if err != nil {
21✔
1222
                return nil, nil, fmt.Errorf("getting appeal details: %w", err)
2✔
1223
        }
2✔
1224

1225
        approval := appeal.GetApproval(approvalID)
17✔
1226
        if approval == nil {
18✔
1227
                return nil, nil, ErrApprovalNotFound
1✔
1228
        }
1✔
1229

1230
        return appeal, approval, nil
16✔
1231
}
1232

1233
// getAppealsMap returns map[account_id]map[resource_id]map[role]*domain.Appeal, error
1234
func (s *Service) getAppealsMap(ctx context.Context, filters *domain.ListAppealsFilter) (map[string]map[string]map[string]*domain.Appeal, error) {
54✔
1235
        appeals, err := s.repo.Find(ctx, filters)
54✔
1236
        if err != nil {
56✔
1237
                return nil, err
2✔
1238
        }
2✔
1239

1240
        appealsMap := map[string]map[string]map[string]*domain.Appeal{}
52✔
1241
        for _, a := range appeals {
54✔
1242
                accountID := strings.ToLower(a.AccountID)
2✔
1243
                if appealsMap[accountID] == nil {
4✔
1244
                        appealsMap[accountID] = map[string]map[string]*domain.Appeal{}
2✔
1245
                }
2✔
1246
                if appealsMap[accountID][a.ResourceID] == nil {
4✔
1247
                        appealsMap[accountID][a.ResourceID] = map[string]*domain.Appeal{}
2✔
1248
                }
2✔
1249
                appealsMap[accountID][a.ResourceID][a.Role] = a
2✔
1250
        }
1251

1252
        return appealsMap, nil
52✔
1253
}
1254

1255
func (s *Service) getResourcesMap(ctx context.Context, ids []string) (map[string]*domain.Resource, error) {
30✔
1256
        filters := domain.ListResourcesFilter{IDs: ids}
30✔
1257
        resources, err := s.resourceService.Find(ctx, filters)
30✔
1258
        if err != nil {
31✔
1259
                return nil, err
1✔
1260
        }
1✔
1261

1262
        result := map[string]*domain.Resource{}
29✔
1263
        for _, r := range resources {
57✔
1264
                result[r.ID] = r
28✔
1265
        }
28✔
1266

1267
        return result, nil
29✔
1268
}
1269

1270
func (s *Service) getProvidersMap(ctx context.Context) (map[string]map[string]*domain.Provider, error) {
54✔
1271
        providers, err := s.providerService.Find(ctx)
54✔
1272
        if err != nil {
56✔
1273
                return nil, err
2✔
1274
        }
2✔
1275

1276
        providersMap := map[string]map[string]*domain.Provider{}
52✔
1277
        for _, p := range providers {
95✔
1278
                providerType := p.Type
43✔
1279
                providerURN := p.URN
43✔
1280
                if providersMap[providerType] == nil {
86✔
1281
                        providersMap[providerType] = map[string]*domain.Provider{}
43✔
1282
                }
43✔
1283
                if providersMap[providerType][providerURN] == nil {
86✔
1284
                        providersMap[providerType][providerURN] = p
43✔
1285
                }
43✔
1286
        }
1287

1288
        return providersMap, nil
52✔
1289
}
1290

1291
func (s *Service) getPoliciesMap(ctx context.Context) (map[string]map[uint]*domain.Policy, error) {
54✔
1292
        policies, err := s.policyService.Find(ctx)
54✔
1293
        if err != nil {
56✔
1294
                return nil, err
2✔
1295
        }
2✔
1296

1297
        policiesMap := map[string]map[uint]*domain.Policy{}
52✔
1298
        for _, p := range policies {
100✔
1299
                id := p.ID
48✔
1300
                if policiesMap[id] == nil {
94✔
1301
                        policiesMap[id] = map[uint]*domain.Policy{}
46✔
1302
                }
46✔
1303
                policiesMap[id][p.Version] = p
48✔
1304
                // set policiesMap[id][0] to latest policy version
48✔
1305
                if policiesMap[id][0] == nil || p.Version > policiesMap[id][0].Version {
94✔
1306
                        policiesMap[id][0] = p
46✔
1307
                }
46✔
1308
        }
1309

1310
        return policiesMap, nil
52✔
1311
}
1312

1313
func (s *Service) getApprovalNotifications(ctx context.Context, appeal *domain.Appeal) []domain.Notification {
18✔
1314
        notifications := []domain.Notification{}
18✔
1315
        approval := appeal.GetNextPendingApproval()
18✔
1316

18✔
1317
        duration := domain.PermanentDurationLabel
18✔
1318
        var err error
18✔
1319
        if !appeal.IsDurationEmpty() {
23✔
1320
                duration, err = utils.GetReadableDuration(appeal.Options.Duration)
5✔
1321
                if err != nil {
5✔
1322
                        s.logger.Error(ctx, "failed to get readable duration", "error", err, "appeal_id", appeal.ID)
×
1323
                }
×
1324
        }
1325

1326
        if approval != nil {
32✔
1327
                for _, approver := range approval.Approvers {
29✔
1328
                        notifications = append(notifications, domain.Notification{
15✔
1329
                                User: approver,
15✔
1330
                                Labels: map[string]string{
15✔
1331
                                        "appeal_id": appeal.ID,
15✔
1332
                                },
15✔
1333
                                Message: domain.NotificationMessage{
15✔
1334
                                        Type: domain.NotificationTypeApproverNotification,
15✔
1335
                                        Variables: map[string]interface{}{
15✔
1336
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
15✔
1337
                                                "role":          appeal.Role,
15✔
1338
                                                "requestor":     appeal.CreatedBy,
15✔
1339
                                                "appeal_id":     appeal.ID,
15✔
1340
                                                "account_id":    appeal.AccountID,
15✔
1341
                                                "account_type":  appeal.AccountType,
15✔
1342
                                                "provider_type": appeal.Resource.ProviderType,
15✔
1343
                                                "resource_type": appeal.Resource.Type,
15✔
1344
                                                "created_at":    appeal.CreatedAt,
15✔
1345
                                                "approval_step": approval.Name,
15✔
1346
                                                "actor":         approver,
15✔
1347
                                                "details":       appeal.Details,
15✔
1348
                                                "duration":      duration,
15✔
1349
                                                "creator":       appeal.Creator,
15✔
1350
                                        },
15✔
1351
                                },
15✔
1352
                        })
15✔
1353
                }
15✔
1354
        }
1355
        return notifications
18✔
1356
}
1357

1358
func checkIfAppealStatusStillPending(status string) error {
19✔
1359
        switch status {
19✔
1360
        case domain.AppealStatusPending:
15✔
1361
                return nil
15✔
1362
        case
1363
                domain.AppealStatusCanceled,
1364
                domain.AppealStatusApproved,
1365
                domain.AppealStatusRejected:
3✔
1366
                return fmt.Errorf("%w: %q", ErrAppealNotEligibleForApproval, status)
3✔
1367
        default:
1✔
1368
                return fmt.Errorf("%w: %q", ErrAppealStatusUnrecognized, status)
1✔
1369
        }
1370
}
1371

1372
func checkPreviousApprovalStatus(status, name string) error {
11✔
1373
        switch status {
11✔
1374
        case
1375
                domain.ApprovalStatusApproved,
1376
                domain.ApprovalStatusSkipped:
8✔
1377
                return nil
8✔
1378
        case
1379
                domain.ApprovalStatusBlocked,
1380
                domain.ApprovalStatusPending,
1381
                domain.ApprovalStatusRejected:
2✔
1382
                return fmt.Errorf("%w: found previous approval %q with status %q", ErrApprovalNotEligibleForAction, name, status)
2✔
1383
        default:
1✔
1384
                return fmt.Errorf("%w: found previous approval %q with unrecognized status %q", ErrApprovalStatusUnrecognized, name, status)
1✔
1385
        }
1386
}
1387

1388
func checkApprovalStatus(status string) error {
11✔
1389
        switch status {
11✔
1390
        case domain.ApprovalStatusPending:
7✔
1391
                return nil
7✔
1392
        case
1393
                domain.ApprovalStatusBlocked,
1394
                domain.ApprovalStatusApproved,
1395
                domain.ApprovalStatusRejected,
1396
                domain.ApprovalStatusSkipped:
3✔
1397
                return fmt.Errorf("%w: approval status %q is not actionable", ErrApprovalNotEligibleForAction, status)
3✔
1398
        default:
1✔
1399
                return fmt.Errorf("%w: %q", ErrApprovalStatusUnrecognized, status)
1✔
1400
        }
1401
}
1402

1403
func (s *Service) handleAppealRequirements(ctx context.Context, a *domain.Appeal, p *domain.Policy) error {
7✔
1404
        if p.Requirements != nil && len(p.Requirements) > 0 {
9✔
1405
                g, ctx := errgroup.WithContext(ctx)
2✔
1406

2✔
1407
                for reqIndex, r := range p.Requirements {
5✔
1408
                        isAppealMatchesRequirement, err := r.On.IsMatch(a)
3✔
1409
                        if err != nil {
4✔
1410
                                return fmt.Errorf("evaluating requirements[%v]: %v", reqIndex, err)
1✔
1411
                        }
1✔
1412
                        if !isAppealMatchesRequirement {
3✔
1413
                                continue
1✔
1414
                        }
1415

1416
                        for _, aa := range r.Appeals {
2✔
1417
                                aa := aa // https://golang.org/doc/faq#closures_and_goroutines
1✔
1418
                                g.Go(func() error {
2✔
1419
                                        // TODO: populate resource data from policyService
1✔
1420
                                        resource, err := s.resourceService.Get(ctx, aa.Resource)
1✔
1421
                                        if err != nil {
1✔
1422
                                                return fmt.Errorf("retrieving resource: %v", err)
×
1423
                                        }
×
1424

1425
                                        additionalAppeal := &domain.Appeal{
1✔
1426
                                                AccountID:   a.AccountID,
1✔
1427
                                                AccountType: a.AccountType,
1✔
1428
                                                CreatedBy:   a.CreatedBy,
1✔
1429
                                                Role:        aa.Role,
1✔
1430
                                                ResourceID:  resource.ID,
1✔
1431
                                        }
1✔
1432
                                        if aa.Options != nil {
1✔
1433
                                                additionalAppeal.Options = aa.Options
×
1434
                                        }
×
1435
                                        if aa.Policy != nil {
1✔
1436
                                                additionalAppeal.PolicyID = aa.Policy.ID
×
1437
                                                additionalAppeal.PolicyVersion = uint(aa.Policy.Version)
×
1438
                                        }
×
1439
                                        if err := s.Create(ctx, []*domain.Appeal{additionalAppeal}, CreateWithAdditionalAppeal()); err != nil {
1✔
1440
                                                if errors.Is(err, ErrAppealDuplicate) {
×
1441
                                                        s.logger.Warn(ctx, "creating additional appeals, duplicate appeal error log", "error", err)
×
1442
                                                        return nil
×
1443
                                                }
×
1444
                                                return fmt.Errorf("creating additional appeals: %w", err)
×
1445
                                        }
1446
                                        return nil
1✔
1447
                                })
1448
                        }
1449
                }
1450
                if err := g.Wait(); err == nil {
2✔
1451
                        return err
1✔
1452
                }
1✔
1453
        }
1454
        return nil
5✔
1455
}
1456

1457
func (s *Service) GrantAccessToProvider(ctx context.Context, a *domain.Appeal, opts ...CreateAppealOption) error {
10✔
1458
        policy := a.Policy
10✔
1459
        if policy == nil {
18✔
1460
                p, err := s.policyService.GetOne(ctx, a.PolicyID, a.PolicyVersion)
8✔
1461
                if err != nil {
9✔
1462
                        return fmt.Errorf("retrieving policy: %w", err)
1✔
1463
                }
1✔
1464
                policy = p
7✔
1465
        }
1466

1467
        createAppealOpts := &createAppealOptions{}
9✔
1468
        for _, opt := range opts {
11✔
1469
                opt(createAppealOpts)
2✔
1470
        }
2✔
1471

1472
        isAdditionalAppealCreation := createAppealOpts.IsAdditionalAppeal
9✔
1473
        if !isAdditionalAppealCreation {
16✔
1474
                if err := s.handleAppealRequirements(ctx, a, policy); err != nil {
8✔
1475
                        return fmt.Errorf("handling appeal requirements: %w", err)
1✔
1476
                }
1✔
1477
        }
1478

1479
        if err := s.providerService.GrantAccess(ctx, *a.Grant); err != nil {
9✔
1480
                return fmt.Errorf("granting access: %w", err)
1✔
1481
        }
1✔
1482

1483
        return nil
7✔
1484
}
1485

1486
func (s *Service) checkExtensionEligibility(a *domain.Appeal, p *domain.Provider, policy *domain.Policy, activeGrant *domain.Grant) error {
20✔
1487
        allowActiveAccessExtensionIn := ""
20✔
1488

20✔
1489
        // Default to use provider config if policy config is not set
20✔
1490
        if p.Config.Appeal != nil {
40✔
1491
                allowActiveAccessExtensionIn = p.Config.Appeal.AllowActiveAccessExtensionIn
20✔
1492
        }
20✔
1493

1494
        // Use policy config if set
1495
        if policy != nil &&
20✔
1496
                policy.AppealConfig != nil &&
20✔
1497
                policy.AppealConfig.AllowActiveAccessExtensionIn != "" {
20✔
1498
                allowActiveAccessExtensionIn = policy.AppealConfig.AllowActiveAccessExtensionIn
×
1499
        }
×
1500

1501
        if allowActiveAccessExtensionIn == "" {
22✔
1502
                return ErrAppealFoundActiveGrant
2✔
1503
        }
2✔
1504

1505
        extensionDurationRule, err := time.ParseDuration(allowActiveAccessExtensionIn)
18✔
1506
        if err != nil {
20✔
1507
                return fmt.Errorf("%w: %q: %v", ErrAppealInvalidExtensionDuration, allowActiveAccessExtensionIn, err)
2✔
1508
        }
2✔
1509

1510
        if !activeGrant.IsEligibleForExtension(extensionDurationRule) {
18✔
1511
                return fmt.Errorf("%w: extension is allowed %q before grant expiration", ErrGrantNotEligibleForExtension, allowActiveAccessExtensionIn)
2✔
1512
        }
2✔
1513
        return nil
14✔
1514
}
1515

1516
func getPolicy(a *domain.Appeal, p *domain.Provider, policiesMap map[string]map[uint]*domain.Policy) (*domain.Policy, error) {
39✔
1517
        var resourceConfig *domain.ResourceConfig
39✔
1518
        for _, rc := range p.Config.Resources {
81✔
1519
                if rc.Type == a.Resource.Type {
79✔
1520
                        resourceConfig = rc
37✔
1521
                        break
37✔
1522
                }
1523
        }
1524
        if resourceConfig == nil {
41✔
1525
                return nil, fmt.Errorf("%w: couldn't find %q resource type in the provider config", ErrInvalidResourceType, a.Resource.Type)
2✔
1526
        }
2✔
1527
        policyConfig := resourceConfig.Policy
37✔
1528

37✔
1529
        policy, ok := policiesMap[policyConfig.ID][uint(policyConfig.Version)]
37✔
1530
        if !ok {
41✔
1531
                return nil, fmt.Errorf("couldn't find details for policy %q: %w", fmt.Sprintf("%s@%v", policyConfig.ID, policyConfig.Version), ErrPolicyNotFound)
4✔
1532
        }
4✔
1533
        return policy, nil
33✔
1534
}
1535

1536
func (s *Service) populateAppealMetadata(ctx context.Context, a *domain.Appeal, p *domain.Policy) error {
18✔
1537
        if !p.HasAppealMetadataSources() {
33✔
1538
                return nil
15✔
1539
        }
15✔
1540

1541
        eg, egctx := errgroup.WithContext(ctx)
3✔
1542
        var mu sync.Mutex
3✔
1543
        appealMetadata := map[string]interface{}{}
3✔
1544
        for key, metadata := range p.AppealConfig.MetadataSources {
6✔
1545
                key, metadata := key, metadata
3✔
1546
                eg.Go(func() error {
6✔
1547
                        switch metadata.Type {
3✔
1548
                        case "http":
3✔
1549
                                var cfg policy.AppealMetadataSourceConfigHTTP
3✔
1550
                                if err := mapstructure.Decode(metadata.Config, &cfg); err != nil {
3✔
1551
                                        return fmt.Errorf("error decoding metadata config: %w", err)
×
1552
                                }
×
1553
                                
1554
                                if cfg.URL == "" {
3✔
1555
                                        return fmt.Errorf("URL cannot be empty for http type")
×
1556
                                }
×
1557

1558
                                var err error
3✔
1559
                                cfg.URL, err = evaluateExpressionWithAppeal(a, cfg.URL)
3✔
1560
                                if err != nil {
3✔
NEW
1561
                                        return err
×
UNCOV
1562
                                }
×
1563

1564
                                cfg.Body, err = evaluateExpressionWithAppeal(a, cfg.Body)
3✔
1565
                                if err != nil {
3✔
NEW
1566
                                        return err
×
NEW
1567
                                }
×
1568
                                clientCreator := &http.HttpClientCreatorStruct{}
3✔
1569
                                metadataCl, err := http.NewHTTPClient(&cfg.HTTPClientConfig, clientCreator)
3✔
1570

3✔
1571
                                if err != nil {
3✔
1572
                                        return fmt.Errorf("key: %s, %w", key, err)
×
1573
                                }
×
1574

1575
                                res, err := metadataCl.MakeRequest(egctx)
3✔
1576
                                if err != nil || (res.StatusCode < 200 && res.StatusCode > 300) {
3✔
1577
                                        if !cfg.AllowFailed {
×
1578
                                                return fmt.Errorf("error fetching resource: %w", err)
×
1579
                                        }
×
1580
                                }
1581

1582
                                body, err := io.ReadAll(res.Body)
3✔
1583
                                if err != nil {
3✔
1584
                                        return fmt.Errorf("error reading response body: %w", err)
×
1585
                                }
×
1586
                                res.Body.Close()
3✔
1587
                                var jsonBody interface{}
3✔
1588
                                err = json.Unmarshal(body, &jsonBody)
3✔
1589
                                if err != nil {
3✔
1590
                                        return fmt.Errorf("error unmarshaling response body: %w", err)
×
1591
                                }
×
1592

1593
                                responseMap := map[string]interface{}{
3✔
1594
                                        "status":      res.Status,
3✔
1595
                                        "status_code": res.StatusCode,
3✔
1596
                                        "headers":     res.Header,
3✔
1597
                                        "body":        jsonBody,
3✔
1598
                                }
3✔
1599
                                params := map[string]interface{}{
3✔
1600
                                        "response": responseMap,
3✔
1601
                                        "appeal":   a,
3✔
1602
                                }
3✔
1603

3✔
1604
                                value, err := metadata.EvaluateValue(params)
3✔
1605
                                if err != nil {
3✔
1606
                                        return fmt.Errorf("error parsing value: %w", err)
×
1607
                                }
×
1608
                                mu.Lock()
3✔
1609
                                appealMetadata[key] = value
3✔
1610
                                mu.Unlock()
3✔
1611
                        case "static":
×
1612
                                params := map[string]interface{}{"appeal": a}
×
1613
                                value, err := metadata.EvaluateValue(params)
×
1614
                                if err != nil {
×
1615
                                        return fmt.Errorf("error parsing value: %w", err)
×
1616
                                }
×
1617
                                mu.Lock()
×
1618
                                appealMetadata[key] = value
×
1619
                                mu.Unlock()
×
1620
                        default:
×
1621
                                return fmt.Errorf("invalid metadata source type")
×
1622
                        }
1623

1624
                        return nil
3✔
1625
                })
1626
        }
1627

1628
        if err := eg.Wait(); err != nil {
3✔
1629
                return err
×
1630
        }
×
1631

1632
        if a.Details == nil {
6✔
1633
                a.Details = map[string]interface{}{}
3✔
1634
        }
3✔
1635
        a.Details[domain.ReservedDetailsKeyPolicyMetadata] = appealMetadata
3✔
1636

3✔
1637
        return nil
3✔
1638
}
1639

1640
func (s *Service) addCreatorDetails(ctx context.Context, a *domain.Appeal, p *domain.Policy) error {
18✔
1641
        if p.IAM == nil {
23✔
1642
                return nil
5✔
1643
        }
5✔
1644

1645
        iamConfig, err := s.iam.ParseConfig(p.IAM)
13✔
1646
        if err != nil {
13✔
1647
                return fmt.Errorf("parsing policy.iam config: %w", err)
×
1648
        }
×
1649
        iamClient, err := s.iam.GetClient(iamConfig)
13✔
1650
        if err != nil {
13✔
1651
                return fmt.Errorf("initializing iam client: %w", err)
×
1652
        }
×
1653

1654
        userDetails, err := iamClient.GetUser(a.CreatedBy)
13✔
1655
        if err != nil {
17✔
1656
                if p.AppealConfig != nil && p.AppealConfig.AllowCreatorDetailsFailure {
8✔
1657
                        s.logger.Warn(ctx, "unable to get creator details", "error", err)
4✔
1658
                        return nil
4✔
1659
                }
4✔
1660
                return fmt.Errorf("unable to get creator details: %w", err)
×
1661
        }
1662

1663
        userDetailsMap, ok := userDetails.(map[string]interface{})
9✔
1664
        if !ok {
9✔
1665
                return nil
×
1666
        }
×
1667

1668
        if p.IAM.Schema == nil {
10✔
1669
                a.Creator = userDetailsMap
1✔
1670
                return nil
1✔
1671
        }
1✔
1672

1673
        creator := map[string]interface{}{}
8✔
1674
        for schemaKey, targetKey := range p.IAM.Schema {
40✔
1675
                if strings.Contains(targetKey, "$response") {
48✔
1676
                        params := map[string]interface{}{
16✔
1677
                                "response": userDetailsMap,
16✔
1678
                        }
16✔
1679
                        v, err := evaluator.Expression(targetKey).EvaluateWithVars(params)
16✔
1680
                        if err != nil {
16✔
1681
                                return fmt.Errorf("evaluating expression: %w", err)
×
1682
                        }
×
1683
                        creator[schemaKey] = v
16✔
1684
                } else {
16✔
1685
                        creator[schemaKey] = userDetailsMap[targetKey]
16✔
1686
                }
16✔
1687
        }
1688

1689
        a.Creator = creator
8✔
1690
        s.logger.Debug(ctx, "added creator details", "creator", creator)
8✔
1691

8✔
1692
        return nil
8✔
1693
}
1694

1695
func addResource(a *domain.Appeal, resourcesMap map[string]*domain.Resource) error {
26✔
1696
        r := resourcesMap[a.ResourceID]
26✔
1697
        if r == nil {
27✔
1698
                return ErrResourceNotFound
1✔
1699
        } else if r.IsDeleted {
26✔
1700
                return ErrResourceDeleted
×
1701
        }
×
1702

1703
        a.Resource = r
25✔
1704
        return nil
25✔
1705
}
1706

1707
func getProvider(a *domain.Appeal, providersMap map[string]map[string]*domain.Provider) (*domain.Provider, error) {
44✔
1708
        provider, ok := providersMap[a.Resource.ProviderType][a.Resource.ProviderURN]
44✔
1709
        if !ok {
48✔
1710
                return nil, fmt.Errorf("couldn't find details for provider %q: %w", a.Resource.ProviderType+" - "+a.Resource.ProviderURN, ErrProviderNotFound)
4✔
1711
        }
4✔
1712
        return provider, nil
40✔
1713
}
1714

1715
func validateAppeal(a *domain.Appeal, pendingAppealsMap map[string]map[string]map[string]*domain.Appeal) error {
40✔
1716
        accountID := strings.ToLower(a.AccountID)
40✔
1717
        if pendingAppealsMap[accountID] != nil &&
40✔
1718
                pendingAppealsMap[accountID][a.ResourceID] != nil &&
40✔
1719
                pendingAppealsMap[accountID][a.ResourceID][a.Role] != nil {
42✔
1720
                return ErrAppealDuplicate
2✔
1721
        }
2✔
1722

1723
        return nil
38✔
1724
}
1725

1726
func (s *Service) getPermissions(ctx context.Context, pc *domain.ProviderConfig, resourceType, role string) ([]string, error) {
22✔
1727
        permissions, err := s.providerService.GetPermissions(ctx, pc, resourceType, role)
22✔
1728
        if err != nil {
22✔
1729
                return nil, err
×
1730
        }
×
1731

1732
        if permissions == nil {
22✔
1733
                return nil, nil
×
1734
        }
×
1735

1736
        strPermissions := []string{}
22✔
1737
        for _, p := range permissions {
40✔
1738
                strPermissions = append(strPermissions, fmt.Sprintf("%s", p))
18✔
1739
        }
18✔
1740
        return strPermissions, nil
22✔
1741
}
1742

1743
// TODO(feature): add relation between new and revoked grant for traceability
1744
func (s *Service) prepareGrant(ctx context.Context, appeal *domain.Appeal) (newGrant *domain.Grant, deactivatedGrant *domain.Grant, err error) {
6✔
1745
        filter := domain.ListGrantsFilter{
6✔
1746
                AccountIDs:  []string{appeal.AccountID},
6✔
1747
                ResourceIDs: []string{appeal.ResourceID},
6✔
1748
                Statuses:    []string{string(domain.GrantStatusActive)},
6✔
1749
                Permissions: appeal.Permissions,
6✔
1750
        }
6✔
1751
        revocationReason := RevokeReasonForExtension
6✔
1752
        if s.providerService.IsExclusiveRoleAssignment(ctx, appeal.Resource.ProviderType, appeal.Resource.Type) {
6✔
1753
                filter.Permissions = nil
×
1754
                revocationReason = RevokeReasonForOverride
×
1755
        }
×
1756

1757
        activeGrants, err := s.grantService.List(ctx, filter)
6✔
1758
        if err != nil {
6✔
1759
                return nil, nil, fmt.Errorf("unable to retrieve existing active grants: %w", err)
×
1760
        }
×
1761

1762
        if len(activeGrants) > 0 {
8✔
1763
                deactivatedGrant = &activeGrants[0]
2✔
1764
                if err := deactivatedGrant.Revoke(domain.SystemActorName, revocationReason); err != nil {
2✔
1765
                        return nil, nil, fmt.Errorf("revoking previous grant: %w", err)
×
1766
                }
×
1767
        }
1768

1769
        if err := appeal.Approve(); err != nil {
6✔
1770
                return nil, nil, fmt.Errorf("activating appeal: %w", err)
×
1771
        }
×
1772

1773
        grant, err := s.grantService.Prepare(ctx, *appeal)
6✔
1774
        if err != nil {
6✔
1775
                return nil, nil, err
×
1776
        }
×
1777

1778
        return grant, deactivatedGrant, nil
6✔
1779
}
1780

1781
func (s *Service) GetAppealsTotalCount(ctx context.Context, filters *domain.ListAppealsFilter) (int64, error) {
2✔
1782
        return s.repo.GetAppealsTotalCount(ctx, filters)
2✔
1783
}
2✔
1784

1785
func evaluateExpressionWithAppeal(a *domain.Appeal, expression string) (string, error) {
6✔
1786
        if expression != "" && strings.Contains(expression, "$appeal") {
9✔
1787
                appealMap, err := a.ToMap()
3✔
1788
                if err != nil {
3✔
NEW
1789
                        return "", fmt.Errorf("error converting appeal to map: %w", err)
×
NEW
1790
                }
×
1791
                params := map[string]interface{}{"appeal": appealMap}
3✔
1792
                evaluated, err := evaluator.Expression(expression).EvaluateWithVars(params)
3✔
1793
                if err != nil {
3✔
NEW
1794
                        return "", fmt.Errorf("error evaluating expression %w", err)
×
NEW
1795
                }
×
1796
                evaluatedStr, ok := evaluated.(string)
3✔
1797
                if !ok {
3✔
NEW
1798
                        return "", fmt.Errorf("expression must evaluate to a string")
×
NEW
1799
                }
×
1800
                return evaluatedStr, nil
3✔
1801
        }
1802
        return expression, nil
3✔
1803
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc