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

goto / guardian / 9344046161

03 Jun 2024 03:59AM UTC coverage: 74.915% (-0.3%) from 75.222%
9344046161

Pull #125

github

rahmatrhd
chore: add additional checking for isExpired
Pull Request #125: feat(grant): add restore grant API

34 of 108 new or added lines in 4 files covered. (31.48%)

73 existing lines in 3 files now uncovered.

9664 of 12900 relevant lines covered (74.91%)

4.48 hits per line

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

81.94
/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
        BulkUpsert(context.Context, []*domain.Grant) error
30
        GetGrantsTotalCount(context.Context, domain.ListGrantsFilter) (int64, error)
31
        ListUserRoles(context.Context, string) ([]string, error)
32
}
33

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

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

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

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

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

66
type Service struct {
67
        repo            repository
68
        providerService providerService
69
        resourceService resourceService
70

71
        notifier    notifier
72
        validator   *validator.Validate
73
        logger      log.Logger
74
        auditLogger auditLogger
75
}
76

77
type ServiceDeps struct {
78
        Repository      repository
79
        ProviderService providerService
80
        ResourceService resourceService
81

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

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

32✔
94
                notifier:    deps.Notifier,
32✔
95
                validator:   deps.Validator,
32✔
96
                logger:      deps.Logger,
32✔
97
                auditLogger: deps.AuditLogger,
32✔
98
        }
32✔
99
}
32✔
100

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

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

112
func (s *Service) Update(ctx context.Context, payload *domain.Grant) error {
2✔
113
        grantDetails, err := s.GetByID(ctx, payload.ID)
2✔
114
        if err != nil {
2✔
115
                return fmt.Errorf("getting grant details: %w", err)
×
116
        }
×
117

118
        if payload.Owner == "" {
3✔
119
                return ErrEmptyOwner
1✔
120
        }
1✔
121
        updatedGrant := &domain.Grant{
1✔
122
                ID: payload.ID,
1✔
123

1✔
124
                // Only allow updating several fields
1✔
125
                Owner: payload.Owner,
1✔
126
        }
1✔
127
        if err := s.repo.Update(ctx, updatedGrant); err != nil {
1✔
128
                return err
×
129
        }
×
130
        previousOwner := grantDetails.Owner
1✔
131
        grantDetails.Owner = updatedGrant.Owner
1✔
132
        grantDetails.UpdatedAt = updatedGrant.UpdatedAt
1✔
133
        *payload = *grantDetails
1✔
134
        s.logger.Info(ctx, "grant updated", "grant_id", grantDetails.ID, "updatedGrant", updatedGrant)
1✔
135

1✔
136
        if err := s.auditLogger.Log(ctx, AuditKeyUpdate, map[string]interface{}{
1✔
137
                "grant_id":      grantDetails.ID,
1✔
138
                "payload":       updatedGrant,
1✔
139
                "updated_grant": payload,
1✔
140
        }); err != nil {
1✔
141
                s.logger.Error(ctx, "failed to record audit log", "error", err)
×
142
        }
×
143

144
        if previousOwner != updatedGrant.Owner {
2✔
145
                message := domain.NotificationMessage{
1✔
146
                        Type: domain.NotificationTypeGrantOwnerChanged,
1✔
147
                        Variables: map[string]interface{}{
1✔
148
                                "grant_id":       grantDetails.ID,
1✔
149
                                "previous_owner": previousOwner,
1✔
150
                                "new_owner":      updatedGrant.Owner,
1✔
151
                        },
1✔
152
                }
1✔
153
                notifications := []domain.Notification{{
1✔
154
                        User: updatedGrant.Owner,
1✔
155
                        Labels: map[string]string{
1✔
156
                                "appeal_id": grantDetails.AppealID,
1✔
157
                                "grant_id":  grantDetails.ID,
1✔
158
                        },
1✔
159
                        Message: message,
1✔
160
                }}
1✔
161
                if previousOwner != "" {
2✔
162
                        notifications = append(notifications, domain.Notification{
1✔
163
                                User: previousOwner,
1✔
164
                                Labels: map[string]string{
1✔
165
                                        "appeal_id": grantDetails.AppealID,
1✔
166
                                        "grant_id":  grantDetails.ID,
1✔
167
                                },
1✔
168
                                Message: message,
1✔
169
                        })
1✔
170
                }
1✔
171
                if errs := s.notifier.Notify(ctx, notifications); errs != nil {
1✔
172
                        for _, err1 := range errs {
×
173
                                s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
174
                        }
×
175
                }
176
        }
177

178
        return nil
1✔
179
}
180

