// The one-shot sweep that puts the permissions boundary on every sending // user, including ones no walk will ever touch again. package mail import ( "context" "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" ) // UserBoundarySweeper is implemented by a Provisioner whose sending users // carry a permissions ceiling that has to be backfilled (the AWS one). It is // a separate interface, not a Provisioner method, because a ceiling on the // principal is an IAM concept: a provider without one has nothing to sweep. type UserBoundarySweeper interface { SweepUserBoundaries(ctx context.Context) (BoundarySweep, error) } var _ UserBoundarySweeper = (*awsProvisioner)(nil) // BoundarySweep is what one sweep did. Total counts every sending user // listed; Bounded those already carrying the boundary; Fixed the users it // was set on; Failed the users it could not check or fix, with why. type BoundarySweep struct { Total int Bounded int Fixed []string Failed []BoundaryFailure } // BoundaryFailure is one user the sweep could not check or fix. type BoundaryFailure struct { User string Err error } // SweepUserBoundaries lists every user under UserPath and sets the // configured boundary on each that lacks it, or carries another one. // // It exists because EnsureUser and MintKey only backfill a user when a walk // reaches it. An app nobody re-enables, rotates or re-domains keeps an // unbounded user forever, and an orphaned user (its record gone) is never // reached at all. Requiring the boundary on CreateUser and PutUserPolicy // is only safe once this sweep reports every user bounded. // // ListUsers does not return PermissionsBoundary, so each user is read with // GetUser. One user's failure is recorded and the sweep goes on; a failed // listing stops it, with the counts so far, because then Total is not every // user. IAM needed: iam:ListUsers (which AWS scopes to an account, not a // path, so it is granted on Resource "*"; the call itself asks only for // /carlos-mail/), and iam:GetUser and iam:PutUserPermissionsBoundary on // user/carlos-mail/*. func (p *awsProvisioner) SweepUserBoundaries(ctx context.Context) (BoundarySweep, error) { var res BoundarySweep if err := checkUserBoundary(p.boundary); err != nil { return res, err } var marker *string for { out, err := p.iam.ListUsers(ctx, &iam.ListUsersInput{PathPrefix: aws.String(UserPath), Marker: marker}) if err != nil { return res, fmt.Errorf("mail: list users under %s: %w", UserPath, err) } for _, u := range out.Users { name := aws.ToString(u.UserName) res.Total++ got, err := p.iam.GetUser(ctx, &iam.GetUserInput{UserName: aws.String(name)}) if err == nil && got.User == nil { err = fmt.Errorf("no user in the response") } if err != nil { res.Failed = append(res.Failed, BoundaryFailure{User: name, Err: fmt.Errorf("mail: get user %q: %w", name, err)}) continue } if got.User.PermissionsBoundary != nil && aws.ToString(got.User.PermissionsBoundary.PermissionsBoundaryArn) == p.boundary { res.Bounded++ continue } if err := p.backfillBoundary(ctx, name, got.User); err != nil { res.Failed = append(res.Failed, BoundaryFailure{User: name, Err: err}) continue } res.Fixed = append(res.Fixed, name) } if !out.IsTruncated || out.Marker == nil { return res, nil } marker = out.Marker } }