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

goto / guardian / 12304289958

12 Dec 2024 08:30PM UTC coverage: 73.933% (-0.5%) from 74.39%
12304289958

push

github

Ayushi Sharma
fix: update oss client caching logic

10823 of 14639 relevant lines covered (73.93%)

4.8 hits per line

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

76.48
/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
        GetDependencyGrants(context.Context, domain.Grant) ([]*domain.Grant, error)
87
}
88

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

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

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

108
type CreateAppealOption func(*createAppealOptions)
109

110
type createAppealOptions struct {
111
        IsAdditionalAppeal bool
112
}
113

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

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

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

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

149
        notifier    notifier
150
        validator   *validator.Validate
151
        logger      log.Logger
152
        auditLogger auditLogger
153

154
        TimeNow func() time.Time
155
}
156

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

105✔
170
                deps.Notifier,
105✔
171
                deps.Validator,
105✔
172
                deps.Logger,
105✔
173
                deps.AuditLogger,
105✔
174
                time.Now,
105✔
175
        }
105✔
176
}
105✔
177

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

184
        if !utils.IsValidUUID(id) {
54✔
185
                return nil, InvalidError{AppealID: id}
×
186
        }
×
187

188
        return s.repo.GetByID(ctx, id)
54✔
189
}
190

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

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

31✔
204
        resourceIDs := []string{}
31✔
205
        accountIDs := []string{}
31✔
206
        for _, a := range appeals {
59✔
207
                resourceIDs = append(resourceIDs, a.ResourceID)
28✔
208
                accountIDs = append(accountIDs, a.AccountID)
28✔
209
        }
28✔
210

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

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

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

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

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

258
        if err := eg.Wait(); err != nil {
35✔
259
                return err
4✔
260
        }
4✔
261

262
        notifications := []domain.Notification{}
27✔
263

27✔
264
        for _, appeal := range appeals {
55✔
265
                appeal.SetDefaults()
28✔
266

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

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

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

293
                if activeGrant != nil {
33✔
294
                        if err := s.checkExtensionEligibility(appeal, provider, policy, activeGrant); err != nil {
15✔
295
                                return err
3✔
296
                        }
3✔
297
                }
298

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

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

14✔
309
                if err := validateAppealDurationConfig(appeal, policy); err != nil {
15✔
310
                        return err
1✔
311
                }
1✔
312

313
                if err := validateAppealOnBehalf(appeal, policy); err != nil {
14✔
314
                        return err
1✔
315
                }
1✔
316

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

321
                if err := s.populateAppealMetadata(ctx, appeal, policy); err != nil {
13✔
322
                        return fmt.Errorf("getting appeal metadata: %w", err)
1✔
323
                }
1✔
324

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

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

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

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

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

4✔
374
                                notifications = addOnBehalfApprovedNotification(appeal, notifications)
4✔
375
                        }
376
                }
377
        }
378

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

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

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

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

419
                notifications = append(notifications, s.getApprovalNotifications(ctx, a)...)
11✔
420
        }
421

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

433
        return nil
9✔
434
}
435

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

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

449
        if len(grants) == 0 {
49✔
450
                return nil, ErrGrantNotFound
14✔
451
        }
14✔
452

453
        return &grants[0], nil
21✔
454
}
455

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

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

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

492
func validateAppealOnBehalf(a *domain.Appeal, policy *domain.Policy) error {
21✔
493
        if a.AccountType == domain.DefaultAppealAccountType {
42✔
494
                if policy.AppealConfig != nil && policy.AppealConfig.AllowOnBehalf {
34✔
495
                        return nil
13✔
496
                }
13✔
497
                if a.AccountID != a.CreatedBy {
10✔
498
                        return ErrCannotCreateAppealForOtherUser
2✔
499
                }
2✔
500
        }
501
        return nil
6✔
502
}
503

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

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

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

520
        if !isAppealUpdated {
26✔
521
                return ErrNoChanges
1✔
522
        }
1✔
523

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

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

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

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

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

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

576
        appeal.SetDefaults()
20✔
577

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
681
                        notifications = addOnBehalfApprovedNotification(appeal, notifications)
×
682
                }
683
        }
684

685
        newApprovals := appeal.Approvals
7✔
686

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

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

12✔
695
                appeal.Approvals = append(appeal.Approvals, approval)
12✔
696
        }
12✔
697

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

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

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

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

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

778
        return nil
6✔
779
}
780

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

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

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

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

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

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

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

828
        appeal.Creator = existingAppeal.Creator
25✔
829
        appeal.Status = existingAppeal.Status
25✔
830

25✔
831
        return isAppealUpdated, nil
25✔
832
}
833

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

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

848
        if err := checkIfAppealStatusStillPending(appeal.Status); err != nil {
25✔
849
                return nil, err
4✔
850
        }
4✔
851

852
        currentApproval := appeal.GetApproval(approvalAction.ApprovalName)
17✔
853
        if currentApproval == nil {
18✔
854
                return nil, fmt.Errorf("%w: %q", ErrApprovalNotFound, approvalAction.ApprovalName)
1✔
855
        }
1✔
856

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

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

876
        // update approval
877
        currentApproval.Actor = &approvalAction.Actor
8✔
878
        currentApproval.Reason = approvalAction.Reason
8✔
879
        currentApproval.UpdatedAt = TimeNow()
8✔
880
        if approvalAction.Action == domain.AppealActionNameApprove {
13✔
881
                if appeal.Policy == nil {
10✔
882
                        appeal.Policy, err = s.policyService.GetOne(ctx, appeal.PolicyID, appeal.PolicyVersion)
5✔
883
                        if err != nil {
6✔
884
                                return nil, err
1✔
885
                        }
1✔
886
                }
887

888
                policyStep := appeal.Policy.GetStepByName(currentApproval.Name)
4✔
889
                if policyStep == nil {
4✔
890
                        return nil, fmt.Errorf("%w: %q for appeal %q", ErrNoPolicyStepFound, approvalAction.ApprovalName, appeal.ID)
×
891
                }
×
892

893
                // check if user is self approving the appeal
894
                if policyStep.DontAllowSelfApproval {
5✔
895
                        if approvalAction.Actor == appeal.CreatedBy {
2✔
896
                                return nil, ErrSelfApprovalNotAllowed
1✔
897
                        }
1✔
898
                }
899

900
                currentApproval.Approve()
3✔
901

3✔
902
                // mark next approval as pending
3✔
903
                nextApproval := appeal.GetApprovalByIndex(currentApproval.Index + 1)
3✔
904
                if nextApproval != nil {
4✔
905
                        nextApproval.Status = domain.ApprovalStatusPending
1✔
906
                }
1✔
907

908
                if err := appeal.AdvanceApproval(appeal.Policy); err != nil {
3✔
909
                        return nil, err
×
910
                }
×
911
        } else if approvalAction.Action == domain.AppealActionNameReject {
6✔
912
                currentApproval.Reject()
3✔
913
                appeal.Reject()
3✔
914

3✔
915
                // mark the rest of approvals as skipped
3✔
916
                i := currentApproval.Index
3✔
917
                for {
7✔
918
                        nextApproval := appeal.GetApprovalByIndex(i + 1)
4✔
919
                        if nextApproval == nil {
7✔
920
                                break
3✔
921
                        }
922
                        nextApproval.Skip()
1✔
923
                        nextApproval.UpdatedAt = TimeNow()
1✔
924
                        i++
1✔
925
                }
926
        } else {
×
927
                return nil, ErrActionInvalidValue
×
928
        }
