| 1 | import * as iam from "@distilled.cloud/aws/iam"; |
| 2 | import type * as Duration from "effect/Duration"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Provider from "../../Provider.ts"; |
| 5 | import { Resource } from "../../Resource.ts"; |
| 6 | import type { Providers } from "../Providers.ts"; |
| 7 | import { toWireDays } from "../../Util/Duration.ts"; |
| 8 | |
| 9 | export interface AccountPasswordPolicyProps extends Omit< |
| 10 | iam.UpdateAccountPasswordPolicyRequest, |
| 11 | "MaxPasswordAge" |
| 12 | > { |
| 13 | /** |
| 14 | * Maximum password validity duration, e.g. `"90 days"` or |
| 15 | * `Duration.days(90)`. Sent to IAM as whole days (a bare number is |
| 16 | * milliseconds). |
| 17 | */ |
| 18 | MaxPasswordAge?: Duration.Input; |
| 19 | } |
| 20 | |
| 21 | export interface AccountPasswordPolicy extends Resource< |
| 22 | "AWS.IAM.AccountPasswordPolicy", |
| 23 | AccountPasswordPolicyProps, |
| 24 | iam.PasswordPolicy, |
| 25 | never, |
| 26 | Providers |
| 27 | > {} |
| 28 | |
| 29 | /** |
| 30 | * The singleton IAM account password policy. |
| 31 | * |
| 32 | * `AccountPasswordPolicy` manages the account-wide password requirements that |
| 33 | * apply to IAM users with console passwords. |
| 34 | * ### Managing Password Rules |
| 35 | * **Example:** Require Strong Passwords |
| 36 | * ```typescript |
| 37 | * const policy = yield* AccountPasswordPolicy("PasswordPolicy", { |
| 38 | * MinimumPasswordLength: 16, |
| 39 | * RequireSymbols: true, |
| 40 | * RequireNumbers: true, |
| 41 | * RequireUppercaseCharacters: true, |
| 42 | * RequireLowercaseCharacters: true, |
| 43 | * AllowUsersToChangePassword: true, |
| 44 | * }); |
| 45 | * ``` |
| 46 | * |
| 47 | * @resource |
| 48 | */ |
| 49 | export const AccountPasswordPolicy = Resource<AccountPasswordPolicy>( |
| 50 | "AWS.IAM.AccountPasswordPolicy", |
| 51 | ); |
| 52 | |
| 53 | export const AccountPasswordPolicyProvider = () => |
| 54 | Provider.succeed(AccountPasswordPolicy, { |
| 55 | read: Effect.fn(function* () { |
| 56 | const response = yield* iam |
| 57 | .getAccountPasswordPolicy({}) |
| 58 | .pipe( |
| 59 | Effect.catchTag("NoSuchEntityException", () => |
| 60 | Effect.succeed(undefined), |
| 61 | ), |
| 62 | ); |
| 63 | return response?.PasswordPolicy; |
| 64 | }), |
| 65 | // Account-level singleton: IAM exposes no enumeration API, only |
| 66 | // `getAccountPasswordPolicy`. Return the single policy as a one-element |
| 67 | // array, or `[]` when none is set (typed `NoSuchEntityException`). |
| 68 | list: () => |
| 69 | iam.getAccountPasswordPolicy({}).pipe( |
| 70 | Effect.map((response) => [response.PasswordPolicy]), |
| 71 | Effect.catchTag("NoSuchEntityException", () => Effect.succeed([])), |
| 72 | ), |
| 73 | reconcile: Effect.fn(function* ({ news, session }) { |
| 74 | // The account password policy is a singleton driven entirely by |
| 75 | // `updateAccountPasswordPolicy`, which is itself a full upsert. |
| 76 | // Observation is implicit in the API — there is nothing meaningful |
| 77 | // to diff because the request payload *is* the desired state. |
| 78 | const request = { |
| 79 | ...news, |
| 80 | MaxPasswordAge: toWireDays(news.MaxPasswordAge), |
| 81 | }; |
| 82 | yield* iam.updateAccountPasswordPolicy(request); |
| 83 | yield* session.note("account-password-policy"); |
| 84 | return request; |
| 85 | }), |
| 86 | delete: Effect.fn(function* () { |
| 87 | yield* iam |
| 88 | .deleteAccountPasswordPolicy({}) |
| 89 | .pipe(Effect.catchTag("NoSuchEntityException", () => Effect.void)); |
| 90 | }), |
| 91 | }); |