181
func (s *Service) Prepare(ctx context.Context, appeal domain.Appeal) (*domain.Grant, error) {
7✔
182
        // validation
7✔
183
        if err := s.validator.Struct(grantCreation{
7✔
184
                AppealStatus: appeal.Status,
7✔
185
                AccountID:    appeal.AccountID,
7✔
186
                AccountType:  appeal.AccountType,
7✔
187
                ResourceID:   appeal.ResourceID,
7✔
188
        }); err != nil {
11✔
189
                return nil, fmt.Errorf("validating appeal: %w", err)
4✔
190
        }
4✔
191

192
        // converting aapeal into a new grant
193
        return appeal.ToGrant()
3✔
194
}
195

196
func (s *Service) Revoke(ctx context.Context, id, actor, reason string, opts ...Option) (*domain.Grant, error) {
2✔
197
        grant, err := s.GetByID(ctx, id)
2✔
198
        if err != nil {
2✔
199
                return nil, fmt.Errorf("getting grant details: %w", err)
×
200
        }
×
201

202
        revokedGrant := &domain.Grant{}
2✔
203
        *revokedGrant = *grant
2✔
204
        if err := grant.Revoke(actor, reason); err != nil {
2✔
205
                return nil, err
×
206
        }
×
207
        if err := s.repo.Update(ctx, grant); err != nil {
2✔
208
                return nil, fmt.Errorf("updating grant record in db: %w", err)
×
209
        }
×
210

211
        options := s.getOptions(opts...)
2✔
212

2✔
213
        if !options.skipRevokeInProvider {
3✔
214
                if err := s.providerService.RevokeAccess(ctx, *grant); err != nil {
1✔
215
                        if err := s.repo.Update(ctx, grant); err != nil {
×
216
                                return nil, fmt.Errorf("failed to rollback grant status: %w", err)
×
217
                        }
×
218
                        return nil, fmt.Errorf("removing grant in provider: %w", err)
×
219
                }
220
        }
221

222
        if !options.skipNotification {
3✔
223
                if errs := s.notifier.Notify(ctx, []domain.Notification{{
1✔
224
                        User: grant.CreatedBy,
1✔
225
                        Labels: map[string]string{
1✔
226
                                "appeal_id": grant.AppealID,
1✔
227
                                "grant_id":  grant.ID,
1✔
228
                        },
1✔
229
                        Message: domain.NotificationMessage{
1✔
230
                                Type: domain.NotificationTypeAccessRevoked,
1✔
231
                                Variables: map[string]interface{}{
1✔
232
                                        "resource_name": fmt.Sprintf("%s (%s: %s)", grant.Resource.Name, grant.Resource.ProviderType, grant.Resource.URN),
1✔
233
                                        "role":          grant.Role,
1✔
234
                                        "account_type":  grant.AccountType,
1✔
235
                                        "account_id":    grant.AccountID,
1✔
236
                                        "requestor":     grant.Owner,
1✔
237
                                        "revoke_reason": grant.RevokeReason,
1✔
238
                                },
1✔
239
                        },
1✔
240
                }}); errs != nil {
1✔
241
                        for _, err1 := range errs {
×
242
                                s.logger.Error(ctx, "failed to send notifications", "error", err1.Error())
×
243
                        }
×
244
                }
245
        }
246

247
        s.logger.Info(ctx, "grant revoked", "grant_id", id)
2✔
248

2✔
249
        if err := s.auditLogger.Log(ctx, AuditKeyRevoke, map[string]interface{}{
2✔
250
                "grant_id": id,
2✔
251
                "reason":   reason,
2✔
252
        }); err != nil {
2✔
253
                s.logger.Error(ctx, "failed to record audit log", "error", err)
×
254
        }
×
255

256
        return grant, nil
2✔
257
}
258

