• 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

80.81
/core/grant/service.go
1
package grant
2

3
import (
4
        "context"
5
        "fmt"
6
        "sort"
7
        "strings"
8
        "time"
9

10
        "github.com/go-playground/validator/v10"
11
        "github.com/goto/guardian/domain"
12
        "github.com/goto/guardian/pkg/log"
13
        "github.com/goto/guardian/pkg/slices"
14
        "github.com/goto/guardian/plugins/notifiers"
15
        "github.com/goto/guardian/utils"
16
)
17

18
const (
19
        AuditKeyRevoke  = "grant.revoke"
20
        AuditKeyUpdate  = "grant.update"
21
        AuditKeyRestore = "grant.restore"
22
)
23

24
//go:generate mockery --name=repository --exported --with-expecter
25
type repository interface {
26
        List(context.Context, domain.ListGrantsFilter) ([]domain.Grant, error)
27
        GetByID(context.Context, string) (*domain.Grant, error)
28
        Update(context.Context, *domain.Grant) error
29
        Patch(context.Context, domain.GrantUpdate) error
30
        BulkUpsert(context.Context, []*domain.Grant) error
31
        GetGrantsTotalCount(context.Context, domain.ListGrantsFilter) (int64, error)
32
        ListUserRoles(context.Context, string) ([]string, error)
33
        Create(context.Context, *domain.Grant) error
34
}
35

36
//go:generate mockery --name=providerService --exported --with-expecter
37
type providerService interface {
38
        GetByID(context.Context, string) (*domain.Provider, error)
39
        GrantAccess(context.Context, domain.Grant) error
40
        RevokeAccess(context.Context, domain.Grant) error
41
        ListAccess(context.Context, domain.Provider, []*domain.Resource) (domain.MapResourceAccess, error)
42
        ListActivities(context.Context, domain.Provider, domain.ListActivitiesFilter) ([]*domain.Activity, error)
43
        CorrelateGrantActivities(context.Context, domain.Provider, []*domain.Grant, []*domain.Activity) error
44
}
45

46
//go:generate mockery --name=resourceService --exported --with-expecter
47
type resourceService interface {
48
        Find(context.Context, domain.ListResourcesFilter) ([]*domain.Resource, error)
49
}
50

51
//go:generate mockery --name=auditLogger --exported --with-expecter
52
type auditLogger interface {
53
        Log(ctx context.Context, action string, data interface{}) error
54
}
55

56
//go:generate mockery --name=notifier --exported --with-expecter
57
type notifier interface {
58
        notifiers.Client
59
}
60

61
type grantCreation struct {
62
        AppealStatus string `validate:"required,eq=approved"`
63
        AccountID    string `validate:"required"`
64
        AccountType  string `validate:"required"`
65
        ResourceID   string `validate:"required"`
66
}
67

68
type Service struct {
69
        repo            repository
70
        providerService providerService
71
        resourceService resourceService
72

73
        notifier    notifier
74
        validator   *validator.Validate
75
        logger      log.Logger
76
        auditLogger auditLogger
77
}
78

79
type ServiceDeps struct {
80
        Repository      repository
81
        ProviderService providerService
82
        ResourceService resourceService
83

84
        Notifier    notifier
85
        Validator   *validator.Validate
86
        Logger      log.Logger
87
        AuditLogger auditLogger
88
}
89

90
func NewService(deps ServiceDeps) *Service {
33✔
91
        return &Service{
33✔
92
                repo:            deps.Repository,
33✔
93
                providerService: deps.ProviderService,
33✔
94
                resourceService: deps.ResourceService,
33✔
95

33✔
96
                notifier:    deps.Notifier,
33✔
97
                validator:   deps.Validator,
33✔
98
                logger:      deps.Logger,
33✔
99
                auditLogger: deps.AuditLogger,
33✔
100
        }
33✔
101
}
33✔
102

103
func (s *Service) List(ctx context.Context, filter domain.ListGrantsFilter) ([]domain.Grant, error) {
4✔
104
        return s.repo.List(ctx, filter)
4✔
105
}
4✔
106

107
func (s *Service) GetByID(ctx context.Context, id string) (*domain.Grant, error) {
16✔
108
        if id == "" {
17✔
109
                return nil, ErrEmptyIDParam
1✔
110
        }
1✔
111
        return s.repo.GetByID(ctx, id)
15✔
112
}
113

114
func (s *Service) Create(ctx context.Context, grant *domain.Grant) error {
×
115
        return s.repo.Create(ctx, grant)
×
116
}
×
117

