| 1 | // The one-shot sweep that puts the permissions boundary on every sending |
| 2 | // user, including ones no walk will ever touch again. |
| 3 | package mail |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | |
| 9 | "github.com/aws/aws-sdk-go-v2/aws" |
| 10 | "github.com/aws/aws-sdk-go-v2/service/iam" |
| 11 | ) |
| 12 | |
| 13 | // UserBoundarySweeper is implemented by a Provisioner whose sending users |
| 14 | // carry a permissions ceiling that has to be backfilled (the AWS one). It is |
| 15 | // a separate interface, not a Provisioner method, because a ceiling on the |
| 16 | // principal is an IAM concept: a provider without one has nothing to sweep. |
| 17 | type UserBoundarySweeper interface { |
| 18 | SweepUserBoundaries(ctx context.Context) (BoundarySweep, error) |
| 19 | } |
| 20 | |
| 21 | var _ UserBoundarySweeper = (*awsProvisioner)(nil) |
| 22 | |
| 23 | // BoundarySweep is what one sweep did. Total counts every sending user |
| 24 | // listed; Bounded those already carrying the boundary; Fixed the users it |
| 25 | // was set on; Failed the users it could not check or fix, with why. |
| 26 | type BoundarySweep struct { |
| 27 | Total int |
| 28 | Bounded int |
| 29 | Fixed []string |
| 30 | Failed []BoundaryFailure |
| 31 | } |
| 32 | |
| 33 | // BoundaryFailure is one user the sweep could not check or fix. |
| 34 | type BoundaryFailure struct { |
| 35 | User string |
| 36 | Err error |
| 37 | } |
| 38 | |
| 39 | // SweepUserBoundaries lists every user under UserPath and sets the |
| 40 | // configured boundary on each that lacks it, or carries another one. |
| 41 | // |
| 42 | // It exists because EnsureUser and MintKey only backfill a user when a walk |
| 43 | // reaches it. An app nobody re-enables, rotates or re-domains keeps an |
| 44 | // unbounded user forever, and an orphaned user (its record gone) is never |
| 45 | // reached at all. Requiring the boundary on CreateUser and PutUserPolicy |
| 46 | // is only safe once this sweep reports every user bounded. |
| 47 | // |
| 48 | // ListUsers does not return PermissionsBoundary, so each user is read with |
| 49 | // GetUser. One user's failure is recorded and the sweep goes on; a failed |
| 50 | // listing stops it, with the counts so far, because then Total is not every |
| 51 | // user. IAM needed: iam:ListUsers (which AWS scopes to an account, not a |
| 52 | // path, so it is granted on Resource "*"; the call itself asks only for |
| 53 | // /carlos-mail/), and iam:GetUser and iam:PutUserPermissionsBoundary on |
| 54 | // user/carlos-mail/*. |
| 55 | func (p *awsProvisioner) SweepUserBoundaries(ctx context.Context) (BoundarySweep, error) { |
| 56 | var res BoundarySweep |
| 57 | if err := checkUserBoundary(p.boundary); err != nil { |
| 58 | return res, err |
| 59 | } |
| 60 | var marker *string |
| 61 | for { |
| 62 | out, err := p.iam.ListUsers(ctx, &iam.ListUsersInput{PathPrefix: aws.String(UserPath), Marker: marker}) |
| 63 | if err != nil { |
| 64 | return res, fmt.Errorf("mail: list users under %s: %w", UserPath, err) |
| 65 | } |
| 66 | for _, u := range out.Users { |
| 67 | name := aws.ToString(u.UserName) |
| 68 | res.Total++ |
| 69 | got, err := p.iam.GetUser(ctx, &iam.GetUserInput{UserName: aws.String(name)}) |
| 70 | if err == nil && got.User == nil { |
| 71 | err = fmt.Errorf("no user in the response") |
| 72 | } |
| 73 | if err != nil { |
| 74 | res.Failed = append(res.Failed, BoundaryFailure{User: name, Err: fmt.Errorf("mail: get user %q: %w", name, err)}) |
| 75 | continue |
| 76 | } |
| 77 | if got.User.PermissionsBoundary != nil && aws.ToString(got.User.PermissionsBoundary.PermissionsBoundaryArn) == p.boundary { |
| 78 | res.Bounded++ |
| 79 | continue |
| 80 | } |
| 81 | if err := p.backfillBoundary(ctx, name, got.User); err != nil { |
| 82 | res.Failed = append(res.Failed, BoundaryFailure{User: name, Err: err}) |
| 83 | continue |
| 84 | } |
| 85 | res.Fixed = append(res.Fixed, name) |
| 86 | } |
| 87 | if !out.IsTruncated || out.Marker == nil { |
| 88 | return res, nil |
| 89 | } |
| 90 | marker = out.Marker |
| 91 | } |
| 92 | } |
| 93 | |