259
func (s *Service) Restore(ctx context.Context, id, actor, reason string) (*domain.Grant, error) {
7✔
260
        grant, err := s.GetByID(ctx, id)
7✔
261
        if err != nil {
8✔
262
                return nil, fmt.Errorf("getting grant details: %w", err)
1✔
263
        }
1✔
264

265
        if err := grant.Restore(actor, reason); err != nil {
9✔
266
                return nil, fmt.Errorf("%w: %s", ErrInvalidRequest, err.Error())
3✔
267
        }
3✔
268

269
        if err := s.providerService.GrantAccess(ctx, *grant); err != nil {
3✔
NEW
UNCOV
270
                return nil, fmt.Errorf("granting access in provider: %w", err)
×
NEW
UNCOV
271
        }
×
272

273
        if err := s.repo.Update(ctx, grant); err != nil {
3✔
NEW
274
                return nil, fmt.Errorf("updating grant record in db: %w", err)
×
NEW
275
        }
×
276

277
        if err := s.auditLogger.Log(ctx, AuditKeyRestore, map[string]interface{}{
3✔
278
                "grant_id": id,
3✔
279
                "reason":   reason,
3✔
280
        }); err != nil {
3✔
NEW
281
                s.logger.Error(ctx, "failed to record audit log", "error", err)
×
NEW
282
        }
×
283

284
        return grant, nil
3✔
285
}
286

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

292
        grants, err := s.List(ctx, domain.ListGrantsFilter{
1✔
293
                Statuses:      []string{string(domain.GrantStatusActive)},
1✔
294
                AccountIDs:    filter.AccountIDs,
1✔
295
                ProviderTypes: filter.ProviderTypes,
1✔
296
                ProviderURNs:  filter.ProviderURNs,
1✔
297
                ResourceTypes: filter.ResourceTypes,
1✔
298
                ResourceURNs:  filter.ResourceURNs,
1✔
299
        })
1✔
300
        if err != nil {
1✔
301
                return nil, fmt.Errorf("listing active grants: %w", err)
×
302
        }
×
303
        if len(grants) == 0 {
1✔
304
                return nil, nil
×
305
        }
×
306

307
        result := make([]*domain.Grant, 0)
1✔
308
        batchSize := 10
1✔
309
        timeLimiter := make(chan int, batchSize)
1✔
310

1✔
311
        for i := 1; i <= batchSize; i++ {
11✔
312
                timeLimiter <- i
10✔
313
        }
10✔
314

315
        go func() {
2✔
316
                for range time.Tick(1 * time.Second) {
1✔
UNCOV
317
                        for i := 1; i <= batchSize; i++ {
×
UNCOV
318
                                timeLimiter <- i
×
UNCOV
319
                        }
×
320
                }
321
        }()
322

323
        totalRequests := len(grants)
1✔
324
        done := make(chan *domain.Grant, totalRequests)
1✔
325
        resourceGrantMap := make(map[string][]*domain.Grant, 0)
1✔
326

1✔
327
        for i, grant := range grants {
3✔
328
                var resourceGrants []*domain.Grant
2✔
329
                var ok bool
2✔
330
                if resourceGrants, ok = resourceGrantMap[grant.ResourceID]; ok {
3✔
331
                        resourceGrants = append(resourceGrants, &grants[i])
1✔
332
                } else {
2✔
333
                        resourceGrants = []*domain.Grant{&grants[i]}
1✔
334
                }
1✔
335
                resourceGrantMap[grant.ResourceID] = resourceGrants
2✔
336
        }
337