118
func (s *Service) Update(ctx context.Context, payload *domain.GrantUpdate) (*domain.Grant, error) {
2✔
119
        grant, err := s.GetByID(ctx, payload.ID)
2✔
120
        if err != nil {
2✔
121
                return nil, fmt.Errorf("getting grant details: %w", err)
×
122
        }
×
123
        previousOwner := grant.Owner
2✔
124

2✔
125
        if err := payload.Validate(*grant); err != nil {
3✔
126
                return nil, fmt.Errorf("%w: %s", domain.ErrInvalidGrantUpdateRequest, err)
1✔
127
        }
1✔
128

129
        if payload.IsUpdatingExpirationDate() {
1✔
130
                falseBool := false
×
131
                payload.IsPermanent = &falseBool
×
132
        }
×
133
        if err := s.repo.Patch(ctx, *payload); err != nil {
1✔
134
                return nil, err
×
135
        }
×
136

137
        latestGrant, err := s.GetByID(ctx, grant.ID)
1✔
138
        if err != nil {
1✔
139
                return nil, err
×
140
        }
×
141

142
        s.logger.Info(ctx, "grant updated", "grant_id", grant.ID, "updatedGrant", latestGrant)
1✔
143

1✔
144
        go func() {
2✔
145
                diff, err := latestGrant.Compare(grant, payload.Actor)
1✔
146
                if err != nil {
1✔
147
                        s.logger.Error(ctx, "failed to compare grant", "error", err)
×
148
                        return
×
149
                }
×
150

151
                ctx := context.WithoutCancel(ctx)
1✔
152
                if err := s.auditLogger.Log(ctx, AuditKeyUpdate, map[string]interface{}{
1✔
153
                        "grant_id":      payload.ID,
1✔
154
                        "payload":       payload,
1✔
155
                        "updated_grant": latestGrant,
1✔
156
                        "diff":          diff,
1✔
157
                }); err != nil {
1✔
158
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
159
                }
×
160
        }()
161

162
        if previousOwner != latestGrant.Owner {
2✔
163
                go func() {
2✔
164
                        message := domain.NotificationMessage{
1✔
165
                                Type: domain.NotificationTypeGrantOwnerChanged,
1✔
166
                                Variables: map[string]interface{}{
1✔
167
                                        "grant_id":       grant.ID,
1✔
168
                                        "previous_owner": previousOwner,
1✔
169
                                        "new_owner":      latestGrant.Owner,
1✔
170
                                },
1✔
171
                        }
1✔
172
                        notifications := []domain.Notification{{
1✔
173
                                User: latestGrant.Owner,
1✔
174
                                Labels: map[string]string{
1✔
175
                                        "appeal_id": grant.AppealID,
1✔
176
                                        "grant_id":  grant.ID,
1✔
177
                                },
1✔
178
                                Message: message,
1✔
179
                        }}
1✔
180
                        if previousOwner != "" {
2✔
181
                                notifications = append(notifications, domain.Notification{
1✔
182
                                        User: previousOwner,
1✔
183
                                        Labels: map[string]string{
1✔
184
                                                "appeal_id": grant.AppealID,
1✔
185
                                                "grant_id":  grant.ID,
1✔
186
                                        },
1✔
187
                                        Message: message,
1✔
188
                                })
1✔
189
                        }
1✔
190
                        ctx := context.WithoutCancel(ctx)
1✔
191
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
1✔
192
                                for _, err1 := range errs {
×
193
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
194
                                }
×
195
                        }
196
                }()
197
        }
198

199
        return latestGrant, nil
1✔
200
}
201

202
func (s *Service) Prepare(ctx context.Context, appeal domain.Appeal) (*domain.Grant, error) {
7✔
203
        // validation
7✔
204
        if err := s.validator.Struct(grantCreation{
7✔
205
                AppealStatus: appeal.Status,
7✔
206
                AccountID:    appeal.AccountID,
7✔
207
                AccountType:  appeal.AccountType,
7✔
208
                ResourceID:   appeal.ResourceID,
7✔
209
        }); err != nil {
11✔
210
                return nil, fmt.Errorf("validating appeal: %w", err)
4✔
211
        }
4✔
212

213
        // converting aapeal into a new grant
214
        return appeal.ToGrant()
3✔
215
}
216