×
929

930
        // evaluate final appeal status
931
        if appeal.Status == domain.AppealStatusApproved {
8✔
932
                newGrant, prevGrant, err := s.prepareGrant(ctx, appeal)
2✔
933
                if err != nil {
2✔
934
                        return nil, fmt.Errorf("preparing grant: %w", err)
×
935
                }
×
936
                newGrant.Resource = appeal.Resource
2✔
937
                appeal.Grant = newGrant
2✔
938
                if prevGrant != nil {
3✔
939
                        if _, err := s.grantService.Revoke(ctx, prevGrant.ID, domain.SystemActorName, prevGrant.RevokeReason,
1✔
940
                                grant.SkipNotifications(),
1✔
941
                                grant.SkipRevokeAccessInProvider(),
1✔
942
                        ); err != nil {
1✔
943
                                return nil, fmt.Errorf("revoking previous grant: %w", err)
×
944
                        }
×
945
                }
946

947
                if err := s.GrantAccessToProvider(ctx, appeal); err != nil {
2✔
948
                        return nil, fmt.Errorf("granting access: %w", err)
×
949
                }
×
950
        }
951

952
        if err := s.Update(ctx, appeal); err != nil {
6✔
953
                if !errors.Is(err, domain.ErrDuplicateActiveGrant) {
×
954
                        if err := s.providerService.RevokeAccess(ctx, *appeal.Grant); err != nil {
×
955
                                return nil, fmt.Errorf("revoking access: %w", err)
×
956
                        }
×
957
                }
958
                return nil, fmt.Errorf("updating appeal: %w", err)
×
959
        }
960

961
        notifications := []domain.Notification{}
6✔
962
        if appeal.Status == domain.AppealStatusApproved {
8✔
963
                notifications = append(notifications, domain.Notification{
2✔
964
                        User: appeal.CreatedBy,
2✔
965
                        Labels: map[string]string{
2✔
966
                                "appeal_id": appeal.ID,
2✔
967
                        },
2✔
968
                        Message: domain.NotificationMessage{
2✔
969
                                Type: domain.NotificationTypeAppealApproved,
2✔
970
                                Variables: map[string]interface{}{
2✔
971
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
2✔
972
                                        "role":          appeal.Role,
2✔
973
                                        "account_id":    appeal.AccountID,
2✔
974
                                        "appeal_id":     appeal.ID,
2✔
975
                                        "requestor":     appeal.CreatedBy,
2✔
976
                                },
2✔
977
                        },
2✔
978
                })
2✔
979
                notifications = addOnBehalfApprovedNotification(appeal, notifications)
2✔
980
        } else if appeal.Status == domain.AppealStatusRejected {
9✔
981
                notifications = append(notifications, domain.Notification{
3✔
982
                        User: appeal.CreatedBy,
3✔
983
                        Labels: map[string]string{
3✔
984
                                "appeal_id": appeal.ID,
3✔
985
                        },
3✔
986
                        Message: domain.NotificationMessage{
3✔
987
                                Type: domain.NotificationTypeAppealRejected,
3✔
988
                                Variables: map[string]interface{}{
3✔
989
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
3✔
990
                                        "role":          appeal.Role,
3✔
991
                                        "account_id":    appeal.AccountID,
3✔
992
                                        "appeal_id":     appeal.ID,
3✔
993
                                        "requestor":     appeal.CreatedBy,
3✔
994
                                },
3✔
995
                        },
3✔
996
                })
3✔
997
        } else {
4✔
998
                notifications = append(notifications, s.getApprovalNotifications(ctx, appeal)...)
1✔
999
        }
1✔
1000
        if len(notifications) > 0 {
12✔
1001
                go func() {
12✔
1002
                        ctx := context.WithoutCancel(ctx)
6✔
1003
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
6✔
1004
                                for _, err1 := range errs {
×
1005
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
1006
                                }
×
1007
                        }
1008
                }()
1009
        }
1010

1011
        var auditKey string
6✔
1012
        if approvalAction.Action == string(domain.ApprovalActionReject) {
9✔
1013
                auditKey = AuditKeyReject
3✔
1014
        } else if approvalAction.Action == string(domain.ApprovalActionApprove) {
9✔
1015
                auditKey = AuditKeyApprove
3✔
1016
        }
3✔
1017
        if auditKey != "" {
12✔
1018
                go func() {
12✔
1019
                        ctx := context.WithoutCancel(ctx)
6✔
1020
                        if err := s.auditLogger.Log(ctx, auditKey, approvalAction); err != nil {
6✔
1021
                                s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1022
                        }
×
1023
                }()
1024
        }
1025

1026
        return appeal, nil
6✔
1027
}
1028

1029
func (s *Service) Update(ctx context.Context, appeal *domain.Appeal) error {
6✔
1030
        return s.repo.Update(ctx, appeal)
6✔
1031
}
6✔
1032