338
        for _, resourceGrants := range resourceGrantMap {
2✔
339
                go s.expiredInActiveUserAccess(ctx, timeLimiter, done, actor, reason, resourceGrants)
1✔
340
        }
1✔
341

342
        var successRevoke []string
1✔
343
        var failedRevoke []string
1✔
344
        for {
3✔
345
                select {
2✔
346
                case grant := <-done:
2✔
347
                        if grant.Status == domain.GrantStatusInactive {
4✔
348
                                successRevoke = append(successRevoke, grant.ID)
2✔
349
                        } else {
2✔
UNCOV
350
                                failedRevoke = append(failedRevoke, grant.ID)
×
UNCOV
351
                        }
×
352
                        result = append(result, grant)
2✔
353
                        if len(result) == totalRequests {
3✔
354
                                s.logger.Info(ctx, "successful grant revocation", "count", len(successRevoke), "ids", successRevoke)
1✔
355
                                if len(failedRevoke) > 0 {
1✔
UNCOV
356
                                        s.logger.Info(ctx, "failed grant revocation", "count", len(failedRevoke), "ids", failedRevoke)
×
UNCOV
357
                                }
×
358
                                return result, nil
1✔
359
                        }
360
                }
361
        }
362
}
363

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

2✔
368
                revokedGrant := &domain.Grant{}
2✔
369
                *revokedGrant = *grant
2✔
370
                if err := revokedGrant.Revoke(actor, reason); err != nil {
2✔
UNCOV
371
                        s.logger.Error(ctx, "failed to revoke grant", "id", grant.ID, "error", err)
×
UNCOV
372
                        return
×
UNCOV
373
                }
×
374
                if err := s.providerService.RevokeAccess(ctx, *grant); err != nil {
2✔
UNCOV
375
                        done <- grant
×
UNCOV
376
                        s.logger.Error(ctx, "failed to revoke grant in provider", "id", grant.ID, "error", err)
×
UNCOV
377
                        return
×
UNCOV
378
                }
×
379

380
                revokedGrant.Status = domain.GrantStatusInactive
2✔
381
                if err := s.repo.Update(ctx, revokedGrant); err != nil {
2✔
382
                        done <- grant
×
UNCOV
383
                        s.logger.Error(ctx, "failed to update access-revoke status", "id", grant.ID, "error", err)
×
384
                        return
×
385
                } else {
2✔
386
                        done <- revokedGrant
2✔
387
                        s.logger.Info(ctx, "grant revoked", "id", grant.ID)
2✔
388
                }
2✔
389
        }
390
}
391

392
type ImportFromProviderCriteria struct {
393
        ProviderID    string `validate:"required"`
394
        ResourceIDs   []string
395
        ResourceTypes []string
396
        ResourceURNs  []string
397
}
398