217
func (s *Service) Revoke(ctx context.Context, id, actor, reason string, opts ...Option) (*domain.Grant, error) {
2✔
218
        grant, err := s.GetByID(ctx, id)
2✔
219
        if err != nil {
2✔
220
                return nil, fmt.Errorf("getting grant details: %w", err)
×
221
        }
×
222

223
        revokedGrant := &domain.Grant{}
2✔
224
        *revokedGrant = *grant
2✔
225
        if err := grant.Revoke(actor, reason); err != nil {
2✔
226
                return nil, err
×
227
        }
×
228
        if err := s.repo.Update(ctx, grant); err != nil {
2✔
229
                return nil, fmt.Errorf("updating grant record in db: %w", err)
×
230
        }
×
231

232
        options := s.getOptions(opts...)
2✔
233

2✔
234
        if !options.skipRevokeInProvider {
3✔
235
                if err := s.providerService.RevokeAccess(ctx, *grant); err != nil {
1✔
236
                        if err := s.repo.Update(ctx, grant); err != nil {
×
237
                                return nil, fmt.Errorf("failed to rollback grant status: %w", err)
×
238
                        }
×
239
                        return nil, fmt.Errorf("removing grant in provider: %w", err)
×
240
                }
241
        }
242

243
        if !options.skipNotification {
3✔
244
                notifications := []domain.Notification{{
1✔
245
                        User: grant.CreatedBy,
1✔
246
                        Labels: map[string]string{
1✔
247
                                "appeal_id": grant.AppealID,
1✔
248
                                "grant_id":  grant.ID,
1✔
249
                        },
1✔
250
                        Message: domain.NotificationMessage{
1✔
251
                                Type: domain.NotificationTypeAccessRevoked,
1✔
252
                                Variables: map[string]interface{}{
1✔
253
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", grant.Resource.Name, grant.Resource.ProviderType, grant.Resource.URN),
1✔
254
                                        "role":          grant.Role,
1✔
255
                                        "account_type":  grant.AccountType,
1✔
256
                                        "account_id":    grant.AccountID,
1✔
257
                                        "requestor":     grant.Owner,
1✔
258
                                        "revoke_reason": grant.RevokeReason,
1✔
259
                                },
1✔
260
                        },
1✔
261
                }}
1✔
262
                go func() {
2✔
263
                        ctx := context.WithoutCancel(ctx)
1✔
264
                        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
1✔
265
                                for _, err1 := range errs {
×
266
                                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
267
                                }
×
268
                        }
269
                }()
270
        }
271

272
        s.logger.Info(ctx, "grant revoked", "grant_id", id)
2✔
273

2✔
274
        go func() {
4✔
275
                ctx := context.WithoutCancel(ctx)
2✔
276
                if err := s.auditLogger.Log(ctx, AuditKeyRevoke, map[string]interface{}{
2✔
277
                        "grant_id": id,
2✔
278
                        "reason":   reason,
2✔
279
                }); err != nil {
2✔
280
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
281
                }
×
282
        }()
283

284
        return grant, nil
2✔
285
}
286

287
func (s *Service) Restore(ctx context.Context, id, actor, reason string) (*domain.Grant, error) {
8✔
288
        originalGrant, err := s.GetByID(ctx, id)
8✔
289
        if err != nil {
9✔
290
                return nil, fmt.Errorf("getting grant details: %w", err)
1✔
291
        }
1✔
292

293
        grant := &domain.Grant{}
7✔
294
        *grant = *originalGrant // copy values
7✔
295

7✔
296
        if err := grant.Restore(actor, reason); err != nil {
10✔
297
                return nil, err
3✔
298
        }
3✔
299

300
        if err := s.repo.Update(ctx, grant); err != nil {
4✔
301
                return nil, fmt.Errorf("updating grant record in db: %w", err)
×
302
        }
×
303

304
        if err := s.providerService.GrantAccess(ctx, *grant); err != nil {
5✔
305
                if err := s.repo.Update(ctx, originalGrant); err != nil {
1✔
306
                        return nil, fmt.Errorf("failed to rollback grant record after restore failed: %w", err)
×
307
                }
×
308
                return nil, fmt.Errorf("granting access in provider: %w", err)
1✔
309
        }
310

311
        go func() {
6✔
312
                ctx := context.WithoutCancel(ctx)
3✔
313
                if err := s.auditLogger.Log(ctx, AuditKeyRestore, map[string]interface{}{
3✔
314
                        "grant_id": id,
3✔
315
                        "reason":   reason,
3✔
316
                }); err != nil {
3✔
317
                        s.logger.Error(ctx, "failed to record audit log", "error", err)
×
318
                }
×
319
        }()
320

321
        return grant, nil
3✔
322
}
323