1033
func (s *Service) Cancel(ctx context.Context, id string) (*domain.Appeal, error) {
2✔
1034
        if id == "" {
3✔
1035
                return nil, ErrAppealIDEmptyParam
1✔
1036
        }
1✔
1037

1038
        if !utils.IsValidUUID(id) {
2✔
1039
                return nil, InvalidError{AppealID: id}
1✔
1040
        }
1✔
1041

1042
        appeal, err := s.GetByID(ctx, id)
×
1043
        if err != nil {
×
1044
                return nil, err
×
1045
        }
×
1046

1047
        // TODO: check only appeal creator who is allowed to cancel the appeal
1048

1049
        if err := checkIfAppealStatusStillPending(appeal.Status); err != nil {
×
1050
                return nil, err
×
1051
        }
×
1052

1053
        appeal.Cancel()
×
1054
        if err := s.repo.Update(ctx, appeal); err != nil {
×
1055
                return nil, err
×
1056
        }
×
1057

1058
        go func() {
×
1059
                ctx := context.WithoutCancel(ctx)
×
1060
                if err := s.auditLogger.Log(ctx, AuditKeyCancel, map[string]interface{}{
×
1061
                        "appeal_id": id,
×
1062
                }); err != nil {
×
1063
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1064
                }
×
1065
        }()
1066

1067
        return appeal, nil
×
1068
}
1069

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

1075
        appeal, approval, err := s.getApproval(ctx, appealID, approvalID)
12✔
1076
        if err != nil {
16✔
1077
                return nil, err
4✔
1078
        }
4✔
1079
        if appeal.Status != domain.AppealStatusPending {
9✔
1080
                return nil, fmt.Errorf("%w: can't add new approver to appeal with %q status", ErrUnableToAddApprover, appeal.Status)
1✔
1081
        }
1✔
1082
        if approval.IsStale {
8✔
1083
                return nil, fmt.Errorf("%w: can't add new approver to a stale approval", ErrUnableToAddApprover)
1✔
1084
        }
1✔
1085
        if approval.IsExistingApprover(email) {
7✔
1086
                return nil, fmt.Errorf("%w: approver %q already exists", ErrUnableToAddApprover, email)
1✔
1087
        }
1✔
1088

1089
        switch approval.Status {
5✔
1090
        case domain.ApprovalStatusPending:
3✔
1091
                break
3✔
1092
        case domain.ApprovalStatusBlocked:
1✔
1093
                // check if approval type is auto
1✔
1094
                // 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✔
1095
                if approval.Approvers == nil || len(approval.Approvers) == 0 {
2✔
1096
                        // approval is automatic (strategy: auto) that is still on blocked
1✔
1097
                        return nil, fmt.Errorf("%w: can't modify approvers for approval with strategy auto", ErrUnableToAddApprover)
1✔
1098
                }
1✔
1099
        default:
1✔
1100
                return nil, fmt.Errorf("%w: can't add approver to approval with %q status", ErrUnableToAddApprover, approval.Status)
1✔
1101
        }
1102

1103
        if err := s.approvalService.AddApprover(ctx, approval.ID, email); err != nil {
4✔
1104
                return nil, fmt.Errorf("adding new approver: %w", err)
1✔
1105
        }
1✔
1106
        approval.Approvers = append(approval.Approvers, email)
2✔
1107

2✔
1108
        auditData, err := utils.StructToMap(approval)
2✔
1109
        if err != nil {
2✔
1110
                return nil, fmt.Errorf("converting approval to map: %w", err)
×
1111
        }
×
1112
        auditData["affected_approver"] = email
2✔
1113
        go func() {
4✔
1114
                ctx := context.WithoutCancel(ctx)
2✔
1115
                if err := s.auditLogger.Log(ctx, AuditKeyAddApprover, auditData); err != nil {
2✔
1116
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1117
                }
×
1118
        }()
1119

1120
        duration := domain.PermanentDurationLabel
2✔
1121
        if !appeal.IsDurationEmpty() {
2✔
1122
                duration, err = utils.GetReadableDuration(appeal.Options.Duration)
×
1123
                if err != nil {
×
1124
                        s.logger.Error(ctx, "failed to get readable duration", "error", err, "appeal_id", appeal.ID)
×
1125
                }
×
1126
        }
1127

1128
        go func() {
4✔
1129
                ctx := context.WithoutCancel(ctx)
2✔
1130
                if errs := s.notifier.Notify(ctx, []domain.Notification{
2✔
1131
                        {
2✔
1132
                                User: email,
2✔
1133
                                Labels: map[string]string{
2✔
1134
                                        "appeal_id": appeal.ID,
2✔
1135
                                },
2✔
1136
                                Message: domain.NotificationMessage{
2✔
1137
                                        Type: domain.NotificationTypeApproverNotification,
2✔
1138
                                        Variables: map[string]interface{}{
2✔
1139
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
2✔
1140
                                                "role":          appeal.Role,
2✔
1141
                                                "requestor":     appeal.CreatedBy,
2✔
1142
                                                "appeal_id":     appeal.ID,
2✔
1143
                                                "account_id":    appeal.AccountID,
2✔
1144
                                                "account_type":  appeal.AccountType,
2✔
1145
                                                "provider_type": appeal.Resource.ProviderType,
2✔
1146
                                                "resource_type": appeal.Resource.Type,
2✔
1147
                                                "created_at":    appeal.CreatedAt,
2✔
1148
                                                "approval_step": approval.Name,
2✔
1149
                                                "actor":         email,
2✔
1150
                                                "details":       appeal.Details,
2✔
1151
                                                "duration":      duration,
2✔
1152
                                                "creator":       appeal.Creator,
2✔
1153
                                        },
2✔
1154
                                },
2✔
1155
                        },
2✔
1156
                }); errs != nil {
2✔
1157
                        for _, err1 := range errs {
×
1158
                                s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
1159
                        }
×
1160
                }
1161
        }()
1162

1163
        return appeal, nil
2✔
1164
}
1165

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

1171
        appeal, approval, err := s.getApproval(ctx, appealID, approvalID)
11✔
1172
        if err != nil {
14✔
1173
                return nil, err
3✔
1174
        }
3✔
1175
        if appeal.Status != domain.AppealStatusPending {
9✔
1176
                return nil, fmt.Errorf("%w: can't delete approver to appeal with %q status", ErrUnableToDeleteApprover, appeal.Status)
1✔
1177
        }
1✔
1178
        if approval.IsStale {
8✔
1179
                return nil, fmt.Errorf("%w: can't delete approver in a stale approval", ErrUnableToDeleteApprover)
1✔
1180
        }
1✔
1181

1182
        switch approval.Status {
6✔
1183
        case domain.ApprovalStatusPending:
3✔
1184
                break
3✔
1185
        case domain.ApprovalStatusBlocked:
2✔
1186
                // check if approval type is auto
2✔
1187
                // 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✔
1188
                if approval.Approvers == nil || len(approval.Approvers) == 0 {
3✔
1189
                        // approval is automatic (strategy: auto) that is still on blocked
1✔
1190
                        return nil, fmt.Errorf("%w: can't modify approvers for approval with strategy auto", ErrUnableToDeleteApprover)
1✔
1191
                }
1✔
1192
        default:
1✔
1193
                return nil, fmt.Errorf("%w: can't delete approver to approval with %q status", ErrUnableToDeleteApprover, approval.Status)
1✔
1194
        }