399
func (s *Service) ImportFromProvider(ctx context.Context, criteria ImportFromProviderCriteria) ([]*domain.Grant, error) {
4✔
400
        p, err := s.providerService.GetByID(ctx, criteria.ProviderID)
4✔
401
        if err != nil {
4✔
UNCOV
402
                return nil, fmt.Errorf("getting provider details: %w", err)
×
UNCOV
403
        }
×
404

405
        listResourcesFilter := domain.ListResourcesFilter{
4✔
406
                ProviderType: p.Type,
4✔
407
                ProviderURN:  p.URN,
4✔
408
        }
4✔
409
        listGrantsFilter := domain.ListGrantsFilter{
4✔
410
                Statuses:      []string{string(domain.GrantStatusActive)},
4✔
411
                ProviderTypes: []string{p.Type},
4✔
412
                ProviderURNs:  []string{p.URN},
4✔
413
        }
4✔
414
        if criteria.ResourceIDs != nil {
4✔
UNCOV
415
                listResourcesFilter.IDs = criteria.ResourceIDs
×
UNCOV
416
                listGrantsFilter.ResourceIDs = criteria.ResourceIDs
×
417
        } else {
4✔
418
                listResourcesFilter.ResourceTypes = criteria.ResourceTypes
4✔
419
                listResourcesFilter.ResourceURNs = criteria.ResourceURNs
4✔
420

4✔
421
                listGrantsFilter.ResourceTypes = criteria.ResourceTypes
4✔
422
                listGrantsFilter.ResourceURNs = criteria.ResourceURNs
4✔
423
        }
4✔
424
        resources, err := s.resourceService.Find(ctx, listResourcesFilter)
4✔
425
        if err != nil {
4✔
UNCOV
426
                return nil, fmt.Errorf("getting resources: %w", err)
×
UNCOV
427
        }
×
428

429
        resourceAccess, err := s.providerService.ListAccess(ctx, *p, resources)
4✔
430
        if err != nil {
4✔
UNCOV
431
                return nil, fmt.Errorf("fetching access from provider: %w", err)
×
UNCOV
432
        }
×
433

434
        resourceConfigs := make(map[string]*domain.ResourceConfig)
4✔
435
        for _, rc := range p.Config.Resources {
8✔
436
                resourceConfigs[rc.Type] = rc
4✔
437
        }
4✔
438

439
        resourcesMap := make(map[string]*domain.Resource)
4✔
440
        for _, r := range resources {
8✔
441
                resourcesMap[r.URN] = r
4✔
442
        }
4✔
443

444
        activeGrants, err := s.repo.List(ctx, listGrantsFilter)
4✔
445
        if err != nil {
4✔
UNCOV
446
                return nil, fmt.Errorf("getting active grants: %w", err)
×
UNCOV
447
        }
×
448
        // map[resourceURN]map[accounttype:accountId]map[permissionsKey]grant
449
        activeGrantsMap := map[string]map[string]map[string]*domain.Grant{}
4✔
450
        for i, g := range activeGrants {
7✔
451
                if activeGrantsMap[g.Resource.URN] == nil {
5✔
452
                        activeGrantsMap[g.Resource.URN] = map[string]map[string]*domain.Grant{}
2✔
453
                }
2✔
454

455
                accountSignature := getAccountSignature(g.AccountType, g.AccountID)
3✔
456
                if activeGrantsMap[g.Resource.URN][accountSignature] == nil {
6✔
457
                        activeGrantsMap[g.Resource.URN][accountSignature] = map[string]*domain.Grant{}
3✔
458
                }
3✔
459

460
                activeGrantsMap[g.Resource.URN][accountSignature][g.PermissionsKey()] = &activeGrants[i]
3✔
461
        }
462

463
        var newAndUpdatedGrants []*domain.Grant
4✔
464
        for rURN, accessEntries := range resourceAccess {
7✔
465
                resource, ok := resourcesMap[rURN]
3✔
466
                if !ok {
3✔
UNCOV
467
                        continue // skip access for resources that not yet added to guardian
×
468
                }
469

470
                importedGrants := []*domain.Grant{}
3✔
471
                for accountSignature, accessEntries := range groupAccessEntriesByAccount(accessEntries) {
8✔
472
                        // convert access entries to grants
5✔
473
                        var grants []*domain.Grant
5✔
474
                        for _, ae := range accessEntries {
13✔
475
                                g := ae.ToGrant(*resource)
8✔
476
                                grants = append(grants, &g)
8✔
477
                        }
8✔
478

479
                        // group grants for the same account (accountGrants) by provider role
480
                        rc := resourceConfigs[resource.Type]
5✔
481
                        grants = reduceGrantsByProviderRole(*rc, grants)
5✔
482
                        for i, g := range grants {
11✔
483
                                key := g.PermissionsKey()
6✔
484
                                if existingGrant, ok := activeGrantsMap[rURN][accountSignature][key]; ok {
8✔
485
                                        // replace imported grant values with existing grant
2✔
486
                                        *grants[i] = *existingGrant
2✔
487

2✔
488
                                        // remove updated grant from active grants map
2✔
489
                                        delete(activeGrantsMap[rURN][accountSignature], key)
2✔
490
                                }
2✔
491
                        }
492

493
                        importedGrants = append(importedGrants, grants...)
5✔
494
                }
495

496
                if len(importedGrants) > 0 {
6✔
497
                        if err := s.repo.BulkUpsert(ctx, importedGrants); err != nil {
3✔
UNCOV
498
                                return nil, fmt.Errorf("inserting new and updated grants into the db for %q: %w", rURN, err)
×
UNCOV
499
                        }
×
500
                        newAndUpdatedGrants = append(newAndUpdatedGrants, importedGrants...)
3✔
501
                }
502
        }
503

504
        // mark remaining active grants as inactive
505
        var deactivatedGrants []*domain.Grant
4✔
506
        for _, v := range activeGrantsMap {
6✔
507
                for _, v2 := range v {
5✔
508
                        for _, g := range v2 {
4✔
509
                                g.StatusInProvider = domain.GrantStatusInactive
1✔
510
                                deactivatedGrants = append(deactivatedGrants, g)
1✔
511
                        }
1✔
512
                }
513
        }
514
        if len(deactivatedGrants) > 0 {
5✔
515
                if err := s.repo.BulkUpsert(ctx, deactivatedGrants); err != nil {
1✔
UNCOV
516
                        return nil, fmt.Errorf("updating grants provider status: %w", err)
×
UNCOV
517
                }
×
518
        }
519

520
        return newAndUpdatedGrants, nil
4✔
521
}
522