324
func (s *Service) BulkRevoke(ctx context.Context, filter domain.RevokeGrantsFilter, actor, reason string) ([]*domain.Grant, error) {
1✔
325
        if filter.AccountIDs == nil || len(filter.AccountIDs) == 0 {
1✔
326
                return nil, fmt.Errorf("account_ids is required")
×
327
        }
×
328

329
        grants, err := s.List(ctx, domain.ListGrantsFilter{
1✔
330
                Statuses:      []string{string(domain.GrantStatusActive)},
1✔
331
                AccountIDs:    filter.AccountIDs,
1✔
332
                ProviderTypes: filter.ProviderTypes,
1✔
333
                ProviderURNs:  filter.ProviderURNs,
1✔
334
                ResourceTypes: filter.ResourceTypes,
1✔
335
                ResourceURNs:  filter.ResourceURNs,
1✔
336
        })
1✔
337
        if err != nil {
1✔
338
                return nil, fmt.Errorf("listing active grants: %w", err)
×
339
        }
×
340
        if len(grants) == 0 {
1✔
341
                return nil, nil
×
342
        }
×
343

344
        result := make([]*domain.Grant, 0)
1✔
345
        batchSize := 10
1✔
346
        timeLimiter := make(chan int, batchSize)
1✔
347

1✔
348
        for i := 1; i <= batchSize; i++ {
11✔
349
                timeLimiter <- i
10✔
350
        }
10✔
351

352
        go func() {
2✔
353
                for range time.Tick(1 * time.Second) {
1✔
354
                        for i := 1; i <= batchSize; i++ {
×
355
                                timeLimiter <- i
×
356
                        }
×
357
                }
358
        }()
359

360
        totalRequests := len(grants)
1✔
361
        done := make(chan *domain.Grant, totalRequests)
1✔
362
        resourceGrantMap := make(map[string][]*domain.Grant, 0)
1✔
363

1✔
364
        for i, grant := range grants {
3✔
365
                var resourceGrants []*domain.Grant
2✔
366
                var ok bool
2✔
367
                if resourceGrants, ok = resourceGrantMap[grant.ResourceID]; ok {
3✔
368
                        resourceGrants = append(resourceGrants, &grants[i])
1✔
369
                } else {
2✔
370
                        resourceGrants = []*domain.Grant{&grants[i]}
1✔
371
                }
1✔
372
                resourceGrantMap[grant.ResourceID] = resourceGrants
2✔
373
        }
374

375
        for _, resourceGrants := range resourceGrantMap {
2✔
376
                go s.expiredInActiveUserAccess(ctx, timeLimiter, done, actor, reason, resourceGrants)
1✔
377
        }
1✔
378

379
        var successRevoke []string
1✔
380
        var failedRevoke []string
1✔
381
        for {
3✔
382
                select {
2✔
383
                case grant := <-done:
2✔
384
                        if grant.Status == domain.GrantStatusInactive {
4✔
385
                                successRevoke = append(successRevoke, grant.ID)
2✔
386
                        } else {
2✔
387
                                failedRevoke = append(failedRevoke, grant.ID)
×
388
                        }
×
389
                        result = append(result, grant)
2✔
390
                        if len(result) == totalRequests {
3✔
391
                                s.logger.Info(ctx, "successful grant revocation", "count", len(successRevoke), "ids", successRevoke)
1✔
392
                                if len(failedRevoke) > 0 {
1✔
393
                                        s.logger.Info(ctx, "failed grant revocation", "count", len(failedRevoke), "ids", failedRevoke)
×
394
                                }
×
395
                                return result, nil
1✔
396
                        }
397
                }
398
        }
399
}
400

401
func (s *Service) expiredInActiveUserAccess(ctx context.Context, timeLimiter chan int, done chan *domain.Grant, actor string, reason string, grants []*domain.Grant) {
1✔
402
        for _, grant := range grants {
3✔
403
                <-timeLimiter
2✔
404

2✔
405
                revokedGrant := &domain.Grant{}
2✔
406
                *revokedGrant = *grant
2✔
407
                if err := revokedGrant.Revoke(actor, reason); err != nil {
2✔
408
                        s.logger.Error(ctx, "failed to revoke grant", "id", grant.ID, "error", err)
×
409
                        return
×
410
                }
×
411
                if err := s.providerService.RevokeAccess(ctx, *grant); err != nil {
2✔
412
                        done <- grant
×
413
                        s.logger.Error(ctx, "failed to revoke grant in provider", "id", grant.ID, "error", err)
×
414
                        return
×
415
                }
×
416

417
                revokedGrant.Status = domain.GrantStatusInactive
2✔
418
                if err := s.repo.Update(ctx, revokedGrant); err != nil {
2✔
419
                        done <- grant
×
420
                        s.logger.Error(ctx, "failed to update access-revoke status", "id", grant.ID, "error", err)
×
421
                        return
×
422
                } else {
2✔
423
                        done <- revokedGrant
2✔
424
                        s.logger.Info(ctx, "grant revoked", "id", grant.ID)
2✔
425
                }
2✔
426
        }
427
}
428