1195

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

1200
        if err := s.approvalService.DeleteApprover(ctx, approvalID, email); err != nil {
4✔
1201
                return nil, err
1✔
1202
        }
1✔
1203

1204
        var newApprovers []string
2✔
1205
        for _, a := range approval.Approvers {
6✔
1206
                if a != email {
6✔
1207
                        newApprovers = append(newApprovers, a)
2✔
1208
                }
2✔
1209
        }
1210
        approval.Approvers = newApprovers
2✔
1211

2✔
1212
        auditData, err := utils.StructToMap(approval)
2✔
1213
        if err != nil {
2✔
1214
                return nil, fmt.Errorf("converting approval to map: %w", err)
×
1215
        }
×
1216
        auditData["affected_approver"] = email
2✔
1217
        go func() {
4✔
1218
                ctx := context.WithoutCancel(ctx)
2✔
1219
                if err := s.auditLogger.Log(ctx, AuditKeyDeleteApprover, auditData); err != nil {
2✔
1220
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
1221
                }
×
1222
        }()
1223

1224
        return appeal, nil
2✔
1225
}
1226

1227
func (s *Service) getApproval(ctx context.Context, appealID, approvalID string) (*domain.Appeal, *domain.Approval, error) {
23✔
1228
        if appealID == "" {
25✔
1229
                return nil, nil, ErrAppealIDEmptyParam
2✔
1230
        }
2✔
1231
        if approvalID == "" {
23✔
1232
                return nil, nil, ErrApprovalIDEmptyParam
2✔
1233
        }
2✔
1234

1235
        appeal, err := s.repo.GetByID(ctx, appealID)
19✔
1236
        if err != nil {
21✔
1237
                return nil, nil, fmt.Errorf("getting appeal details: %w", err)
2✔
1238
        }
2✔
1239

1240
        approval := appeal.GetApproval(approvalID)
17✔
1241
        if approval == nil {
18✔
1242
                return nil, nil, ErrApprovalNotFound
1✔
1243
        }
1✔
1244

1245
        return appeal, approval, nil
16✔
1246
}
1247

1248
// getAppealsMap returns map[account_id]map[resource_id]map[role]*domain.Appeal, error
1249
func (s *Service) getAppealsMap(ctx context.Context, filters *domain.ListAppealsFilter) (map[string]map[string]map[string]*domain.Appeal, error) {
55✔
1250
        appeals, err := s.repo.Find(ctx, filters)
55✔
1251
        if err != nil {
57✔
1252
                return nil, err
2✔
1253
        }
2✔
1254

1255
        appealsMap := map[string]map[string]map[string]*domain.Appeal{}
53✔
1256
        for _, a := range appeals {
55✔
1257
                accountID := strings.ToLower(a.AccountID)
2✔
1258
                if appealsMap[accountID] == nil {
4✔
1259
                        appealsMap[accountID] = map[string]map[string]*domain.Appeal{}
2✔
1260
                }
2✔
1261
                if appealsMap[accountID][a.ResourceID] == nil {
4✔
1262
                        appealsMap[accountID][a.ResourceID] = map[string]*domain.Appeal{}
2✔
1263
                }
2✔
1264
                appealsMap[accountID][a.ResourceID][a.Role] = a
2✔
1265
        }
1266

1267
        return appealsMap, nil
53✔
1268
}
1269

1270
func (s *Service) getResourcesMap(ctx context.Context, ids []string) (map[string]*domain.Resource, error) {
31✔
1271
        filters := domain.ListResourcesFilter{IDs: ids}
31✔
1272
        resources, err := s.resourceService.Find(ctx, filters)
31✔
1273
        if err != nil {
32✔
1274
                return nil, err
1✔
1275
        }
1✔
1276

1277
        result := map[string]*domain.Resource{}
30✔
1278
        for _, r := range resources {
60✔
1279
                result[r.ID] = r
30✔
1280
        }
30✔
1281

1282
        return result, nil
30✔
1283
}
1284

1285
func (s *Service) getProvidersMap(ctx context.Context) (map[string]map[string]*domain.Provider, error) {
55✔
1286
        providers, err := s.providerService.Find(ctx)
55✔
1287
        if err != nil {
57✔
1288
                return nil, err
2✔
1289
        }
2✔
1290

1291
        providersMap := map[string]map[string]*domain.Provider{}
53✔
1292
        for _, p := range providers {
97✔
1293
                providerType := p.Type
44✔
1294
                providerURN := p.URN
44✔
1295
                if providersMap[providerType] == nil {
88✔
1296
                        providersMap[providerType] = map[string]*domain.Provider{}
44✔
1297
                }
44✔
1298
                if providersMap[providerType][providerURN] == nil {
88✔
1299
                        providersMap[providerType][providerURN] = p
44✔
1300
                }
44✔
1301
        }
1302

1303
        return providersMap, nil
53✔
1304
}
1305

1306
func (s *Service) getPoliciesMap(ctx context.Context) (map[string]map[uint]*domain.Policy, error) {
55✔
1307
        policies, err := s.policyService.Find(ctx)
55✔
1308
        if err != nil {
57✔
1309
                return nil, err
2✔
1310
        }
2✔
1311

1312
        policiesMap := map[string]map[uint]*domain.Policy{}
53✔
1313
        for _, p := range policies {
102✔
1314
                id := p.ID
49✔
1315
                if policiesMap[id] == nil {
96✔
1316
                        policiesMap[id] = map[uint]*domain.Policy{}
47✔
1317
                }
47✔
1318
                policiesMap[id][p.Version] = p
49✔
1319
                // set policiesMap[id][0] to latest policy version
49✔
1320
                if policiesMap[id][0] == nil || p.Version > policiesMap[id][0].Version {
96✔
1321
                        policiesMap[id][0] = p
47✔
1322
                }
47✔
1323
        }
1324

1325
        return policiesMap, nil
53✔
1326
}
1327

