carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The one-shot sweep that puts the permissions boundary on every sending
2// user, including ones no walk will ever touch again.
3package mail
4
5import (
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.
17type UserBoundarySweeper interface {
18 SweepUserBoundaries(ctx context.Context) (BoundarySweep, error)
19}
20
21var _ 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.
26type 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.
34type 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/*.
55func (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