429
type ImportFromProviderCriteria struct {
430
        ProviderID    string `validate:"required"`
431
        ResourceIDs   []string
432
        ResourceTypes []string
433
        ResourceURNs  []string
434
}
435

436
func (s *Service) ImportFromProvider(ctx context.Context, criteria ImportFromProviderCriteria) ([]*domain.Grant, error) {
4✔
437
        p, err := s.providerService.GetByID(ctx, criteria.ProviderID)
4✔
438
        if err != nil {
4✔
439
                return nil, fmt.Errorf("getting provider details: %w", err)
×
440
        }
×
441

442
        listResourcesFilter := domain.ListResourcesFilter{
4✔
443
                ProviderType: p.Type,
4✔
444
                ProviderURN:  p.URN,
4✔
445
        }
4✔
446
        listGrantsFilter := domain.ListGrantsFilter{
4✔
447
                Statuses:      []string{string(domain.GrantStatusActive)},
4✔
448
                ProviderTypes: []string{p.Type},
4✔
449
                ProviderURNs:  []string{p.URN},
4✔
450
        }
4✔
451
        if criteria.ResourceIDs != nil {
4✔
452
                listResourcesFilter.IDs = criteria.ResourceIDs
×
453
                listGrantsFilter.ResourceIDs = criteria.ResourceIDs
×
454
        } else {
4✔
455
                listResourcesFilter.ResourceTypes = criteria.ResourceTypes
4✔
456
                listResourcesFilter.ResourceURNs = criteria.ResourceURNs
4✔
457

4✔
458
                listGrantsFilter.ResourceTypes = criteria.ResourceTypes
4✔
459
                listGrantsFilter.ResourceURNs = criteria.ResourceURNs
4✔
460
        }
4✔
461
        resources, err := s.resourceService.Find(ctx, listResourcesFilter)
4✔
462
        if err != nil {
4✔
463
                return nil, fmt.Errorf("getting resources: %w", err)
×
464
        }
×
465

466
        resourceAccess, err := s.providerService.ListAccess(ctx, *p, resources)
4✔
467
        if err != nil {
4✔
468
                return nil, fmt.Errorf("fetching access from provider: %w", err)
×
469
        }
×
470

471
        resourceConfigs := make(map[string]*domain.ResourceConfig)
4✔
472
        for _, rc := range p.Config.Resources {
8✔
473
                resourceConfigs[rc.Type] = rc
4✔
474
        }
4✔
475

476
        resourcesMap := make(map[string]*domain.Resource)
4✔
477
        for _, r := range resources {
8✔
478
                resourcesMap[r.URN] = r
4✔
479
        }
4✔
480

481
        activeGrants, err := s.repo.List(ctx, listGrantsFilter)
4✔
482
        if err != nil {
4✔
483
                return nil, fmt.Errorf("getting active grants: %w", err)
×
484
        }
×
485
        // map[resourceURN]map[accounttype:accountId]map[permissionsKey]grant
486
        activeGrantsMap := map[string]map[string]map[string]*domain.Grant{}
4✔
487
        for i, g := range activeGrants {
7✔
488
                if activeGrantsMap[g.Resource.URN] == nil {
5✔
489
                        activeGrantsMap[g.Resource.URN] = map[string]map[string]*domain.Grant{}
2✔
490
                }
2✔
491

492
                accountSignature := getAccountSignature(g.AccountType, g.AccountID)
3✔
493
                if activeGrantsMap[g.Resource.URN][accountSignature] == nil {
6✔
494
                        activeGrantsMap[g.Resource.URN][accountSignature] = map[string]*domain.Grant{}
3✔
495
                }
3✔
496

497
                activeGrantsMap[g.Resource.URN][accountSignature][g.PermissionsKey()] = &activeGrants[i]
3✔
498
        }
499

500
        var newAndUpdatedGrants []*domain.Grant
4✔
501
        for rURN, accessEntries := range resourceAccess {
7✔
502
                resource, ok := resourcesMap[rURN]
3✔
503
                if !ok {
3✔
504
                        continue // skip access for resources that not yet added to guardian
×
505
                }
506

507
                importedGrants := []*domain.Grant{}
3✔
508
                for accountSignature, accessEntries := range groupAccessEntriesByAccount(accessEntries) {
8✔
509
                        // convert access entries to grants
5✔
510
                        var grants []*domain.Grant
5✔
511
                        for _, ae := range accessEntries {
13✔
512
                                g := ae.ToGrant(*resource)
8✔
513
                                grants = append(grants, &g)
8✔
514
                        }
8✔
515

516
                        // group grants for the same account (accountGrants) by provider role
517
                        rc := resourceConfigs[resource.Type]
5✔
518
                        grants = reduceGrantsByProviderRole(*rc, grants)
5✔
519
                        for i, g := range grants {
11✔
520
                                key := g.PermissionsKey()
6✔
521
                                if existingGrant, ok := activeGrantsMap[rURN][accountSignature][key]; ok {
8✔
522
                                        // replace imported grant values with existing grant
2✔
523
                                        *grants[i] = *existingGrant
2✔
524

2✔
525
                                        // remove updated grant from active grants map
2✔
526
                                        delete(activeGrantsMap[rURN][accountSignature], key)
2✔
527
                                }
2✔
528
                        }
529

530
                        importedGrants = append(importedGrants, grants...)
5✔
531
                }
532

533
                if len(importedGrants) > 0 {
6✔
534
                        if err := s.repo.BulkUpsert(ctx, importedGrants); err != nil {
3✔
535
                                return nil, fmt.Errorf("inserting new and updated grants into the db for %q: %w", rURN, err)
×
536
                        }
×
537
                        newAndUpdatedGrants = append(newAndUpdatedGrants, importedGrants...)
3✔
538
                }
539
        }
540

541
        // mark remaining active grants as inactive
542
        var deactivatedGrants []*domain.Grant
4✔
543
        for _, v := range activeGrantsMap {
6✔
544
                for _, v2 := range v {
5✔
545
                        for _, g := range v2 {
4✔
546
                                g.StatusInProvider = domain.GrantStatusInactive
1✔
547
                                deactivatedGrants = append(deactivatedGrants, g)
1✔
548
                        }
1✔
549
                }
550
        }
551
        if len(deactivatedGrants) > 0 {
5✔
552
                if err := s.repo.BulkUpsert(ctx, deactivatedGrants); err != nil {
1✔
553
                        return nil, fmt.Errorf("updating grants provider status: %w", err)
×
554
                }
×
555
        }
556

557
        return newAndUpdatedGrants, nil
4✔
558
}
559