1328
func (s *Service) getApprovalNotifications(ctx context.Context, appeal *domain.Appeal) []domain.Notification {
18✔
1329
        notifications := []domain.Notification{}
18✔
1330
        approval := appeal.GetNextPendingApproval()
18✔
1331

18✔
1332
        duration := domain.PermanentDurationLabel
18✔
1333
        var err error
18✔
1334
        if !appeal.IsDurationEmpty() {
23✔
1335
                duration, err = utils.GetReadableDuration(appeal.Options.Duration)
5✔
1336
                if err != nil {
5✔
1337
                        s.logger.Error(ctx, "failed to get readable duration", "error", err, "appeal_id", appeal.ID)
×
1338
                }
×
1339
        }
1340

1341
        if approval != nil {
32✔
1342
                for _, approver := range approval.Approvers {
29✔
1343
                        notifications = append(notifications, domain.Notification{
15✔
1344
                                User: approver,
15✔
1345
                                Labels: map[string]string{
15✔
1346
                                        "appeal_id": appeal.ID,
15✔
1347
                                },
15✔
1348
                                Message: domain.NotificationMessage{
15✔
1349
                                        Type: domain.NotificationTypeApproverNotification,
15✔
1350
                                        Variables: map[string]interface{}{
15✔
1351
                                                "resource_name": fmt.Sprintf("%s (%s: %s)", appeal.Resource.Name, appeal.Resource.ProviderType, appeal.Resource.URN),
15✔
1352
                                                "role":          appeal.Role,
15✔
1353
                                                "requestor":     appeal.CreatedBy,
15✔
1354
                                                "appeal_id":     appeal.ID,
15✔
1355
                                                "account_id":    appeal.AccountID,
15✔
1356
                                                "account_type":  appeal.AccountType,
15✔
1357
                                                "provider_type": appeal.Resource.ProviderType,
15✔
1358
                                                "resource_type": appeal.Resource.Type,
15✔
1359
                                                "created_at":    appeal.CreatedAt,
15✔
1360
                                                "approval_step": approval.Name,
15✔
1361
                                                "actor":         approver,
15✔
1362
                                                "details":       appeal.Details,
15✔
1363
                                                "duration":      duration,
15✔
1364
                                                "creator":       appeal.Creator,
15✔
1365
                                        },
15✔
1366
                                },
15✔
1367
                        })
15✔
1368
                }
15✔
1369
        }
1370
        return notifications
18✔
1371
}
1372

1373
func checkIfAppealStatusStillPending(status string) error {
21✔
1374
        switch status {
21✔
1375
        case domain.AppealStatusPending:
17✔
1376
                return nil
17✔
1377
        case
1378
                domain.AppealStatusCanceled,
1379
                domain.AppealStatusApproved,
1380
                domain.AppealStatusRejected:
3✔
1381
                return fmt.Errorf("%w: %q", ErrAppealNotEligibleForApproval, status)
3✔
1382
        default:
1✔
1383
                return fmt.Errorf("%w: %q", ErrAppealStatusUnrecognized, status)
1✔
1384
        }
1385
}
1386

1387
func checkPreviousApprovalStatus(status, name string) error {
13✔
1388
        switch status {
13✔
1389
        case
1390
                domain.ApprovalStatusApproved,
1391
                domain.ApprovalStatusSkipped:
10✔
1392
                return nil
10✔
1393
        case
1394
                domain.ApprovalStatusBlocked,
1395
                domain.ApprovalStatusPending,
1396
                domain.ApprovalStatusRejected:
2✔
1397
                return fmt.Errorf("%w: found previous approval %q with status %q", ErrApprovalNotEligibleForAction, name, status)
2✔
1398
        default:
1✔
1399
                return fmt.Errorf("%w: found previous approval %q with unrecognized status %q", ErrApprovalStatusUnrecognized, name, status)
1✔
1400
        }
1401
}
1402

1403
func checkApprovalStatus(status string) error {
13✔
1404
        switch status {
13✔
1405
        case domain.ApprovalStatusPending:
9✔
1406
                return nil
9✔
1407
        case
1408
                domain.ApprovalStatusBlocked,
1409
                domain.ApprovalStatusApproved,
1410
                domain.ApprovalStatusRejected,
1411
                domain.ApprovalStatusSkipped:
3✔
1412
                return fmt.Errorf("%w: approval status %q is not actionable", ErrApprovalNotEligibleForAction, status)
3✔
1413
        default:
1✔
1414
                return fmt.Errorf("%w: %q", ErrApprovalStatusUnrecognized, status)
1✔
1415
        }
1416
}
1417

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

2✔
1422
                for reqIndex, r := range p.Requirements {
5✔
1423
                        isAppealMatchesRequirement, err := r.On.IsMatch(a)
3✔
1424
                        if err != nil {
4✔
1425
                                return fmt.Errorf("evaluating requirements[%v]: %v", reqIndex, err)
1✔
1426
                        }
1✔
1427
                        if !isAppealMatchesRequirement {
3✔
1428
                                continue
1✔
1429
                        }
1430

1431
                        for _, aa := range r.Appeals {
2✔
1432
                                aa := aa // https://golang.org/doc/faq#closures_and_goroutines
1✔
1433
                                g.Go(func() error {
2✔
1434
                                        // TODO: populate resource data from policyService
1✔
1435
                                        resource, err := s.resourceService.Get(ctx, aa.Resource)
1✔
1436
                                        if err != nil {
1✔
1437
                                                return fmt.Errorf("retrieving resource: %v", err)
×
1438
                                        }
×
1439

1440
                                        additionalAppeal := &domain.Appeal{
1✔
1441
                                                AccountID:   a.AccountID,
1✔
1442
                                                AccountType: a.AccountType,
1✔
1443
                                                CreatedBy:   a.CreatedBy,
1✔
1444
                                                Role:        aa.Role,
1✔
1445
                                                ResourceID:  resource.ID,
1✔
1446
                                        }
1✔
1447
                                        if aa.Options != nil {
1✔
1448
                                                additionalAppeal.Options = aa.Options
×
1449
                                        }
×
1450
                                        if aa.Policy != nil {
1✔
1451
                                                additionalAppeal.PolicyID = aa.Policy.ID
×
1452
                                                additionalAppeal.PolicyVersion = uint(aa.Policy.Version)
×
1453
                                        }
×
1454
                                        if err := s.Create(ctx, []*domain.Appeal{additionalAppeal}, CreateWithAdditionalAppeal()); err != nil {
1✔
1455
                                                if errors.Is(err, ErrAppealDuplicate) {
×
1456
                                                        s.logger.Warn(ctx, "creating additional appeals, duplicate appeal error log", "error", err)
×
1457
                                                        return nil
×
1458
                                                }
×
1459
                                                return fmt.Errorf("creating additional appeals: %w", err)
×
1460
                                        }
1461
                                        return nil
1✔
1462
                                })
1463
                        }
1464
                }
