import * as core from 'aws-cdk-lib'; import { aws_apigatewayv2 as apigwv2, aws_cloudfront as cloudfront, aws_dynamodb as dynamodb, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3 } from 'aws-cdk-lib'; import * as constructs from 'constructs'; /** * An atomic permission in the consumer's catalogue: a stable id and a * human-readable name. No consumer domain term appears here — the id is opaque * to the construct. */ export interface PermissionCatalogueEntry { readonly id: string; readonly name: string; } /** The injected permission catalogue (R8.1). */ export interface PermissionCatalogue { readonly permissions: PermissionCatalogueEntry[]; } /** * An injected predefined role: a named subset of the catalogue. Predefined * roles are immutable through this surface (R11.3) and occupy the predefined * 16-bit mask (R13.1). */ export interface PredefinedRole { readonly id: string; readonly name: string; readonly permissionIds: string[]; /** Fixed bit 0-15 in the predefined mask for this role's scope. */ readonly bit: number; } /** Where the live scope list comes from (R8.4). */ export declare enum ScopeSourceType { /** An SSM StringParameter holding a JSON list of scope ids. */ SSM_PARAMETER = "SSM_PARAMETER", /** A DynamoDB table the consumer owns. */ DYNAMODB_TABLE = "DYNAMODB_TABLE", /** A static list baked at synth. */ STATIC = "STATIC" } /** The source of the scope dimension's live values. */ export interface ScopeSource { readonly type: ScopeSourceType; /** SSM parameter name (SSM_PARAMETER), table name (DYNAMODB_TABLE), or unused. */ readonly location?: string; /** Static scope ids (STATIC only). */ readonly staticScopeIds?: string[]; } /** * The injected scope dimension — what a "scope" is and where its values come * from (R8.4). Domain meaning enters ONLY through {@link label}; the construct * never names the consumer's concept (no `brand`, no `subject`). */ export interface ScopeDimension { /** Generic human label, e.g. 'scope' — NEVER a consumer domain term. */ readonly label: string; readonly source: ScopeSource; /** Permission ids that are estate-level (no scope) rather than scope-level. */ readonly estateLevelPermissionIds: string[]; } /** The management permission required for each admin operation (R8.5, R10.1). */ export interface ManagementPermissionMap { readonly createRole: string; readonly deleteRole: string; readonly listRoles: string; readonly createAssignment: string; readonly deleteAssignment: string; readonly listAssignments: string; } /** How the management API authenticates the caller. */ export interface CallerIdentityConfig { /** * HTTPS URL of the consumer's published JWKS — the RS256 public keys the * caller's `web_identity` token is verified against. The two-token model: the * HMAC session cookie is validated ONLY at the CloudFront edge; behind the * edge the caller is identified by the asymmetric `web_identity` token alone, * so NO HMAC secret is read here. */ readonly jwksUri: string; /** * Expected token issuer (`iss`). When set, the API rejects a token whose * issuer differs. ALSO supplies the OIDC provider host for the management * role's trust-policy condition key (`:aud`/`:sub`), so it is REQUIRED * whenever {@link audience} or {@link trustedSubject} is set — a condition key * is a map key and must be a concrete synth-time string (the provider ARN, * which may be a token, cannot be split to derive it). @default no issuer check */ readonly issuer?: string; /** * Expected token audience (`aud`). When set, the API rejects a token whose * audience differs. @default no audience check */ readonly audience?: string; /** Cookie name carrying the `web_identity` token. @default '__Host-web_identity' */ readonly sessionCookieName?: string; /** * The IAM OIDC provider ARN registered for the consumer's token issuer * (horton's JWKS issuer, NOT the IdP). The management role's trust policy pins * this provider so the Lambda can exchange the caller's token for the role via * `sts:AssumeRoleWithWebIdentity`. */ readonly webIdentityOidcProviderArn: string; /** * The `sub` the management role's trust policy pins (the token subject allowed * to assume it). @default no sub condition (any token from the pinned provider * with the right audience may assume — the management-permission authZ in the * Lambda is the real gate) */ readonly trustedSubject?: string; } /** * Props for {@link PermissionsAdmin}. Every domain-specific input is injected * here (R7, R8); no prop, resource name, or type names a consumer domain term. */ export interface PermissionsAdminProps { /** Canonical domain, for default resource names (as CognitoSessionBackend). */ readonly domainName: string; /** * The consumer's existing CloudFront distribution. * * OPTIONAL. When supplied, the construct mounts two behaviours on it — * `//api/*` -> the HTTP API, and `//*` -> the SPA bucket — * i.e. the single-stack convenience path, where the distribution lives in the * SAME stack as this construct. * * OMIT it when the distribution lives in a DIFFERENT stack or region from the * management API (e.g. CloudFront's us-east-1 cert stack vs a regional API): * the construct then builds only the regional half (Lambda, HTTP API, role, * table access) and exposes {@link PermissionsAdmin.apiDomainName}. The * consumer mounts the `//api/*` behaviour itself in the * distribution's own stack — typically by publishing `apiDomainName` to the * distribution's region via a named SSM parameter and reading it region- * locally as an `HttpOrigin` (no cross-region references). */ readonly distribution?: cloudfront.IDistribution; /** * The SPA asset bucket. * * REQUIRED when {@link distribution} is supplied (the construct deploys the * bundled SPA into it and serves it through that distribution). OMIT it in * standalone mode along with `distribution`: the consumer owns SPA hosting in * the distribution's own stack. Supplying one without the other is rejected. */ readonly assetBucket?: s3.IBucket; /** Path prefix the admin SPA + API mount under. @default 'admin' */ readonly pathPrefix?: string; /** Injected authority model — all domain meaning enters here (R7, R8). */ readonly catalogue: PermissionCatalogue; readonly predefinedRoles: PredefinedRole[]; /** Permission ids that can never be granted through this surface (R8.2, R11.2). */ readonly rightsActLockout: string[]; readonly scopeDimension: ScopeDimension; readonly managementPermissions: ManagementPermissionMap; /** Injected store — the construct reads/writes only through these refs (R8.3). */ readonly roleTable: dynamodb.ITable; readonly assignmentTable: dynamodb.ITable; /** * @deprecated #195 retired the custom-role bit registry: roles are now plain * permission-id sets and the handler no longer allocates/retires bits. This * prop is accepted for backward compatibility but is UNUSED — the construct * no longer grants access to it or passes it to the handler. Consumers should * stop passing it and remove the table. It will be deleted in a future major. */ readonly bitRegistryTable?: dynamodb.ITable; /** * Name of the assignment table's GSI that answers "is this role in use?" — * partition key `roleId`. Used by archive-not-delete (R11.4). @default 'roleId-index' */ readonly assignmentRoleIndexName?: string; /** How the API authenticates the caller. */ readonly callerIdentity: CallerIdentityConfig; /** * @deprecated #195 (#66) retired the per-request authority-resolver invoke: * the handler no longer decides authority (the estate role's IAM policy is the * gate) and reads the caller's permissions for reflection from the token's * `ui_permissions` claim. This prop is accepted for backward compatibility but * is UNUSED — the construct no longer sets the resolver env var or grants * invoke. Consumers should stop passing it. It will be deleted in a future * major. */ readonly callerPermissionsResolver?: lambda.IFunction; /** Lambda log retention. @default RetentionDays.ONE_MONTH */ readonly logRetention?: logs.RetentionDays; readonly removalPolicy?: core.RemovalPolicy; } /** * `PermissionsAdmin` — the write side of a reusable `(role, scope, assignment)` * authority model: a bundled admin SPA plus a regional management API. * * TWO MOUNTING MODES: * - SINGLE-STACK (supply `distribution` + `assetBucket`): the construct mounts * `//api/*` -> the HTTP API and `//*` -> the SPA bucket on * the distribution, and deploys the bundled SPA. Use when the distribution * lives in the same stack. * - STANDALONE (omit both): the construct builds ONLY the regional half * (Lambda, HTTP API, estate role, table access) and exposes `apiDomainName`. * Use when the distribution is in a different stack/region (e.g. CloudFront's * us-east-1 cert stack fronting a Sydney-regional API): the consumer * publishes `apiDomainName` to the distribution's region via SSM and wires * the `//api/*` `HttpOrigin` + SPA hosting there (no cross-region * references). This keeps the estate role's trust policy pinned to a LOCAL * OIDC provider rather than a cross-region one. * * The construct is DOMAIN-NEUTRAL (R7): it names no consumer concept. The * consumer injects the permission catalogue, predefined roles, scope dimension, * management-permission gating, and the DynamoDB store refs; the construct * carries the authority model without being forked. * * The regional management Lambda is the SOLE enforcement point (R9.3, R10, R11): * authN (session-cookie HMAC), authZ (per-op management permission), and the * server-side invariants (no-escalation, rights-act lockout, immutable * predefined roles, archive-not-delete), plus the custom-role bit registry * (R13) and an actor-stamped audit trail (R12). The SPA enforces nothing; it * reflects the caller's own permissions and hides what they lack (R9.2). */ export declare class PermissionsAdmin extends constructs.Construct { /** The regional management Lambda (sole enforcement point). */ readonly managementFunction: lambda.Function; /** * The estate management role the Lambda assumes per request (via the caller's * web_identity token). Holds the only grants on the three authority tables; * the Lambda's own role has none. */ readonly managementRole: core.aws_iam.Role; /** The regional HTTP API fronting the management Lambda. */ readonly httpApi: apigwv2.HttpApi; /** * The HTTP API's bare host (no scheme, no path), e.g. * `abc123.execute-api.ap-southeast-2.amazonaws.com`. In standalone mode * (no {@link PermissionsAdminProps.distribution}) the consumer publishes this * to the distribution's region via SSM and wires it as the `//api/*` * `HttpOrigin` there. Also set in single-stack mode, for symmetry. */ readonly apiDomainName: string; /** The path prefix the SPA + API mount under. */ readonly pathPrefix: string; constructor(scope: constructs.Construct, id: string, props: PermissionsAdminProps); }