From c2949964b3b9492bc72242a455cf77fd18f80d89 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 15 May 2025 04:18:01 +0800 Subject: [PATCH] misc: added route for acme --- .../20250512133213_add-external-ca-pki.ts | 1 + .../external-certificate-authorities.ts | 3 +- backend/src/server/routes/index.ts | 29 ++- .../acme-certificate-authority-router.ts | 18 ++ .../v1/certificate-authority-routers/index.ts | 3 +- .../acme/acme-certificate-authority-enums.ts | 3 + .../acme/acme-certificate-authority-fns.ts | 199 ++++++++++++++++++ .../acme-certificate-authority-schemas.ts | 29 +++ .../acme/acme-certificate-authority-types.ts | 15 ++ .../certificate-authority-dal.ts | 47 ++++- .../certificate-authority-schemas.ts | 11 +- .../certificate-authority-service.ts | 109 +++++++++- .../certificate-authority-types.ts | 5 +- .../external-certificate-authority-dal.ts | 13 ++ .../internal-certificate-authority-types.ts | 7 +- .../CertificateAuthoritiesPage.tsx | 2 +- 16 files changed, 457 insertions(+), 37 deletions(-) create mode 100644 backend/src/server/routes/v1/certificate-authority-routers/acme-certificate-authority-router.ts create mode 100644 backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts create mode 100644 backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts create mode 100644 backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts create mode 100644 backend/src/services/certificate-authority/acme/acme-certificate-authority-types.ts create mode 100644 backend/src/services/certificate-authority/external-certificate-authority-dal.ts diff --git a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts index 00e63e41d..d93f0217b 100644 --- a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts +++ b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts @@ -78,6 +78,7 @@ export async function up(knex: Knex): Promise { .onDelete("CASCADE"); t.binary("credentials"); t.json("configuration"); + t.string("status").notNullable(); }); } } diff --git a/backend/src/db/schemas/external-certificate-authorities.ts b/backend/src/db/schemas/external-certificate-authorities.ts index d20fb27c4..5613db355 100644 --- a/backend/src/db/schemas/external-certificate-authorities.ts +++ b/backend/src/db/schemas/external-certificate-authorities.ts @@ -17,7 +17,8 @@ export const ExternalCertificateAuthoritiesSchema = z.object({ dnsAppConnectionId: z.string().uuid().nullable().optional(), certificateAuthorityId: z.string().uuid(), credentials: zodBuffer.nullable().optional(), - configuration: z.unknown().nullable().optional() + configuration: z.unknown().nullable().optional(), + status: z.string() }); export type TExternalCertificateAuthorities = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f2726c6a3..6a45d98d6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -133,6 +133,7 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; +import { externalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal"; import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal"; import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; @@ -820,6 +821,7 @@ export const registerRoutes = async ( const certificateAuthorityDAL = certificateAuthorityDALFactory(db); const internalCertificateAuthorityDAL = internalCertificateAuthorityDALFactory(db); + const externalCertificateAuthorityDAL = externalCertificateAuthorityDALFactory(db); const certificateAuthorityCertDAL = certificateAuthorityCertDALFactory(db); const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); @@ -904,23 +906,6 @@ export const registerRoutes = async ( groupDAL }); - const certificateAuthorityService = certificateAuthorityServiceFactory({ - certificateAuthorityDAL, - certificateAuthorityCertDAL, - certificateAuthoritySecretDAL, - certificateAuthorityCrlDAL, - certificateTemplateDAL, - certificateAuthorityQueue, - certificateDAL, - certificateBodyDAL, - certificateSecretDAL, - pkiCollectionDAL, - pkiCollectionItemDAL, - projectDAL, - kmsService, - permissionService - }); - const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({ certificateAuthorityDAL, certificateAuthorityCertDAL, @@ -1695,6 +1680,16 @@ export const registerRoutes = async ( appConnectionDAL }); + const certificateAuthorityService = certificateAuthorityServiceFactory({ + certificateAuthorityDAL, + projectDAL, + permissionService, + appConnectionDAL, + appConnectionService, + externalCertificateAuthorityDAL, + internalCertificateAuthorityService + }); + await secretRotationV2QueueServiceFactory({ secretRotationV2Service, secretRotationV2DAL, diff --git a/backend/src/server/routes/v1/certificate-authority-routers/acme-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/acme-certificate-authority-router.ts new file mode 100644 index 000000000..6e43fd2cf --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/acme-certificate-authority-router.ts @@ -0,0 +1,18 @@ +import { + AcmeCertificateAuthoritySchema, + CreateAcmeCertificateAuthoritySchema, + UpdateAcmeCertificateAuthoritySchema +} from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerAcmeCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.ACME, + server, + responseSchema: AcmeCertificateAuthoritySchema, + createSchema: CreateAcmeCertificateAuthoritySchema, + updateSchema: UpdateAcmeCertificateAuthoritySchema + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/index.ts b/backend/src/server/routes/v1/certificate-authority-routers/index.ts index 9708d615a..56a236911 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/index.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/index.ts @@ -1,5 +1,6 @@ import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router"; import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; export * from "./internal-certificate-authority-router"; @@ -7,5 +8,5 @@ export * from "./internal-certificate-authority-router"; export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record Promise> = { [CaType.INTERNAL]: registerInternalCertificateAuthorityRouter, - [CaType.ACME]: registerInternalCertificateAuthorityRouter + [CaType.ACME]: registerAcmeCertificateAuthorityRouter }; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts new file mode 100644 index 000000000..9f6fbe752 --- /dev/null +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-enums.ts @@ -0,0 +1,3 @@ +export enum AcmeDnsProvider { + Route53 = "route53" +} diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts new file mode 100644 index 000000000..ef7d859fe --- /dev/null +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -0,0 +1,199 @@ +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; + +import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; +import { CaStatus, CaType } from "../certificate-authority-enums"; +import { TCertificateAuthority } from "../certificate-authority-types"; +import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; +import { AcmeDnsProvider } from "./acme-certificate-authority-enums"; +import { + TCreateAcmeCertificateAuthorityDTO, + TUpdateAcmeCertificateAuthorityDTO +} from "./acme-certificate-authority-types"; + +type TAcmeCertificateAuthorityFnsDeps = { + appConnectionDAL: Pick; + appConnectionService: Pick; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" + >; + externalCertificateAuthorityDAL: Pick; +}; + +export const AcmeCertificateAuthorityFns = ({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL +}: TAcmeCertificateAuthorityFnsDeps) => { + const createCertificateAuthority = async ({ + name, + projectId, + configuration, + disableDirectIssuance, + actor, + status + }: { + status: CaStatus; + name: string; + projectId: string; + configuration: TCreateAcmeCertificateAuthorityDTO["configuration"]; + disableDirectIssuance: boolean; + actor: OrgServiceActor; + }) => { + const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration; + const appConnection = await appConnectionDAL.findById(dnsAppConnectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` }); + } + + if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection` + }); + } + + // validates permission to connect + await appConnectionService.connectAppConnectionById(appConnection.app as AppConnection, dnsAppConnectionId, actor); + + const caEntity = await certificateAuthorityDAL.transaction(async (tx) => { + const ca = await certificateAuthorityDAL.create( + { + projectId, + disableDirectIssuance + }, + tx + ); + + await externalCertificateAuthorityDAL.create( + { + certificateAuthorityId: ca.id, + dnsAppConnectionId, + type: CaType.ACME, + name, + configuration: { + directoryUrl, + accountEmail, + dnsProvider + }, + status + }, + tx + ); + + return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); + }); + + if (!caEntity.externalCa) { + throw new BadRequestError({ message: "Failed to create external certificate authority" }); + } + + return { + id: caEntity.id, + type: CaType.ACME, + disableDirectIssuance: caEntity.disableDirectIssuance, + name: caEntity.externalCa.name, + projectId, + status, + configuration: caEntity.externalCa.configuration + } as TCertificateAuthority; + }; + + const updateCertificateAuthority = async ({ + id, + status, + configuration, + disableDirectIssuance, + actor + }: { + id: string; + status?: CaStatus; + configuration: TUpdateAcmeCertificateAuthorityDTO["configuration"]; + disableDirectIssuance?: boolean; + actor: OrgServiceActor; + }) => { + const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { + if (configuration) { + const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProvider } = configuration; + const appConnection = await appConnectionDAL.findById(dnsAppConnectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${dnsAppConnectionId}' not found` }); + } + + if (dnsProvider === AcmeDnsProvider.Route53 && appConnection.app !== AppConnection.AWS) { + throw new BadRequestError({ + message: `App connection with ID '${dnsAppConnectionId}' is not an AWS connection` + }); + } + + // validates permission to connect + await appConnectionService.connectAppConnectionById( + appConnection.app as AppConnection, + dnsAppConnectionId, + actor + ); + + await externalCertificateAuthorityDAL.update( + { + certificateAuthorityId: id, + type: CaType.ACME + }, + { + configuration: { directoryUrl, accountEmail, dnsProvider, dnsAppConnectionId } + }, + tx + ); + } + + if (status) { + await externalCertificateAuthorityDAL.update( + { + certificateAuthorityId: id, + type: CaType.ACME + }, + { + status + }, + tx + ); + } + + if (disableDirectIssuance !== undefined) { + await certificateAuthorityDAL.updateById( + id, + { + disableDirectIssuance + }, + tx + ); + } + + return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx); + }); + + if (!updatedCa.externalCa) { + throw new BadRequestError({ message: "Failed to update external certificate authority" }); + } + + return { + id: updatedCa.id, + type: CaType.ACME, + disableDirectIssuance: updatedCa.disableDirectIssuance, + name: updatedCa.externalCa.name, + projectId: updatedCa.projectId, + status: updatedCa.externalCa.status, + configuration: updatedCa.externalCa.configuration + }; + }; + + return { + createCertificateAuthority, + updateCertificateAuthority + }; +}; diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts new file mode 100644 index 000000000..755346600 --- /dev/null +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-schemas.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +import { CaType } from "../certificate-authority-enums"; +import { + BaseCertificateAuthoritySchema, + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../certificate-authority-schemas"; +import { AcmeDnsProvider } from "./acme-certificate-authority-enums"; + +export const AcmeCertificateAuthorityConfigurationSchema = z.object({ + dnsAppConnectionId: z.string().trim(), + dnsProvider: z.nativeEnum(AcmeDnsProvider), + directoryUrl: z.string().trim(), + accountEmail: z.string().trim() +}); + +export const AcmeCertificateAuthoritySchema = BaseCertificateAuthoritySchema(CaType.ACME).extend({ + type: z.literal(CaType.ACME), + configuration: AcmeCertificateAuthorityConfigurationSchema +}); + +export const CreateAcmeCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(CaType.ACME).extend({ + configuration: AcmeCertificateAuthorityConfigurationSchema +}); + +export const UpdateAcmeCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(CaType.ACME).extend({ + configuration: AcmeCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/acme/acme-certificate-authority-types.ts b/backend/src/services/certificate-authority/acme/acme-certificate-authority-types.ts new file mode 100644 index 000000000..dc6c45971 --- /dev/null +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-types.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +import { + AcmeCertificateAuthoritySchema, + CreateAcmeCertificateAuthoritySchema, + UpdateAcmeCertificateAuthoritySchema +} from "./acme-certificate-authority-schemas"; + +export type TAcmeCertificateAuthority = z.infer; + +export type TAcmeCertificateAuthorityInput = z.infer; + +export type TCreateAcmeCertificateAuthorityDTO = z.infer; + +export type TUpdateAcmeCertificateAuthorityDTO = z.infer; diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 4d47159b2..9a1f32d36 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -21,6 +21,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { `${TableName.CertificateAuthority}.id`, `${TableName.InternalCertificateAuthority}.certificateAuthorityId` ) + .leftJoin( + TableName.ExternalCertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.ExternalCertificateAuthority}.certificateAuthorityId` + ) .where(`${TableName.CertificateAuthority}.id`, caId) .select(selectAllTableCols(TableName.CertificateAuthority)) .select( @@ -47,6 +52,18 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { .withSchema(TableName.InternalCertificateAuthority) .as("internalCertificateAuthorityId") ) + .select( + db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"), + db.ref("name").withSchema(TableName.ExternalCertificateAuthority).as("externalName"), + db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"), + db.ref("status").withSchema(TableName.ExternalCertificateAuthority).as("externalStatus"), + db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"), + db + .ref("dnsAppConnectionId") + .withSchema(TableName.ExternalCertificateAuthority) + .as("externalDnsAppConnectionId"), + db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") + ) .first(); const data = { @@ -73,6 +90,17 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { activeCaCertId: result.internalActiveCaCertId, certificateAuthorityId: result.internalCertificateAuthorityId } + : undefined, + externalCa: result + ? { + id: result.externalCaId, + name: result.externalName, + type: result.externalType, + status: result.externalStatus, + configuration: result.externalConfiguration, + dnsAppConnectionId: result.externalDnsAppConnectionId, + appConnectionId: result.externalAppConnectionId + } : undefined }; @@ -158,11 +186,15 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { ) .select( db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"), + db.ref("name").withSchema(TableName.ExternalCertificateAuthority).as("externalName"), db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"), + db.ref("status").withSchema(TableName.ExternalCertificateAuthority).as("externalStatus"), + db.ref("configuration").withSchema(TableName.ExternalCertificateAuthority).as("externalConfiguration"), db - .ref("certificateAuthorityId") + .ref("dnsAppConnectionId") .withSchema(TableName.ExternalCertificateAuthority) - .as("externalCertificateAuthorityId") + .as("externalDnsAppConnectionId"), + db.ref("appConnectionId").withSchema(TableName.ExternalCertificateAuthority).as("externalAppConnectionId") ); if (limit) void query.limit(limit); @@ -201,6 +233,17 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { activeCaCertId: ca.internalActiveCaCertId, certificateAuthorityId: ca.internalCertificateAuthorityId } + : undefined, + externalCa: ca + ? { + id: ca.externalCaId, + name: ca.externalName, + type: ca.externalType, + status: ca.externalStatus, + configuration: ca.externalConfiguration, + dnsAppConnectionId: ca.externalDnsAppConnectionId, + appConnectionId: ca.externalAppConnectionId + } : undefined })); } catch (error) { diff --git a/backend/src/services/certificate-authority/certificate-authority-schemas.ts b/backend/src/services/certificate-authority/certificate-authority-schemas.ts index 32ef88cde..75c2a75af 100644 --- a/backend/src/services/certificate-authority/certificate-authority-schemas.ts +++ b/backend/src/services/certificate-authority/certificate-authority-schemas.ts @@ -3,7 +3,7 @@ import z from "zod"; import { CertificateAuthoritiesSchema } from "@app/db/schemas"; import { slugSchema } from "@app/server/lib/schemas"; -import { CaType } from "./certificate-authority-enums"; +import { CaStatus, CaType } from "./certificate-authority-enums"; // SHEEN TODO: add description mapping using type export const BaseCertificateAuthoritySchema = (type: CaType) => @@ -12,18 +12,21 @@ export const BaseCertificateAuthoritySchema = (type: CaType) => disableDirectIssuance: true, id: true }).extend({ - name: z.string() + name: z.string(), + status: z.nativeEnum(CaStatus) }); export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ name: slugSchema({ field: "name" }), projectId: z.string().trim().min(1, "Project ID required"), - disableDirectIssuance: z.boolean() + disableDirectIssuance: z.boolean(), + status: z.nativeEnum(CaStatus) }); export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => z.object({ name: slugSchema({ field: "name" }).optional(), - disableDirectIssuance: z.boolean().optional() + disableDirectIssuance: z.boolean().optional(), + status: z.nativeEnum(CaStatus).optional() }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 103113a10..f56e38266 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -6,7 +6,14 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; +import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service"; import { TProjectDALFactory } from "../project/project-dal"; +import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; +import { + TCreateAcmeCertificateAuthorityDTO, + TUpdateAcmeCertificateAuthorityDTO +} from "./acme/acme-certificate-authority-types"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { CaType } from "./certificate-authority-enums"; import { @@ -14,9 +21,13 @@ import { TCreateCertificateAuthorityDTO, TUpdateCertificateAuthorityDTO } from "./certificate-authority-types"; +import { TExternalCertificateAuthorityDALFactory } from "./external-certificate-authority-dal"; import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal-certificate-authority-service"; +import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types"; type TCertificateAuthorityServiceFactoryDep = { + appConnectionDAL: Pick; + appConnectionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, | "transaction" @@ -28,6 +39,7 @@ type TCertificateAuthorityServiceFactoryDep = { | "findByIdWithAssociatedCa" | "findWithAssociatedCa" >; + externalCertificateAuthorityDAL: Pick; internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory; projectDAL: Pick< TProjectDALFactory, @@ -42,10 +54,20 @@ export const certificateAuthorityServiceFactory = ({ certificateAuthorityDAL, projectDAL, permissionService, - internalCertificateAuthorityService + internalCertificateAuthorityService, + appConnectionDAL, + appConnectionService, + externalCertificateAuthorityDAL }: TCertificateAuthorityServiceFactoryDep) => { + const acmeFns = AcmeCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL + }); + const createCertificateAuthority = async ( - { type, projectId, configuration, disableDirectIssuance }: TCreateCertificateAuthorityDTO, + { type, projectId, name, disableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, actor: OrgServiceActor ) => { let finalProjectId: string = projectId; @@ -74,7 +96,7 @@ export const certificateAuthorityServiceFactory = ({ if (type === CaType.INTERNAL) { const ca = await internalCertificateAuthorityService.createCa({ - ...configuration, + ...(configuration as TCreateInternalCertificateAuthorityDTO["configuration"]), isInternal: true, projectId: finalProjectId, requireTemplateForIssuance: disableDirectIssuance @@ -92,9 +114,21 @@ export const certificateAuthorityServiceFactory = ({ disableDirectIssuance: ca.disableDirectIssuance, name: ca.internalCa?.friendlyName, projectId, + status, configuration: ca.internalCa } as TCertificateAuthority; } + + if (type === CaType.ACME) { + return acmeFns.createCertificateAuthority({ + name, + projectId: finalProjectId, + configuration: configuration as TCreateAcmeCertificateAuthorityDTO["configuration"], + disableDirectIssuance, + status, + actor + }); + } }; const findCertificateAuthorityById = async ( @@ -135,9 +169,25 @@ export const certificateAuthorityServiceFactory = ({ disableDirectIssuance: certificateAuthority.disableDirectIssuance, name: certificateAuthority.internalCa.friendlyName, projectId: certificateAuthority.projectId, - configuration: certificateAuthority.internalCa + configuration: certificateAuthority.internalCa, + status: certificateAuthority.internalCa.status } as TCertificateAuthority; } + + if (certificateAuthority.externalCa?.type !== type) { + throw new NotFoundError({ + message: `Could not find external certificate authority with ID "${certificateAuthorityId}" and type "${type}"` + }); + } + + return { + id: certificateAuthority.id, + type, + disableDirectIssuance: certificateAuthority.disableDirectIssuance, + name: certificateAuthority.externalCa.name, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.externalCa.configuration + } as TCertificateAuthority; }; const listCertificateAuthoritiesByProjectId = async ( @@ -187,13 +237,26 @@ export const certificateAuthorityServiceFactory = ({ disableDirectIssuance: ca.disableDirectIssuance, name: ca.internalCa.friendlyName, projectId: ca.projectId, - configuration: ca.internalCa + configuration: ca.internalCa, + status: ca.internalCa.status })) as TCertificateAuthority[]; } + + return cas + .filter((ca): ca is typeof ca & { externalCa: NonNullable } => Boolean(ca.externalCa)) + .map((ca) => ({ + id: ca.id, + type, + disableDirectIssuance: ca.disableDirectIssuance, + name: ca.externalCa.name, + projectId: ca.projectId, + configuration: ca.externalCa.configuration, + status: ca.externalCa.status + })) as TCertificateAuthority[]; }; const updateCertificateAuthority = async ( - { id, type, configuration, disableDirectIssuance }: TUpdateCertificateAuthorityDTO, + { id, type, configuration, disableDirectIssuance, status }: TUpdateCertificateAuthorityDTO, actor: OrgServiceActor ) => { const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); @@ -243,9 +306,22 @@ export const certificateAuthorityServiceFactory = ({ disableDirectIssuance: updatedCa.disableDirectIssuance, name: updatedCa.internalCa?.friendlyName, projectId: updatedCa.projectId, - configuration: updatedCa.internalCa + configuration: updatedCa.internalCa, + status: updatedCa.internalCa?.status } as TCertificateAuthority; } + + if (type === CaType.ACME) { + return acmeFns.updateCertificateAuthority({ + id, + configuration: configuration as TUpdateAcmeCertificateAuthorityDTO["configuration"], + disableDirectIssuance, + actor, + status + }); + } + + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; const deleteCertificateAuthority = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => { @@ -276,6 +352,12 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (certificateAuthority.externalCa && certificateAuthority.externalCa.type !== type) { + throw new BadRequestError({ + message: "Certificate authority cannot be deleted due to mismatching type" + }); + } + await certificateAuthorityDAL.deleteById(id); if (type === CaType.INTERNAL) { @@ -285,9 +367,20 @@ export const certificateAuthorityServiceFactory = ({ disableDirectIssuance: certificateAuthority.disableDirectIssuance, name: certificateAuthority.internalCa?.friendlyName, projectId: certificateAuthority.projectId, - configuration: certificateAuthority.internalCa + configuration: certificateAuthority.internalCa, + status: certificateAuthority.internalCa?.status } as TCertificateAuthority; } + + return { + id: certificateAuthority.id, + type, + disableDirectIssuance: certificateAuthority.disableDirectIssuance, + name: certificateAuthority.externalCa?.name, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.externalCa?.configuration, + status: certificateAuthority.externalCa?.status + } as TCertificateAuthority; }; return { diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index aa425bb8f..68e22aef7 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -1,12 +1,13 @@ +import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types"; import { CaType } from "./certificate-authority-enums"; import { TInternalCertificateAuthority, TInternalCertificateAuthorityInput } from "./internal/internal-certificate-authority-types"; -export type TCertificateAuthority = TInternalCertificateAuthority; +export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority; -export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput; +export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput; export type TCreateCertificateAuthorityDTO = Omit & { type: CaType; diff --git a/backend/src/services/certificate-authority/external-certificate-authority-dal.ts b/backend/src/services/certificate-authority/external-certificate-authority-dal.ts new file mode 100644 index 000000000..a27fcbc98 --- /dev/null +++ b/backend/src/services/certificate-authority/external-certificate-authority-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TExternalCertificateAuthorityDALFactory = ReturnType; + +export const externalCertificateAuthorityDALFactory = (db: TDbClient) => { + const caOrm = ormify(db, TableName.ExternalCertificateAuthority); + + return { + ...caOrm + }; +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts index 207230d2a..f74c83406 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -13,13 +13,18 @@ import { CaRenewalType, CaStatus, InternalCaType } from "../certificate-authorit import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { CreateInternalCertificateAuthoritySchema, - InternalCertificateAuthoritySchema + InternalCertificateAuthoritySchema, + UpdateInternalCertificateAuthoritySchema } from "./internal-certificate-authority-schemas"; export type TInternalCertificateAuthority = z.infer; export type TInternalCertificateAuthorityInput = z.infer; +export type TCreateInternalCertificateAuthorityDTO = z.infer; + +export type TUpdateInternalCertificateAuthorityDTO = z.infer; + export type TCreateCaDTO = | { isInternal: true; diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx index 0efe2dbdc..21ba295a8 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/CertificateAuthoritiesPage.tsx @@ -17,7 +17,7 @@ export const CertificateAuthoritiesPage = () => {