1465
                if err := g.Wait(); err == nil {
2✔
1466
                        return err
1✔
1467
                }
1✔
1468
        }
1469
        return nil
5✔
1470
}
1471

1472
func (s *Service) GrantAccessToProvider(ctx context.Context, a *domain.Appeal, opts ...CreateAppealOption) error {
10✔
1473
        policy := a.Policy
10✔
1474
        if policy == nil {
18✔
1475
                p, err := s.policyService.GetOne(ctx, a.PolicyID, a.PolicyVersion)
8✔
1476
                if err != nil {
9✔
1477
                        return fmt.Errorf("retrieving policy: %w", err)
1✔
1478
                }
1✔
1479
                policy = p
7✔
1480
        }
1481

1482
        createAppealOpts := &createAppealOptions{}
9✔
1483
        for _, opt := range opts {
11✔
1484
                opt(createAppealOpts)
2✔
1485
        }
2✔
1486

1487
        isAdditionalAppealCreation := createAppealOpts.IsAdditionalAppeal
9✔
1488
        if !isAdditionalAppealCreation {
16✔
1489
                if err := s.handleAppealRequirements(ctx, a, policy); err != nil {
8✔
1490
                        return fmt.Errorf("handling appeal requirements: %w", err)
1✔
1491
                }
1✔
1492
        }
1493

1494
        appealCopy := *a
8✔
1495
        appealCopy.Grant = nil
8✔
1496
        grantWithAppeal := *a.Grant
8✔
1497
        grantWithAppeal.Appeal = &appealCopy
8✔
1498

8✔
1499
        // grant access dependencies (if any)
8✔
1500
        dependencyGrants, err := s.providerService.GetDependencyGrants(ctx, grantWithAppeal)
8✔
1501
        if err != nil {
8✔
1502
                return fmt.Errorf("getting grant dependencies: %w", err)
×
1503
        }
×
1504
        for _, dg := range dependencyGrants {
8✔
1505
                activeDepGrants, err := s.grantService.List(ctx, domain.ListGrantsFilter{
×
1506
                        Statuses:     []string{string(domain.GrantStatusActive)},
×
1507
                        AccountIDs:   []string{dg.AccountID},
×
1508
                        AccountTypes: []string{dg.AccountType},
×
1509
                        ResourceIDs:  []string{dg.Resource.ID},
×
1510
                        Permissions:  dg.Permissions,
×
1511
                        Size:         1,
×
1512
                })
×
1513
                if err != nil {
×
1514
                        return fmt.Errorf("failed to get existing active grant dependency: %w", err)
×
1515
                }
×
1516

1517
                if len(activeDepGrants) > 0 {
×
1518
                        continue
×
1519
                }
1520

1521
                dg.Status = domain.GrantStatusActive
×
1522
                dg.Appeal = &appealCopy
×
1523
                if err := s.providerService.GrantAccess(ctx, *dg); err != nil {
×
1524
                        return fmt.Errorf("failed to grant an access dependency: %w", err)
×
1525
                }
×
1526
                dg.Appeal = nil
×
1527

×
1528
                dg.Owner = a.CreatedBy
×
1529
                if err := s.grantService.Create(ctx, dg); err != nil {
×
1530
                        return fmt.Errorf("failed to store grant of access dependency: %w", err)
×
1531
                }
×
1532
        }
1533

1534
        // grant main access
1535
        if err := s.providerService.GrantAccess(ctx, grantWithAppeal); err != nil {
9✔
1536
                return fmt.Errorf("granting access: %w", err)
1✔
1537
        }
1✔
1538

1539
        grantWithAppeal.Appeal = nil
7✔
1540
        return nil
7✔
1541
}
1542

1543
func (s *Service) checkExtensionEligibility(a *domain.Appeal, p *domain.Provider, policy *domain.Policy, activeGrant *domain.Grant) error {
21✔
1544
        allowActiveAccessExtensionIn := ""
21✔
1545

21✔
1546
        // Default to use provider config if policy config is not set
21✔
1547
        if p.Config.Appeal != nil {
42✔
1548
                allowActiveAccessExtensionIn = p.Config.Appeal.AllowActiveAccessExtensionIn
21✔
1549
        }
21✔
1550

1551
        // Use policy config if set
1552
        if policy != nil &&
21✔
1553
                policy.AppealConfig != nil &&
21✔
1554
                policy.AppealConfig.AllowActiveAccessExtensionIn != "" {
21✔
1555
                allowActiveAccessExtensionIn = policy.AppealConfig.AllowActiveAccessExtensionIn
×
1556
        }
×
1557

1558
        if allowActiveAccessExtensionIn == "" {
23✔
1559
                return ErrAppealFoundActiveGrant
2✔
1560
        }
2✔
1561

1562
        extensionDurationRule, err := time.ParseDuration(allowActiveAccessExtensionIn)
19✔
1563
        if err != nil {
21✔
1564
                return fmt.Errorf("%w: %q: %v", ErrAppealInvalidExtensionDuration, allowActiveAccessExtensionIn, err)
2✔
1565
        }
2✔
1566

1567
        if !activeGrant.IsEligibleForExtension(extensionDurationRule) {
19✔
1568
                return fmt.Errorf("%w: extension is allowed %q before grant expiration", ErrGrantNotEligibleForExtension, allowActiveAccessExtensionIn)
2✔
1569
        }
2✔
1570
        return nil
15✔
1571
}
1572

1573
func getPolicy(a *domain.Appeal, p *domain.Provider, policiesMap map[string]map[uint]*domain.Policy) (*domain.Policy, error) {
40✔
1574
        var resourceConfig *domain.ResourceConfig
40✔
1575
        for _, rc := range p.Config.Resources {
83✔
1576
                if rc.Type == a.Resource.Type {
81✔
1577
                        resourceConfig = rc
38✔
1578
                        break
38✔
1579
                }
1580
        }
1581
        if resourceConfig == nil {
42✔
1582
                return nil, fmt.Errorf("%w: couldn't find %q resource type in the provider config", ErrInvalidResourceType, a.Resource.Type)
2✔
1583
        }
2✔
1584
        policyConfig := resourceConfig.Policy
38✔
1585

38✔
1586
        policy, ok := policiesMap[policyConfig.ID][uint(policyConfig.Version)]
38✔
1587
        if !ok {
42✔
1588
                return nil, fmt.Errorf("couldn't find details for policy %q: %w", fmt.Sprintf("%s@%v", policyConfig.ID, policyConfig.Version), ErrPolicyNotFound)
4✔
1589
        }
4✔
1590
        return policy, nil
34✔
1591
}
1592