523
func (s *Service) DormancyCheck(ctx context.Context, criteria domain.DormancyCheckCriteria) error {
1✔
524
        if err := criteria.Validate(); err != nil {
1✔
525
                return fmt.Errorf("invalid dormancy check criteria: %w", err)
×
526
        }
×
527
        startDate := time.Now().Add(-criteria.Period)
1✔
528

1✔
529
        provider, err := s.providerService.GetByID(ctx, criteria.ProviderID)
1✔
530
        if err != nil {
1✔
UNCOV
531
                return fmt.Errorf("getting provider details: %w", err)
×
UNCOV
532
        }
×
533

534
        s.logger.Info(ctx, "getting active grants", "provider_urn", provider.URN)
1✔
535
        grants, err := s.List(ctx, domain.ListGrantsFilter{
1✔
536
                Statuses:      []string{string(domain.GrantStatusActive)}, // TODO: evaluate later to use status_in_provider
1✔
537
                ProviderTypes: []string{provider.Type},
1✔
538
                ProviderURNs:  []string{provider.URN},
1✔
539
                CreatedAtLte:  startDate,
1✔
540
        })
1✔
541
        if err != nil {
1✔
UNCOV
542
                return fmt.Errorf("listing active grants: %w", err)
×
UNCOV
543
        }
×
544
        if len(grants) == 0 {
1✔
UNCOV
545
                s.logger.Info(ctx, "no active grants found", "provider_urn", provider.URN)
×
UNCOV
546
                return nil
×
UNCOV
547
        }
×
548
        grantIDs := getGrantIDs(grants)
1✔
549
        s.logger.Info(ctx, fmt.Sprintf("found %d active grants", len(grants)), "grant_ids", grantIDs, "provider_urn", provider.URN)
1✔
550

1✔
551
        var accountIDs []string
1✔
552
        for _, g := range grants {
3✔
553
                accountIDs = append(accountIDs, g.AccountID)
2✔
554
        }
2✔
555
        accountIDs = slices.UniqueStringSlice(accountIDs)
1✔
556

1✔
557
        s.logger.Info(ctx, "getting activities", "provider_urn", provider.URN)
1✔
558
        activities, err := s.providerService.ListActivities(ctx, *provider, domain.ListActivitiesFilter{
1✔
559
                AccountIDs:   accountIDs,
1✔
560
                TimestampGte: &startDate,
1✔
561
        })
1✔
562
        if err != nil {
1✔
UNCOV
563
                return fmt.Errorf("listing activities for provider %q: %w", provider.URN, err)
×
UNCOV
564
        }
×
565
        s.logger.Info(ctx, fmt.Sprintf("found %d activities", len(activities)), "provider_urn", provider.URN)
1✔
566

1✔
567
        grantsPointer := make([]*domain.Grant, len(grants))
1✔
568
        for i, g := range grants {
3✔
569
                g := g
2✔
570
                grantsPointer[i] = &g
2✔
571
        }
2✔
572
        if err := s.providerService.CorrelateGrantActivities(ctx, *provider, grantsPointer, activities); err != nil {
1✔
573
                return fmt.Errorf("correlating grant activities: %w", err)
×
UNCOV
574
        }
×
575

576
        s.logger.Info(ctx, "checking grants dormancy...", "provider_urn", provider.URN)
1✔
577
        var dormantGrants []*domain.Grant
1✔
578
        var dormantGrantsIDs []string
1✔
579
        var dormantGrantsByOwner = map[string][]*domain.Grant{}
1✔
580
        for _, g := range grantsPointer {
3✔
581
                if len(g.Activities) == 0 {
4✔
582
                        g.ExpirationDateReason = fmt.Sprintf("%s: %s", domain.GrantExpirationReasonDormant, criteria.RetainDuration)
2✔
583
                        newExpDate := time.Now().Add(criteria.RetainDuration)
2✔
584
                        g.ExpirationDate = &newExpDate
2✔
585
                        g.IsPermanent = false
2✔
586

2✔
587
                        dormantGrants = append(dormantGrants, g)
2✔
588
                        dormantGrantsIDs = append(dormantGrantsIDs, g.ID)
2✔
589

2✔
590
                        dormantGrantsByOwner[g.Owner] = append(dormantGrantsByOwner[g.Owner], g)
2✔
591
                }
2✔
592
        }
593
        s.logger.Info(ctx, fmt.Sprintf("found %d dormant grants", len(dormantGrants)), "grant_ids", dormantGrantsIDs, "provider_urn", provider.URN)
1✔
594

1✔
595
        if criteria.DryRun {
1✔
UNCOV
596
                s.logger.Info(ctx, "dry run mode, skipping updating grants expiration date", "provider_urn", provider.URN)
×
UNCOV
597
                return nil
×
UNCOV
598
        }
×
599

600
        if err := s.repo.BulkUpsert(ctx, dormantGrants); err != nil {
1✔
UNCOV
601
                return fmt.Errorf("updating grants expiration date: %w", err)
×
UNCOV
602
        }
×
603

604
        var notifications []domain.Notification
1✔
605
prepare_notifications:
1✔
606
        for owner, grants := range dormantGrantsByOwner {
2✔
607
                var grantsMap []map[string]interface{}
1✔
608
                var grantIDs []string
1✔
609

1✔
610
                for _, g := range grants {
3✔
611
                        grantMap, err := utils.StructToMap(g)
2✔
612
                        if err != nil {
2✔
UNCOV
613
                                s.logger.Error(ctx, "failed to convert grant to map", "error", err)
×
UNCOV
614
                                continue prepare_notifications
×
615
                        }
616
                        grantsMap = append(grantsMap, grantMap)
2✔
617
                }
618

619
                notifications = append(notifications, domain.Notification{
1✔
620
                        User: owner,
1✔
621
                        Labels: map[string]string{
1✔
622
                                "owner":     owner,
1✔
623
                                "grant_ids": strings.Join(grantIDs, ", "),
1✔
624
                        },
1✔
625
                        Message: domain.NotificationMessage{
1✔
626
                                Type: domain.NotificationTypeUnusedGrant,
1✔
627
                                Variables: map[string]interface{}{
1✔
628
                                        "dormant_grants":       grantsMap,
1✔
629
                                        "period":               criteria.Period.String(),
1✔
630
                                        "retain_duration":      criteria.RetainDuration.String(),
1✔
631
                                        "start_date_formatted": startDate.Format("Jan 02, 2006 15:04:05 UTC"),
1✔
632
                                },
1✔
633
                        },
1✔
634
                })
1✔
635
        }
636

637
        if errs := s.notifier.Notify(ctx, notifications); errs != nil {
1✔
UNCOV
638
                for _, err1 := range errs {
×
UNCOV
639
                        s.logger.Error(ctx, "failed to send notifications", "error", err1.Error(), "provider_urn", provider.URN)
×
UNCOV
640
                }
×
641
        }
642

643
        return nil
1✔
644
}
645

