| 1 | // The backend seam (spec §5): everything the console needs a backend to do, |
| 2 | // and nothing an app ever calls. |
| 3 | // |
| 4 | // Every method is idempotent — "already exists" and "already gone" are both |
| 5 | // successes — because every walk that drives one must be re-runnable after a |
| 6 | // half-failure. That is not a style preference; it is the only reason a |
| 7 | // crashed provisioning run can be repaired by running it again rather than |
| 8 | // by hand. |
| 9 | package mail |
| 10 | |
| 11 | import ( |
| 12 | "context" |
| 13 | "errors" |
| 14 | ) |
| 15 | |
| 16 | // Identity is what EnsureIdentity answers: the ARN a policy names and the |
| 17 | // DKIM tokens whose CNAMEs must be published. The tokens are STABLE for the |
| 18 | // life of the identity — re-ensuring returns the same three, because fresh |
| 19 | // ones would orphan DNS already published (and, for a custom domain, DNS |
| 20 | // somebody else published by hand). |
| 21 | type Identity struct { |
| 22 | ARN string |
| 23 | DKIMTokens []string |
| 24 | // Created is true when this call made the identity, false when it was |
| 25 | // already there. The walk records it, because an identity CARLOS did not |
| 26 | // create is not CARLOS's to delete (discussion #216). |
| 27 | Created bool |
| 28 | } |
| 29 | |
| 30 | // ErrIdentityExists is EnsureIdentity's refusal, with adopt false, of a |
| 31 | // domain that already has an identity. Nothing about the identity has been |
| 32 | // read or changed when it is returned. |
| 33 | var ErrIdentityExists = errors.New("mail: identity already exists") |
| 34 | |
| 35 | // Status is SES's own verdict on an identity. All three must read |
| 36 | // "SUCCESS" before the identity is usable; a domain that is verified for |
| 37 | // sending but whose MAIL FROM has not converged still sends, from the |
| 38 | // wrong envelope domain, and fails DMARC alignment. |
| 39 | type Status struct { |
| 40 | DKIM string |
| 41 | MailFrom string |
| 42 | Sending string |
| 43 | // ConfigSet is the configuration set SES currently has BOUND to this |
| 44 | // identity, empty for none. It is read here because |
| 45 | // GetEmailIdentity already returns it and this call already happens on |
| 46 | // every verify — which makes drift detection free (issue #272). |
| 47 | // |
| 48 | // The alternative was reconciling on every status read, which would put |
| 49 | // a GetEmailIdentity per domain on a path that today answers from the |
| 50 | // bucket alone, and would make a working read fail whenever AWS is |
| 51 | // unreachable. Reading it where the call is already being made costs |
| 52 | // nothing and catches the same drift a beat later. |
| 53 | ConfigSet string |
| 54 | } |
| 55 | |
| 56 | // Verified reports whether every axis has converged. |
| 57 | func (s Status) Verified() bool { |
| 58 | return s.DKIM == "SUCCESS" && s.MailFrom == "SUCCESS" && s.Sending == "SUCCESS" |
| 59 | } |
| 60 | |
| 61 | type Provisioner interface { |
| 62 | // EnsureTenant creates the account's SES tenant if absent. Additive: |
| 63 | // a backend without tenant support returns nil and the walks still |
| 64 | // isolate correctly, because isolation is the IAM policy (spec §4). |
| 65 | EnsureTenant(ctx context.Context, name string) error |
| 66 | // EnsureConfigSet creates the event destination that feeds bounce and |
| 67 | // complaint ingestion (spec §9). |
| 68 | EnsureConfigSet(ctx context.Context, region, name, snsTopicARN string) error |
| 69 | // SetIdentityConfigSet binds an identity to the configuration set that |
| 70 | // carries SES's event destination (spec §9; C1, review follow-up). |
| 71 | // EnsureConfigSet alone creates the SET — it does not touch any |
| 72 | // identity. SES only emits events (send, bounce, complaint) for a |
| 73 | // message sent WITH a configuration set, so an identity that never |
| 74 | // gets this call generates NO events, ever, no matter how many |
| 75 | // messages it sends: every count stays zero forever, the daily cap |
| 76 | // never enforces, and the automatic abuse pause never fires. Called |
| 77 | // once per (region, domain) — idempotent, like every verb here. |
| 78 | SetIdentityConfigSet(ctx context.Context, region, domain, configSet string) error |
| 79 | |
| 80 | // AssociateTenantResource associates an identity, a configuration set or |
| 81 | // a template with a tenant. Associating does not modify the resource and |
| 82 | // one resource may belong to many tenants, so this is safe to call on an |
| 83 | // identity another product owns. It has exactly one side effect on the |
| 84 | // owner: an identity cannot be deleted while an association exists, |
| 85 | // which is why DisassociateTenantResource has to run before |
| 86 | // DeleteIdentity. Idempotent, like every verb here. |
| 87 | AssociateTenantResource(ctx context.Context, tenant, resourceARN string) error |
| 88 | // DisassociateTenantResource removes one association. "Already gone" is |
| 89 | // success. |
| 90 | DisassociateTenantResource(ctx context.Context, tenant, resourceARN string) error |
| 91 | |
| 92 | // EnsureIdentity creates the domain identity, enables Easy DKIM and |
| 93 | // sets the custom MAIL FROM subdomain, all idempotently. |
| 94 | // |
| 95 | // adopt says whether an identity that already exists may be taken over: |
| 96 | // its DKIM tokens read back and its MAIL FROM rewritten. With adopt |
| 97 | // false, an existing identity is refused with ErrIdentityExists and left |
| 98 | // exactly as it was — the walk passes false unless the identity is one |
| 99 | // this account already holds or has proved it controls (#216). |
| 100 | EnsureIdentity(ctx context.Context, region, domain, mailFrom string, adopt bool) (Identity, error) |
| 101 | IdentityStatus(ctx context.Context, region, domain string) (Status, error) |
| 102 | DeleteIdentity(ctx context.Context, region, domain string) error |
| 103 | |
| 104 | // EnsureUser creates the principal if absent and writes its inline |
| 105 | // policy wholesale. It MUST refuse an empty grant rather than create a |
| 106 | // principal with no policy. |
| 107 | EnsureUser(ctx context.Context, name, path string, identityARNs, configSetARNs, domains []string) (arn string, err error) |
| 108 | MintKey(ctx context.Context, user string) (id, secret string, err error) |
| 109 | // SetKeyActive is the pause (spec §9): instant, reversible, and it |
| 110 | // needs no policy rewrite. |
| 111 | SetKeyActive(ctx context.Context, user, keyID string, active bool) error |
| 112 | DeleteKey(ctx context.Context, user, keyID string) error |
| 113 | // ReapKeys deletes every access key of user NOT named in keep. A key |
| 114 | // minted before a failed delivery is recorded nowhere, so only |
| 115 | // list-and-delete can find it. |
| 116 | ReapKeys(ctx context.Context, user string, keep []string) error |
| 117 | DeleteUser(ctx context.Context, user string) error |
| 118 | } |
| 119 | |