1593
func (s *Service) populateAppealMetadata(ctx context.Context, a *domain.Appeal, p *domain.Policy) error {
19✔
1594
        if !p.HasAppealMetadataSources() {
34✔
1595
                return nil
15✔
1596
        }
15✔
1597

1598
        eg, egctx := errgroup.WithContext(ctx)
4✔
1599
        var mu sync.Mutex
4✔
1600
        appealMetadata := map[string]interface{}{}
4✔
1601
        for key, metadata := range p.AppealConfig.MetadataSources {
8✔
1602
                key, metadata := key, metadata
4✔
1603
                eg.Go(func() error {
8✔
1604
                        switch metadata.Type {
4✔
1605
                        case "http":
4✔
1606
                                var cfg policy.AppealMetadataSourceConfigHTTP
4✔
1607
                                if err := mapstructure.Decode(metadata.Config, &cfg); err != nil {
4✔
1608
                                        return fmt.Errorf("error decoding metadata config: %w", err)
×
1609
                                }
×
1610

1611
                                if cfg.URL == "" {
4✔
1612
                                        return fmt.Errorf("URL cannot be empty for http type")
×
1613
                                }
×
1614

1615
                                var err error
4✔
1616
                                cfg.URL, err = evaluateExpressionWithAppeal(a, cfg.URL)
4✔
1617
                                if err != nil {
5✔
1618
                                        return err
1✔
1619
                                }
1✔
1620

1621
                                cfg.Body, err = evaluateExpressionWithAppeal(a, cfg.Body)
3✔
1622
                                if err != nil {
3✔
1623
                                        return err
×
1624
                                }
×
1625

1626
                                clientCreator := &http.HttpClientCreatorStruct{}
3✔
1627
                                metadataCl, err := http.NewHTTPClient(&cfg.HTTPClientConfig, clientCreator, "AppealMetadata")
3✔
1628
                                if err != nil {
3✔
1629
                                        return fmt.Errorf("key: %s, %w", key, err)
×
1630
                                }
×
1631

1632
                                res, err := metadataCl.MakeRequest(egctx)
3✔
1633
                                if err != nil || (res.StatusCode < 200 && res.StatusCode > 300) {
3✔
1634
                                        if !cfg.AllowFailed {
×
1635
                                                return fmt.Errorf("error fetching resource: %w", err)
×
1636
                                        }
×
1637
                                }
1638

1639
                                body, err := io.ReadAll(res.Body)
3✔
1640
                                if err != nil {
3✔
1641
                                        return fmt.Errorf("error reading response body: %w", err)
×
1642
                                }
×
1643
                                res.Body.Close()
3✔
1644
                                var jsonBody interface{}
3✔
1645
                                err = json.Unmarshal(body, &jsonBody)
3✔
1646
                                if err != nil {
3✔
1647
                                        return fmt.Errorf("error unmarshaling response body: %w", err)
×
1648
                                }
×
1649

1650
                                responseMap := map[string]interface{}{
3✔
1651
                                        "status":      res.Status,
3✔
1652
                                        "status_code": res.StatusCode,
3✔
1653
                                        "headers":     res.Header,
3✔
1654
                                        "body":        jsonBody,
3✔
1655
                                }
3✔
1656
                                params := map[string]interface{}{
3✔
1657
                                        "response": responseMap,
3✔
1658
                                        "appeal":   a,
3✔
1659
                                }
3✔
1660

3✔
1661
                                value, err := metadata.EvaluateValue(params)
3✔
1662
                                if err != nil {
3✔
1663
                                        return fmt.Errorf("error parsing value: %w", err)
×
1664
                                }
×
1665
                                mu.Lock()
3✔
1666
                                appealMetadata[key] = value
3✔
1667
                                mu.Unlock()
3✔
1668
                        case "static":
×
1669
                                params := map[string]interface{}{"appeal": a}
×
1670
                                value, err := metadata.EvaluateValue(params)
×
1671
                                if err != nil {
×
1672
                                        return fmt.Errorf("error parsing value: %w", err)
×
1673
                                }
×
1674
                                mu.Lock()
×
1675
                                appealMetadata[key] = value
×
1676
                                mu.Unlock()
×
1677
                        default:
×
1678
                                return fmt.Errorf("invalid metadata source type")
×
1679
                        }
1680

1681
                        return nil
3✔
1682
                })
1683
        }
1684

1685
        if err := eg.Wait(); err != nil {
5✔
1686
                return err
1✔
1687
        }
1✔
1688

1689
        if a.Details == nil {
6✔
1690
                a.Details = map[string]interface{}{}
3✔
1691
        }
3✔
1692
        a.Details[domain.ReservedDetailsKeyPolicyMetadata] = appealMetadata
3✔
1693

3✔
1694
        return nil
3✔
1695
}
1696

1697
func (s *Service) addCreatorDetails(ctx context.Context, a *domain.Appeal, p *domain.Policy) error {
19✔
1698
        if p.IAM == nil {
24✔
1699
                return nil
5✔
1700
        }
5✔
1701

1702
        iamConfig, err := s.iam.ParseConfig(p.IAM)
14✔
1703
        if err != nil {
14✔
1704
                return fmt.Errorf("parsing policy.iam config: %w", err)
×
1705
        }
×
1706
        iamClient, err := s.iam.GetClient(iamConfig)
14✔
1707
        if err != nil {
14✔
1708
                return fmt.Errorf("initializing iam client: %w", err)
×
1709
        }
×
1710

1711
        userDetails, err := iamClient.GetUser(a.CreatedBy)
14✔
1712
        if err != nil {
19✔
1713
                if p.AppealConfig != nil && p.AppealConfig.AllowCreatorDetailsFailure {
10✔
1714
                        s.logger.Warn(ctx, "unable to get creator details", "error", err)
5✔
1715
                        return nil
5✔
1716
                }
5✔
1717
                return fmt.Errorf("unable to get creator details: %w", err)
×
1718
        }
1719

1720
        userDetailsMap, ok := userDetails.(map[string]interface{})
9✔
1721
        if !ok {
9✔
1722
                return nil
×
1723
        }
×
1724

1725
        if p.IAM.Schema == nil {
10✔
1726
                a.Creator = userDetailsMap
1✔
1727
                return nil
1✔
1728
        }
1✔
1729

1730
        creator := map[string]interface{}{}
8✔
1731
        for schemaKey, targetKey := range p.IAM.Schema {
40✔
1732
                if strings.Contains(targetKey, "$response") {
48✔
1733
                        params := map[string]interface{}{
16✔
1734
                                "response": userDetailsMap,
16✔
1735
                        }
16✔
1736
                        v, err := evaluator.Expression(targetKey).EvaluateWithVars(params)
16✔
1737
                        if err != nil {
16✔
1738
                                return fmt.Errorf("evaluating expression: %w", err)
×
1739
                        }
×
1740
                        creator[schemaKey] = v
16✔
1741
                } else {
16✔
1742
                        creator[schemaKey] = userDetailsMap[targetKey]
16✔
1743
                }
16✔
1744
        }
1745

1746
        a.Creator = creator
8✔
1747
        s.logger.Debug(ctx, "added creator details", "creator", creator)
8✔
1748

8✔
1749
        return nil
8✔
1750
}
1751

