import * as iam from "@distilled.cloud/aws/iam"; import * as Effect from "effect/Effect"; import * as Stream from "effect/Stream"; import { isResolved } from "../../Diff.ts"; import * as Provider from "../../Provider.ts"; import { Resource } from "../../Resource.ts"; import type { Providers } from "../Providers.ts"; export interface SigningCertificateProps { /** * User that owns the signing certificate. */ userName: string; /** * X.509 signing certificate body. */ certificateBody: string; /** * Desired certificate status. * @default "Active" */ status?: iam.StatusType; } export interface SigningCertificate extends Resource< "AWS.IAM.SigningCertificate", SigningCertificateProps, { /** The IAM user the signing certificate belongs to. */ userName: string; /** The unique ID of the signing certificate. */ certificateId: string; /** The PEM-encoded certificate body. */ certificateBody: string; /** Whether the certificate is `Active` or `Inactive`. */ status: iam.StatusType; /** When the certificate was uploaded. */ uploadDate: Date | undefined; }, never, Providers > {} /** * An IAM signing certificate for a user. * * `SigningCertificate` uploads an X.509 signing certificate for legacy * IAM-integrated workflows that still depend on user-scoped certificates. * ### Managing User Certificates * **Example:** Upload a Signing Certificate * ```typescript * const user = yield* User("Signer", { * userName: "build-signer", * }); * * const certificate = yield* SigningCertificate("SigningCertificate", { * userName: user.userName, * certificateBody: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", * }); * ``` * * @resource */ export const SigningCertificate = Resource( "AWS.IAM.SigningCertificate", ); export const SigningCertificateProvider = () => Provider.succeed(SigningCertificate, { stables: ["certificateId"], // IAM is a global service. `listSigningCertificates` requires a `UserName`, // so we enumerate every IAM user first (paginated) and then list each // user's signing certificates (also paginated) with bounded concurrency. // The list response carries the full certificate body, so each entry maps // directly to the `Attributes` shape `read` returns without further // hydration. list: Effect.fn(function* () { const users = yield* iam.listUsers.pages({}).pipe( Stream.runCollect, Effect.map((chunk) => Array.from(chunk).flatMap((page) => page.Users ?? []), ), ); const perUser = yield* Effect.forEach( users, (user) => iam.listSigningCertificates.pages({ UserName: user.UserName }).pipe( Stream.runCollect, Effect.map((chunk) => Array.from(chunk) .flatMap((page) => page.Certificates ?? []) .map((cert) => ({ userName: cert.UserName, certificateId: cert.CertificateId, certificateBody: cert.CertificateBody, status: cert.Status, uploadDate: cert.UploadDate, })), ), // The user may be deleted between enumeration and per-user list. Effect.catchTag("NoSuchEntityException", () => Effect.succeed([])), ), { concurrency: 10 }, ); return perUser.flat(); }), diff: Effect.fn(function* ({ olds, news }) { if (!isResolved(news)) return; if ( olds.userName !== news.userName || olds.certificateBody !== news.certificateBody ) { return { action: "replace" } as const; } }), read: Effect.fn(function* ({ output }) { if (!output) { return undefined; } const listed = yield* iam.listSigningCertificates({ UserName: output.userName, }); const cert = listed.Certificates.find( (entry) => entry.CertificateId === output.certificateId, ); if (!cert?.CertificateId) { return undefined; } return { userName: cert.UserName, certificateId: cert.CertificateId, certificateBody: cert.CertificateBody, status: cert.Status, uploadDate: cert.UploadDate, }; }), reconcile: Effect.fn(function* ({ news, output, session }) { // Observe — certificate ids are AWS-generated; we can only locate // the live entry via the prior output. The certificate body is // immutable (`diff` triggers replacement on body change), so a // missing entry always means we need to upload. const observed = output ? yield* iam .listSigningCertificates({ UserName: output.userName }) .pipe( Effect.map((r) => r.Certificates.find( (entry) => entry.CertificateId === output.certificateId, ), ), Effect.catchTag("NoSuchEntityException", () => Effect.succeed(undefined), ), ) : undefined; // Ensure — upload when missing. let cert = observed; if (!cert?.CertificateId) { const uploaded = yield* iam.uploadSigningCertificate({ UserName: news.userName, CertificateBody: news.certificateBody, }); cert = uploaded.Certificate; } // Sync — apply the desired status when it differs. const desiredStatus = news.status ?? cert.Status; if (desiredStatus !== cert.Status) { yield* iam.updateSigningCertificate({ UserName: news.userName, CertificateId: cert.CertificateId, Status: desiredStatus, }); } yield* session.note(cert.CertificateId); return { userName: cert.UserName, certificateId: cert.CertificateId, certificateBody: cert.CertificateBody, status: desiredStatus, uploadDate: cert.UploadDate, }; }), delete: Effect.fn(function* ({ output }) { yield* iam .deleteSigningCertificate({ UserName: output.userName, CertificateId: output.certificateId, }) .pipe(Effect.catchTag("NoSuchEntityException", () => Effect.void)); }), });