560
func (s *Service) DormancyCheck(ctx context.Context, criteria domain.DormancyCheckCriteria) error {
1✔
561
        if err := criteria.Validate(); err != nil {
1✔
562
                return fmt.Errorf("invalid dormancy check criteria: %w", err)
×
563
        }
×
564
        startDate := time.Now().Add(-criteria.Period)
1✔
565

1✔
566
        provider, err := s.providerService.GetByID(ctx, criteria.ProviderID)
1✔
567
        if err != nil {
1✔
568
                return fmt.Errorf("getting provider details: %w", err)
×
569
        }
×
570

571
        s.logger.Info(ctx, "getting active grants", "provider_urn", provider.URN)
1✔
572
        grants, err := s.List(ctx, domain.ListGrantsFilter{
1✔
573
                Statuses:      []string{string(domain.GrantStatusActive)}, // TODO: evaluate later to use status_in_provider
1✔
574
                ProviderTypes: []string{provider.Type},
1✔
575
                ProviderURNs:  []string{provider.URN},
1✔
576
                CreatedAtLte:  startDate,
1✔
577
        })
1✔
578
        if err != nil {
1✔
579
                return fmt.Errorf("listing active grants: %w", err)
×
580
        }
×
581
        if len(grants) == 0 {
1✔
582
                s.logger.Info(ctx, "no active grants found", "provider_urn", provider.URN)
×
583
                return nil
×
584
        }
×
585
        grantIDs := getGrantIDs(grants)
1✔
586
        s.logger.Info(ctx, fmt.Sprintf("found %d active grants", len(grants)), "grant_ids", grantIDs, "provider_urn", provider.URN)
1✔
587

1✔
588
        var accountIDs []string
1✔
589
        for _, g := range grants {
3✔
590
                accountIDs = append(accountIDs, g.AccountID)
2✔
591
        }
2✔
592
        accountIDs = slices.UniqueStringSlice(accountIDs)
1✔
593

1✔
594
        s.logger.Info(ctx, "getting activities", "provider_urn", provider.URN)
1✔
595
        activities, err := s.providerService.ListActivities(ctx, *provider, domain.ListActivitiesFilter{
1✔
596
                AccountIDs:   accountIDs,
1✔
597
                TimestampGte: &startDate,
1✔
598
        })
1✔
599
        if err != nil {
1✔
600
                return fmt.Errorf("listing activities for provider %q: %w", provider.URN, err)
×
601
        }
×
602
        s.logger.Info(ctx, fmt.Sprintf("found %d activities", len(activities)), "provider_urn", provider.URN)
1✔
603

1✔
604
        grantsPointer := make([]*domain.Grant, len(grants))
1✔
605
        for i, g := range grants {
3✔
606
                g := g
2✔
607
                grantsPointer[i] = &g
2✔
608
        }
2✔
609
        if err := s.providerService.CorrelateGrantActivities(ctx, *provider, grantsPointer, activities); err != nil {
1✔
610
                return fmt.Errorf("correlating grant activities: %w", err)
×
611
        }
×
612

613
        s.logger.Info(ctx, "checking grants dormancy...", "provider_urn", provider.URN)
1✔
614
        var dormantGrants []*domain.Grant
1✔
615
        var dormantGrantsIDs []string
1✔
616
        var dormantGrantsByOwner = map[string][]*domain.Grant{}
1✔
617
        for _, g := range grantsPointer {
3✔
618
                if len(g.Activities) == 0 {
4✔
619
                        g.ExpirationDateReason = fmt.Sprintf("%s: %s", domain.GrantExpirationReasonDormant, criteria.RetainDuration)
2✔
620
                        newExpDate := time.Now().Add(criteria.RetainDuration)
2✔
621
                        g.ExpirationDate = &newExpDate
2✔
622
                        g.IsPermanent = false
2✔
623

2✔
624
                        dormantGrants = append(dormantGrants, g)
2✔
625
                        dormantGrantsIDs = append(dormantGrantsIDs, g.ID)
2✔
626

2✔
627
                        dormantGrantsByOwner[g.Owner] = append(dormantGrantsByOwner[g.Owner], g)
2✔
628
                }
2✔
629
        }
630
        s.logger.Info(ctx, fmt.Sprintf("found %d dormant grants", len(dormantGrants)), "grant_ids", dormantGrantsIDs, "provider_urn", provider.URN)
1✔
631

1✔
632
        if criteria.DryRun {
1✔
633
                s.logger.Info(ctx, "dry run mode, skipping updating grants expiration date", "provider_urn", provider.URN)
×
634
                return nil
×
635
        }
×
636

637
        if err := s.repo.BulkUpsert(ctx, dormantGrants); err != nil {
1✔
638
                return fmt.Errorf("updating grants expiration date: %w", err)
×
639
        }
×
640

641
        go func() {
2✔
642
                ctx := context.WithoutCancel(ctx)
1✔
643
                var notifications []domain.Notification
1✔
644
        prepare_notifications:
1✔
645
                for owner, grants := range dormantGrantsByOwner {
2✔
646
                        var grantsMap []map[string]interface{}
1✔
647
                        var grantIDs []string
1✔
648

1✔
649
                        for _, g := range grants {
3✔
650
                                grantMap, err := utils.StructToMap(g)
2✔
651
                                if err != nil {
2✔
652
                                        s.logger.Error(ctx, "failed to convert grant to map", "error", err)
×
653
                                        continue prepare_notifications
×
654
                                }
655
                                grantsMap = append(grantsMap, grantMap)
2✔
656
                        }
657

658
                        notifications = append(notifications, domain.Notification{
1✔
659
                                User: owner,
1✔
660
                                Labels: map[string]string{
1✔
661
                                        "owner":     owner,
1✔
662
                                        "grant_ids": strings.Join(grantIDs, ", "),
1✔
663
                                },
1✔
664
                                Message: domain.NotificationMessage{
1✔
665
                                        Type: domain.NotificationTypeUnusedGrant,
1✔
666
                                        Variables: map[string]interface{}{
1✔
667
                                                "dormant_grants":       grantsMap,
1✔
668
                                                "period":               criteria.Period.String(),
1✔
669
                                                "retain_duration":      criteria.RetainDuration.String(),
1✔
670
                                                "start_date_formatted": startDate.Format("Jan 02, 2006 15:04:05 UTC"),
1✔
671
                                        },
1✔
672
                                },
1✔
673
                        })
1✔
674
                }
675

676
                if errs := s.notifier.Notify(ctx, notifications); errs != nil {
1✔
677
                        for _, err1 := range errs {
×
678
                                s.logger.Error(ctx, "failed to send notifications", "error", err1.Error(), "provider_urn", provider.URN)
×
679
                        }
×
680
                }
681
        }()
682

683
        return nil
1✔
684
}
685