1752
func addResource(a *domain.Appeal, resourcesMap map[string]*domain.Resource) error {
27✔
1753
        r := resourcesMap[a.ResourceID]
27✔
1754
        if r == nil {
28✔
1755
                return ErrResourceNotFound
1✔
1756
        } else if r.IsDeleted {
27✔
1757
                return ErrResourceDeleted
×
1758
        }
×
1759

1760
        a.Resource = r
26✔
1761
        return nil
26✔
1762
}
1763

1764
func getProvider(a *domain.Appeal, providersMap map[string]map[string]*domain.Provider) (*domain.Provider, error) {
45✔
1765
        provider, ok := providersMap[a.Resource.ProviderType][a.Resource.ProviderURN]
45✔
1766
        if !ok {
49✔
1767
                return nil, fmt.Errorf("couldn't find details for provider %q: %w", a.Resource.ProviderType+" - "+a.Resource.ProviderURN, ErrProviderNotFound)
4✔
1768
        }
4✔
1769
        return provider, nil
41✔
1770
}
1771

1772
func validateAppeal(a *domain.Appeal, pendingAppealsMap map[string]map[string]map[string]*domain.Appeal) error {
41✔
1773
        accountID := strings.ToLower(a.AccountID)
41✔
1774
        if pendingAppealsMap[accountID] != nil &&
41✔
1775
                pendingAppealsMap[accountID][a.ResourceID] != nil &&
41✔
1776
                pendingAppealsMap[accountID][a.ResourceID][a.Role] != nil {
43✔
1777
                return ErrAppealDuplicate
2✔
1778
        }
2✔
1779

1780
        return nil
39✔
1781
}
1782

1783
func (s *Service) getPermissions(ctx context.Context, pc *domain.ProviderConfig, resourceType, role string) ([]string, error) {
23✔
1784
        permissions, err := s.providerService.GetPermissions(ctx, pc, resourceType, role)
23✔
1785
        if err != nil {
23✔
1786
                return nil, err
×
1787
        }
×
1788

1789
        if permissions == nil {
23✔
1790
                return nil, nil
×
1791
        }
×
1792

1793
        strPermissions := []string{}
23✔
1794
        for _, p := range permissions {
42✔
1795
                strPermissions = append(strPermissions, fmt.Sprintf("%s", p))
19✔
1796
        }
19✔
1797
        return strPermissions, nil
23✔
1798
}
1799

1800
// TODO(feature): add relation between new and revoked grant for traceability
1801
func (s *Service) prepareGrant(ctx context.Context, appeal *domain.Appeal) (newGrant *domain.Grant, deactivatedGrant *domain.Grant, err error) {
6✔
1802
        filter := domain.ListGrantsFilter{
6✔
1803
                AccountIDs:  []string{appeal.AccountID},
6✔
1804
                ResourceIDs: []string{appeal.ResourceID},
6✔
1805
                Statuses:    []string{string(domain.GrantStatusActive)},
6✔
1806
                Permissions: appeal.Permissions,
6✔
1807
        }
6✔
1808
        revocationReason := RevokeReasonForExtension
6✔
1809
        if s.providerService.IsExclusiveRoleAssignment(ctx, appeal.Resource.ProviderType, appeal.Resource.Type) {
6✔
1810
                filter.Permissions = nil
×
1811
                revocationReason = RevokeReasonForOverride
×
1812
        }
×
1813

1814
        activeGrants, err := s.grantService.List(ctx, filter)
6✔
1815
        if err != nil {
6✔
1816
                return nil, nil, fmt.Errorf("unable to retrieve existing active grants: %w", err)
×
1817
        }
×
1818

1819
        if len(activeGrants) > 0 {
8✔
1820
                deactivatedGrant = &activeGrants[0]
2✔
1821
                if err := deactivatedGrant.Revoke(domain.SystemActorName, revocationReason); err != nil {
2✔
1822
                        return nil, nil, fmt.Errorf("revoking previous grant: %w", err)
×
1823
                }
×
1824
        }
1825

1826
        if err := appeal.Approve(); err != nil {
6✔
1827
                return nil, nil, fmt.Errorf("activating appeal: %w", err)
×
1828
        }
×
1829

1830
        grant, err := s.grantService.Prepare(ctx, *appeal)
6✔
1831
        if err != nil {
6✔
1832
                return nil, nil, err
×
1833
        }
×
1834

1835
        return grant, deactivatedGrant, nil
6✔
1836
}
1837

1838
func (s *Service) GetAppealsTotalCount(ctx context.Context, filters *domain.ListAppealsFilter) (int64, error) {
2✔
1839
        return s.repo.GetAppealsTotalCount(ctx, filters)
2✔
1840
}
2✔
1841

1842
func evaluateExpressionWithAppeal(a *domain.Appeal, expression string) (string, error) {
7✔
1843
        if expression != "" && strings.Contains(expression, "$appeal") {
11✔
1844
                appealMap, err := a.ToMap()
4✔
1845
                if err != nil {
4✔
1846
                        return "", fmt.Errorf("error converting appeal to map: %w", err)
×
1847
                }
×
1848
                params := map[string]interface{}{"appeal": appealMap}
4✔
1849
                evaluated, err := evaluator.Expression(expression).EvaluateWithVars(params)
4✔
1850
                if err != nil {
5✔
1851
                        return "", fmt.Errorf("error evaluating expression %w", err)
1✔
1852
                }
1✔
1853
                evaluatedStr, ok := evaluated.(string)
3✔
1854
                if !ok {
3✔
1855
                        return "", fmt.Errorf("expression must evaluate to a string")
×
1856
                }
×
1857
                return evaluatedStr, nil
3✔
1858
        }
1859
        return expression, nil
3✔
1860
}
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