646
func getAccountSignature(accountType, accountID string) string {
11✔
647
        return fmt.Sprintf("%s:%s", accountType, accountID)
11✔
648
}
11✔
649

650
func groupAccessEntriesByAccount(accessEntries []domain.AccessEntry) map[string][]domain.AccessEntry {
3✔
651
        result := map[string][]domain.AccessEntry{}
3✔
652
        for _, ae := range accessEntries {
11✔
653
                accountSignature := getAccountSignature(ae.AccountType, ae.AccountID)
8✔
654
                result[accountSignature] = append(result[accountSignature], ae)
8✔
655
        }
8✔
656
        return result
3✔
657
}
658

659
// 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
660
func reduceGrantsByProviderRole(rc domain.ResourceConfig, grants []*domain.Grant) (reducedGrants []*domain.Grant) {
5✔
661
        grantsGroupedByPermission := map[string]*domain.Grant{}
5✔
662
        var allGrantPermissions []string
5✔
663
        for _, g := range grants {
13✔
664
                // TODO: validate if permissions is empty
8✔
665
                allGrantPermissions = append(allGrantPermissions, g.Permissions[0])
8✔
666
                grantsGroupedByPermission[g.Permissions[0]] = g
8✔
667
        }
8✔
668
        sort.Strings(allGrantPermissions)
5✔
669

5✔
670
        // prioritize roles with more permissions
5✔
671
        sort.Slice(rc.Roles, func(i, j int) bool {
5✔
UNCOV
672
                return len(rc.Roles[i].Permissions) > len(rc.Roles[j].Permissions)
×
UNCOV
673
        })
×
674
        for _, role := range rc.Roles {
10✔
675
                rolePermissions := role.GetOrderedPermissions()
5✔
676
                if containing, headIndex := utils.SubsliceExists(allGrantPermissions, rolePermissions); containing {
9✔
677
                        sampleGrant := grantsGroupedByPermission[rolePermissions[0]]
4✔
678
                        sampleGrant.Role = role.ID
4✔
679
                        sampleGrant.Permissions = rolePermissions
4✔
680
                        reducedGrants = append(reducedGrants, sampleGrant)
4✔
681

4✔
682
                        for _, p := range rolePermissions {
10✔
683
                                // delete combined grants
6✔
684
                                delete(grantsGroupedByPermission, p)
6✔
685
                        }
6✔
686
                        allGrantPermissions = append(allGrantPermissions[:headIndex], allGrantPermissions[headIndex+1:]...)
4✔
687
                }
688
        }
689

690
        if len(grantsGroupedByPermission) > 0 {
7✔
691
                // add remaining grants with non-registered provider role
2✔
692
                for _, g := range grantsGroupedByPermission {
4✔
693
                        reducedGrants = append(reducedGrants, g)
2✔
694
                }
2✔
695
        }
696

697
        return
5✔
698
}
699

700
func getGrantIDs(grants []domain.Grant) []string {
1✔
701
        var ids []string
1✔
702
        for _, g := range grants {
3✔
703
                ids = append(ids, g.ID)
2✔
704
        }
2✔
705
        return ids
1✔
706
}
707

708
func (s *Service) GetGrantsTotalCount(ctx context.Context, filters domain.ListGrantsFilter) (int64, error) {
2✔
709
        return s.repo.GetGrantsTotalCount(ctx, filters)
2✔
710
}
2✔
711

712
func (s *Service) ListUserRoles(ctx context.Context, owner string) ([]string, error) {
3✔
713
        if owner == "" {
4✔
714
                return nil, ErrEmptyOwner
1✔
715
        }
1✔
716
        return s.repo.ListUserRoles(ctx, owner)
2✔
717
}
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