686
func getAccountSignature(accountType, accountID string) string {
11✔
687
        return fmt.Sprintf("%s:%s", accountType, accountID)
11✔
688
}
11✔
689

690
func groupAccessEntriesByAccount(accessEntries []domain.AccessEntry) map[string][]domain.AccessEntry {
3✔
691
        result := map[string][]domain.AccessEntry{}
3✔
692
        for _, ae := range accessEntries {
11✔
693
                accountSignature := getAccountSignature(ae.AccountType, ae.AccountID)
8✔
694
                result[accountSignature] = append(result[accountSignature], ae)
8✔
695
        }
8✔
696
        return result
3✔
697
}
698

699
// reduceGrantsByProviderRole reduces grants based on configured roles in the provider's resource config and returns reduced grants containing the Role according to the resource config
700
func reduceGrantsByProviderRole(rc domain.ResourceConfig, grants []*domain.Grant) (reducedGrants []*domain.Grant) {
5✔
701
        grantsGroupedByPermission := map[string]*domain.Grant{}
5✔
702
        var allGrantPermissions []string
5✔
703
        for _, g := range grants {
13✔
704
                // TODO: validate if permissions is empty
8✔
705
                allGrantPermissions = append(allGrantPermissions, g.Permissions[0])
8✔
706
                grantsGroupedByPermission[g.Permissions[0]] = g
8✔
707
        }
8✔
708
        sort.Strings(allGrantPermissions)
5✔
709

5✔
710
        // prioritize roles with more permissions
5✔
711
        sort.Slice(rc.Roles, func(i, j int) bool {
5✔
712
                return len(rc.Roles[i].Permissions) > len(rc.Roles[j].Permissions)
×
713
        })
×
714
        for _, role := range rc.Roles {
10✔
715
                rolePermissions := role.GetOrderedPermissions()
5✔
716
                if containing, headIndex := utils.SubsliceExists(allGrantPermissions, rolePermissions); containing {
9✔
717
                        sampleGrant := grantsGroupedByPermission[rolePermissions[0]]
4✔
718
                        sampleGrant.Role = role.ID
4✔
719
                        sampleGrant.Permissions = rolePermissions
4✔
720
                        reducedGrants = append(reducedGrants, sampleGrant)
4✔
721

4✔
722
                        for _, p := range rolePermissions {
10✔
723
                                // delete combined grants
6✔
724
                                delete(grantsGroupedByPermission, p)
6✔
725
                        }
6✔
726
                        allGrantPermissions = append(allGrantPermissions[:headIndex], allGrantPermissions[headIndex+1:]...)
4✔
727
                }
728
        }
729

730
        if len(grantsGroupedByPermission) > 0 {
7✔
731
                // add remaining grants with non-registered provider role
2✔
732
                for _, g := range grantsGroupedByPermission {
4✔
733
                        reducedGrants = append(reducedGrants, g)
2✔
734
                }
2✔
735
        }
736

737
        return
5✔
738
}
739

740
func getGrantIDs(grants []domain.Grant) []string {
1✔
741
        var ids []string
1✔
742
        for _, g := range grants {
3✔
743
                ids = append(ids, g.ID)
2✔
744
        }
2✔
745
        return ids
1✔
746
}
747

748
func (s *Service) GetGrantsTotalCount(ctx context.Context, filters domain.ListGrantsFilter) (int64, error) {
2✔
749
        return s.repo.GetGrantsTotalCount(ctx, filters)
2✔
750
}
2✔
751

752
func (s *Service) ListUserRoles(ctx context.Context, owner string) ([]string, error) {
3✔
753
        if owner == "" {
4✔
754
                return nil, ErrEmptyOwner
1✔
755
        }
1✔
756
        return s.repo.ListUserRoles(ctx, owner)
2✔
757
}
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