From 6faad102e22e6ee5c59efc6627d595de991a0978 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 14 May 2025 23:10:10 +0800 Subject: [PATCH] misc: added internal CA route --- backend/src/@types/fastify.d.ts | 2 +- backend/src/@types/knex.d.ts | 8 + .../20250512133213_add-external-ca-pki.ts | 24 +- .../external-certificate-authorities.ts | 30 ++ backend/src/db/schemas/index.ts | 1 + .../ee/services/audit-log/audit-log-types.ts | 2 +- .../certificate-est-service.ts | 2 +- backend/src/server/routes/index.ts | 4 +- .../routes/v1/certificate-authority-router.ts | 10 +- .../certificate-authority-endpoints.ts | 246 ++++++++++++++ .../v1/certificate-authority-routers/index.ts | 11 + .../internal-certificate-authority-router.ts | 18 ++ backend/src/server/routes/v1/index.ts | 11 + .../src/server/routes/v2/project-router.ts | 2 +- .../certificate-authority-dal.ts | 13 + .../certificate-authority-enums.ts | 19 ++ .../certificate-authority-fns.ts | 2 +- .../certificate-authority-queue.ts | 2 +- .../certificate-authority-schemas.ts | 29 ++ .../certificate-authority-service.ts | 299 +++++++++++++++++- .../certificate-authority-types.ts | 190 +---------- .../internal-certificate-authority-dal.ts | 0 .../internal-certificate-authority-schemas.ts | 58 ++++ .../internal-certificate-authority-service.ts | 133 ++++---- .../internal-certificate-authority-types.ts | 209 ++++++++++++ .../pki-subscriber/pki-subscriber-service.ts | 2 +- 26 files changed, 1063 insertions(+), 264 deletions(-) create mode 100644 backend/src/db/schemas/external-certificate-authorities.ts create mode 100644 backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts create mode 100644 backend/src/server/routes/v1/certificate-authority-routers/index.ts create mode 100644 backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts create mode 100644 backend/src/services/certificate-authority/certificate-authority-enums.ts create mode 100644 backend/src/services/certificate-authority/certificate-authority-schemas.ts rename backend/src/services/certificate-authority/{ => internal}/internal-certificate-authority-dal.ts (100%) create mode 100644 backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts rename backend/src/services/certificate-authority/{ => internal}/internal-certificate-authority-service.ts (95%) create mode 100644 backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 2647f091c..ade4625c3 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -53,7 +53,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; -import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal-certificate-authority-service"; +import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 9fbef9558..1c4ab4f6f 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -68,6 +68,9 @@ import { TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate, + TExternalCertificateAuthorities, + TExternalCertificateAuthoritiesInsert, + TExternalCertificateAuthoritiesUpdate, TExternalGroupOrgRoleMappings, TExternalGroupOrgRoleMappingsInsert, TExternalGroupOrgRoleMappingsUpdate, @@ -543,6 +546,11 @@ declare module "knex/types/tables" { TInternalCertificateAuthoritiesInsert, TInternalCertificateAuthoritiesUpdate >; + [TableName.ExternalCertificateAuthority]: KnexOriginal.CompositeTableType< + TExternalCertificateAuthorities, + TExternalCertificateAuthoritiesInsert, + TExternalCertificateAuthoritiesUpdate + >; [TableName.Certificate]: KnexOriginal.CompositeTableType; [TableName.CertificateTemplate]: KnexOriginal.CompositeTableType< TCertificateTemplates, 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 89b0587c2..00e63e41d 100644 --- a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts +++ b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts @@ -12,7 +12,12 @@ export async function up(knex: Knex): Promise { t.uuid("certificateAuthorityId").nullable(); }); - await knex(TableName.InternalCertificateAuthority).insert(knex(TableName.CertificateAuthority).select("*")); + const caRows = await knex(TableName.CertificateAuthority).select("*"); + if (caRows.length > 0) { + // @ts-expect-error intentional: migration + await knex(TableName.InternalCertificateAuthority).insert(caRows); + } + await knex(TableName.InternalCertificateAuthority).update("certificateAuthorityId", knex.ref("id")); await knex.schema.alterTable(TableName.InternalCertificateAuthority, (t) => { @@ -59,7 +64,20 @@ export async function up(knex: Knex): Promise { if (!hasExternalCATable) { await knex.schema.createTable(TableName.ExternalCertificateAuthority, (t) => { - // + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("type").notNullable(); + t.string("name").notNullable(); + t.uuid("appConnectionId").nullable(); + t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection); + t.uuid("dnsAppConnectionId").nullable(); + t.foreign("dnsAppConnectionId").references("id").inTable(TableName.AppConnection); + t.uuid("certificateAuthorityId") + .notNullable() + .references("id") + .inTable(TableName.CertificateAuthority) + .onDelete("CASCADE"); + t.binary("credentials"); + t.json("configuration"); }); } } @@ -113,7 +131,7 @@ export async function down(knex: Knex): Promise { "notAfter" = ica."notAfter", "activeCaCertId" = ica."activeCaCertId" FROM ${TableName.InternalCertificateAuthority} ica - WHERE ca.id = ica.id + WHERE ca.id = ica."certificateAuthorityId" `); await knex.schema.alterTable(TableName.CertificateAuthority, (t) => { diff --git a/backend/src/db/schemas/external-certificate-authorities.ts b/backend/src/db/schemas/external-certificate-authorities.ts new file mode 100644 index 000000000..d20fb27c4 --- /dev/null +++ b/backend/src/db/schemas/external-certificate-authorities.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ExternalCertificateAuthoritiesSchema = z.object({ + id: z.string().uuid(), + type: z.string(), + name: z.string(), + appConnectionId: z.string().uuid().nullable().optional(), + dnsAppConnectionId: z.string().uuid().nullable().optional(), + certificateAuthorityId: z.string().uuid(), + credentials: zodBuffer.nullable().optional(), + configuration: z.unknown().nullable().optional() +}); + +export type TExternalCertificateAuthorities = z.infer; +export type TExternalCertificateAuthoritiesInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TExternalCertificateAuthoritiesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7830c6ba4..babe7cd46 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -20,6 +20,7 @@ export * from "./certificate-templates"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; +export * from "./external-certificate-authorities"; export * from "./external-group-org-role-mappings"; export * from "./external-kms"; export * from "./gateways"; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 03f11219e..5b022aae8 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -20,7 +20,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; -import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; diff --git a/backend/src/ee/services/certificate-est/certificate-est-service.ts b/backend/src/ee/services/certificate-est/certificate-est-service.ts index 59d963558..8039a977f 100644 --- a/backend/src/ee/services/certificate-est/certificate-est-service.ts +++ b/backend/src/ee/services/certificate-est/certificate-est-service.ts @@ -6,7 +6,7 @@ import { isCertChainValid } from "@app/services/certificate/certificate-fns"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns"; -import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal-certificate-authority-service"; +import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 717c3fdc1..f2726c6a3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -133,8 +133,8 @@ 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 { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal-certificate-authority-dal"; -import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal-certificate-authority-service"; +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"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 8193a2b40..77f01efae 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -10,7 +10,11 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; -import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; +import { + CaRenewalType, + CaStatus, + InternalCaType +} from "@app/services/certificate-authority/certificate-authority-enums"; import { validateAltNamesField, validateCaDateField @@ -34,7 +38,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { body: z .object({ projectSlug: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.projectSlug), - type: z.nativeEnum(CaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type), + type: z.nativeEnum(InternalCaType).describe(CERTIFICATE_AUTHORITIES.CREATE.type), friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.CREATE.friendlyName), commonName: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.commonName), organization: z.string().trim().describe(CERTIFICATE_AUTHORITIES.CREATE.organization), @@ -79,6 +83,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, + isInternal: false, actorOrgId: req.permission.orgId, ...req.body }); @@ -209,6 +214,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { caId: req.params.caId, actor: req.permission.type, actorId: req.permission.id, + isInternal: false, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body diff --git a/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts b/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts new file mode 100644 index 000000000..6f1476039 --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/certificate-authority-endpoints.ts @@ -0,0 +1,246 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { ApiDocsTags } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + TCertificateAuthority, + TCertificateAuthorityInput +} from "@app/services/certificate-authority/certificate-authority-types"; + +export const registerCertificateAuthorityEndpoints = < + T extends TCertificateAuthority, + I extends TCertificateAuthorityInput +>({ + server, + caType, + createSchema, + updateSchema, + responseSchema +}: { + caType: CaType; + server: FastifyZodProvider; + createSchema: z.ZodType<{ + name: string; + projectId: string; + configuration: I["configuration"]; + disableDirectIssuance: boolean; + }>; + updateSchema: z.ZodType<{ + name?: string; + configuration?: I["configuration"]; + disableDirectIssuance?: boolean; + }>; + responseSchema: z.ZodTypeAny; +}) => { + server.route({ + method: "GET", + url: `/`, + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + querystring: z.object({ + projectId: z.string().trim().min(1, "Project ID required") + }), + response: { + 200: z.object({ certificateAuthorities: responseSchema.array() }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { + query: { projectId } + } = req; + + const certificateAuthorities = (await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { projectId, type: caType }, + req.permission + )) as T[]; + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId, + // event: { + // type: EventType.GET_SECRET_SYNCS, + // metadata: { + // destination, + // count: secretSyncs.length, + // syncIds: secretSyncs.map((connection) => connection.id) + // } + // } + // }); + + return { certificateAuthorities }; + } + }); + + server.route({ + method: "GET", + url: "/:certificateAuthorityId", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + certificateAuthorityId: z.string().uuid() + }), + response: { + 200: z.object({ certificateAuthority: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateAuthorityId } = req.params; + + const certificateAuthority = (await server.services.certificateAuthority.findCertificateAuthorityById( + { certificateAuthorityId, type: caType }, + req.permission + )) as T; + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: secretSync.projectId, + // event: { + // type: EventType.GET_SECRET_SYNC, + // metadata: { + // syncId, + // destination + // } + // } + // }); + + return { certificateAuthority }; + } + }); + + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + body: createSchema, + response: { + 200: z.object({ certificateAuthority: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const certificateAuthority = (await server.services.certificateAuthority.createCertificateAuthority( + { ...req.body, type: caType }, + req.permission + )) as T; + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: secretSync.projectId, + // event: { + // type: EventType.CREATE_SECRET_SYNC, + // metadata: { + // syncId: secretSync.id, + // destination, + // ...req.body + // } + // } + // }); + + return { certificateAuthority }; + } + }); + + server.route({ + method: "PATCH", + url: "/:certificateAuthorityId", + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + certificateAuthorityId: z.string().uuid() + }), + body: updateSchema, + response: { + 200: z.object({ certificateAuthority: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateAuthorityId } = req.params; + + const certificateAuthority = (await server.services.certificateAuthority.updateCertificateAuthority( + { ...req.body, id: certificateAuthorityId, type: caType }, + req.permission + )) as T; + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: certificateAuthority.projectId, + // event: { + // type: EventType.UPDATE_SECRET_SYNC, + // metadata: { + // syncId, + // destination, + // ...req.body + // } + // } + // }); + + return { certificateAuthority }; + } + }); + + server.route({ + method: "DELETE", + url: `/:certificateAuthorityId`, + config: { + rateLimit: writeLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiCertificateAuthorities], + params: z.object({ + certificateAuthorityId: z.string().uuid() + }), + response: { + 200: z.object({ certificateAuthority: responseSchema }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { certificateAuthorityId } = req.params; + + const certificateAuthority = (await server.services.certificateAuthority.deleteCertificateAuthority( + { id: certificateAuthorityId, type: caType }, + req.permission + )) as T; + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // orgId: req.permission.orgId, + // event: { + // type: EventType.DELETE_SECRET_SYNC, + // metadata: { + // destination, + // syncId, + // removeSecrets + // } + // } + // }); + + return { certificateAuthority }; + } + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/index.ts b/backend/src/server/routes/v1/certificate-authority-routers/index.ts new file mode 100644 index 000000000..9708d615a --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/index.ts @@ -0,0 +1,11 @@ +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; + +import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; + +export * from "./internal-certificate-authority-router"; + +export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record Promise> = + { + [CaType.INTERNAL]: registerInternalCertificateAuthorityRouter, + [CaType.ACME]: registerInternalCertificateAuthorityRouter + }; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts new file mode 100644 index 000000000..61dc3ed57 --- /dev/null +++ b/backend/src/server/routes/v1/certificate-authority-routers/internal-certificate-authority-router.ts @@ -0,0 +1,18 @@ +import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { + CreateInternalCertificateAuthoritySchema, + InternalCertificateAuthoritySchema, + UpdateInternalCertificateAuthoritySchema +} from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas"; + +import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints"; + +export const registerInternalCertificateAuthorityRouter = async (server: FastifyZodProvider) => { + registerCertificateAuthorityEndpoints({ + caType: CaType.INTERNAL, + server, + responseSchema: InternalCertificateAuthoritySchema, + createSchema: CreateInternalCertificateAuthoritySchema, + updateSchema: UpdateInternalCertificateAuthoritySchema + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 7950b9efe..7da8444d7 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -10,6 +10,7 @@ import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; +import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { registerCertRouter } from "./certificate-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router"; @@ -102,6 +103,16 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); + await pkiRouter.register( + async (caRouter) => { + for await (const [caType, router] of Object.entries(CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP)) { + await caRouter.register(router, { prefix: `/${caType}` }); + } + }, + { + prefix: "/ca" + } + ); await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index ed578eb6f..6ca51332c 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -21,7 +21,7 @@ import { slugSchema } from "@app/server/lib/schemas"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema"; import { ProjectFilterType } from "@app/services/project/project-types"; diff --git a/backend/src/services/certificate-authority/certificate-authority-dal.ts b/backend/src/services/certificate-authority/certificate-authority-dal.ts index 27eba2390..4d47159b2 100644 --- a/backend/src/services/certificate-authority/certificate-authority-dal.ts +++ b/backend/src/services/certificate-authority/certificate-authority-dal.ts @@ -125,6 +125,11 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { `${TableName.CertificateAuthority}.id`, `${TableName.InternalCertificateAuthority}.certificateAuthorityId` ) + .leftJoin( + TableName.ExternalCertificateAuthority, + `${TableName.CertificateAuthority}.id`, + `${TableName.ExternalCertificateAuthority}.certificateAuthorityId` + ) .where(filter) .select(selectAllTableCols(TableName.CertificateAuthority)) .select( @@ -150,6 +155,14 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => { .ref("certificateAuthorityId") .withSchema(TableName.InternalCertificateAuthority) .as("internalCertificateAuthorityId") + ) + .select( + db.ref("id").withSchema(TableName.ExternalCertificateAuthority).as("externalCaId"), + db.ref("type").withSchema(TableName.ExternalCertificateAuthority).as("externalType"), + db + .ref("certificateAuthorityId") + .withSchema(TableName.ExternalCertificateAuthority) + .as("externalCertificateAuthorityId") ); if (limit) void query.limit(limit); diff --git a/backend/src/services/certificate-authority/certificate-authority-enums.ts b/backend/src/services/certificate-authority/certificate-authority-enums.ts new file mode 100644 index 000000000..8de80495e --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-enums.ts @@ -0,0 +1,19 @@ +export enum CaType { + INTERNAL = "internal", + ACME = "acme" +} + +export enum InternalCaType { + ROOT = "root", + INTERMEDIATE = "intermediate" +} + +export enum CaStatus { + ACTIVE = "active", + DISABLED = "disabled", + PENDING_CERTIFICATE = "pending-certificate" +} + +export enum CaRenewalType { + EXISTING = "existing" +} diff --git a/backend/src/services/certificate-authority/certificate-authority-fns.ts b/backend/src/services/certificate-authority/certificate-authority-fns.ts index 5ceec28fa..75434a0ca 100644 --- a/backend/src/services/certificate-authority/certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/certificate-authority-fns.ts @@ -12,7 +12,7 @@ import { TGetCaCertChainsDTO, TGetCaCredentialsDTO, TRebuildCaCrlDTO -} from "./certificate-authority-types"; +} from "./internal/internal-certificate-authority-types"; /* eslint-disable no-bitwise */ export const createSerialNumber = () => { diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index efa741692..b99a20330 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -16,7 +16,7 @@ import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificat import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; -import { TRotateCaCrlTriggerDTO } from "./certificate-authority-types"; +import { TRotateCaCrlTriggerDTO } from "./internal/internal-certificate-authority-types"; type TCertificateAuthorityQueueFactoryDep = { // TODO: Pick diff --git a/backend/src/services/certificate-authority/certificate-authority-schemas.ts b/backend/src/services/certificate-authority/certificate-authority-schemas.ts new file mode 100644 index 000000000..32ef88cde --- /dev/null +++ b/backend/src/services/certificate-authority/certificate-authority-schemas.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +import { CertificateAuthoritiesSchema } from "@app/db/schemas"; +import { slugSchema } from "@app/server/lib/schemas"; + +import { CaType } from "./certificate-authority-enums"; + +// SHEEN TODO: add description mapping using type +export const BaseCertificateAuthoritySchema = (type: CaType) => + CertificateAuthoritiesSchema.pick({ + projectId: true, + disableDirectIssuance: true, + id: true + }).extend({ + name: z.string() + }); + +export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => + z.object({ + name: slugSchema({ field: "name" }), + projectId: z.string().trim().min(1, "Project ID required"), + disableDirectIssuance: z.boolean() + }); + +export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => + z.object({ + name: slugSchema({ field: "name" }).optional(), + disableDirectIssuance: z.boolean().optional() + }); diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 23674d70f..103113a10 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -1,5 +1,300 @@ -type TCertificateAuthorityServiceFactoryDep = {}; +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType, ProjectType, TableName } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; + +import { TProjectDALFactory } from "../project/project-dal"; +import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; +import { CaType } from "./certificate-authority-enums"; +import { + TCertificateAuthority, + TCreateCertificateAuthorityDTO, + TUpdateCertificateAuthorityDTO +} from "./certificate-authority-types"; +import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal-certificate-authority-service"; + +type TCertificateAuthorityServiceFactoryDep = { + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + | "transaction" + | "create" + | "findById" + | "updateById" + | "deleteById" + | "findOne" + | "findByIdWithAssociatedCa" + | "findWithAssociatedCa" + >; + internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory; + projectDAL: Pick< + TProjectDALFactory, + "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction" | "getProjectFromSplitId" + >; + permissionService: Pick; +}; export type TCertificateAuthorityServiceFactory = ReturnType; -export const certificateAuthorityServiceFactory = ({}: TCertificateAuthorityServiceFactoryDep) => {}; +export const certificateAuthorityServiceFactory = ({ + certificateAuthorityDAL, + projectDAL, + permissionService, + internalCertificateAuthorityService +}: TCertificateAuthorityServiceFactoryDep) => { + const createCertificateAuthority = async ( + { type, projectId, configuration, disableDirectIssuance }: TCreateCertificateAuthorityDTO, + actor: OrgServiceActor + ) => { + let finalProjectId: string = projectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + + if (certManagerProjectFromSplit) { + finalProjectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: finalProjectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.CertificateAuthorities + ); + + if (type === CaType.INTERNAL) { + const ca = await internalCertificateAuthorityService.createCa({ + ...configuration, + isInternal: true, + projectId: finalProjectId, + requireTemplateForIssuance: disableDirectIssuance + }); + + if (!ca.internalCa) { + throw new BadRequestError({ + message: "Failed to create internal certificate authority" + }); + } + + return { + id: ca.id, + type, + disableDirectIssuance: ca.disableDirectIssuance, + name: ca.internalCa?.friendlyName, + projectId, + configuration: ca.internalCa + } as TCertificateAuthority; + } + }; + + const findCertificateAuthorityById = async ( + { certificateAuthorityId, type }: { certificateAuthorityId: string; type: CaType }, + actor: OrgServiceActor + ) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateAuthorityId); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with ID "${certificateAuthorityId}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + if (type === CaType.INTERNAL) { + if (!certificateAuthority.internalCa) { + throw new NotFoundError({ + message: `Could not find internal certificate authority with ID "${certificateAuthorityId}"` + }); + } + + return { + id: certificateAuthority.id, + type, + disableDirectIssuance: certificateAuthority.disableDirectIssuance, + name: certificateAuthority.internalCa.friendlyName, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.internalCa + } as TCertificateAuthority; + } + }; + + const listCertificateAuthoritiesByProjectId = async ( + { projectId, type }: { projectId: string; type: CaType }, + actor: OrgServiceActor + ) => { + let finalProjectId: string = projectId; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + + if (certManagerProjectFromSplit) { + finalProjectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: finalProjectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + const cas = await certificateAuthorityDAL.findWithAssociatedCa({ + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: finalProjectId, + ...(type === CaType.INTERNAL && { + $notNull: [`${TableName.InternalCertificateAuthority}.id` as "id"] + }), + ...(type !== CaType.INTERNAL && { + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: type + }) + }); + + if (type === CaType.INTERNAL) { + return cas + .filter((ca): ca is typeof ca & { internalCa: NonNullable } => Boolean(ca.internalCa)) + .map((ca) => ({ + id: ca.id, + type, + disableDirectIssuance: ca.disableDirectIssuance, + name: ca.internalCa.friendlyName, + projectId: ca.projectId, + configuration: ca.internalCa + })) as TCertificateAuthority[]; + } + }; + + const updateCertificateAuthority = async ( + { id, type, configuration, disableDirectIssuance }: TUpdateCertificateAuthorityDTO, + actor: OrgServiceActor + ) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with ID "${id}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.CertificateAuthorities + ); + + if (type === CaType.INTERNAL) { + if (!certificateAuthority.internalCa) { + throw new NotFoundError({ + message: `Could not find internal certificate authority with ID "${id}"` + }); + } + + const updatedCa = await internalCertificateAuthorityService.updateCaById({ + ...configuration, + isInternal: true, + requireTemplateForIssuance: disableDirectIssuance, + caId: id + }); + + if (!updatedCa.internalCa) { + throw new BadRequestError({ + message: "Failed to update internal certificate authority" + }); + } + + return { + id: updatedCa.id, + type, + disableDirectIssuance: updatedCa.disableDirectIssuance, + name: updatedCa.internalCa?.friendlyName, + projectId: updatedCa.projectId, + configuration: updatedCa.internalCa + } as TCertificateAuthority; + } + }; + + const deleteCertificateAuthority = async ({ id, type }: { id: string; type: CaType }, actor: OrgServiceActor) => { + const certificateAuthority = await certificateAuthorityDAL.findByIdWithAssociatedCa(id); + + if (!certificateAuthority) + throw new NotFoundError({ + message: `Could not find certificate authority with ID "${id}"` + }); + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + projectId: certificateAuthority.projectId, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.CertificateAuthorities + ); + + if (!certificateAuthority.internalCa && type === CaType.INTERNAL) { + throw new BadRequestError({ + message: "Certificate authority cannot be deleted due to mismatching type" + }); + } + + await certificateAuthorityDAL.deleteById(id); + + if (type === CaType.INTERNAL) { + return { + id: certificateAuthority.id, + type, + disableDirectIssuance: certificateAuthority.disableDirectIssuance, + name: certificateAuthority.internalCa?.friendlyName, + projectId: certificateAuthority.projectId, + configuration: certificateAuthority.internalCa + } as TCertificateAuthority; + } + }; + + return { + createCertificateAuthority, + findCertificateAuthorityById, + listCertificateAuthoritiesByProjectId, + updateCertificateAuthority, + deleteCertificateAuthority + }; +}; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index b5b75780c..aa425bb8f 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -1,186 +1,18 @@ -import { TProjectPermission } from "@app/lib/types"; -import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { CaType } from "./certificate-authority-enums"; +import { + TInternalCertificateAuthority, + TInternalCertificateAuthorityInput +} from "./internal/internal-certificate-authority-types"; -import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; -import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; -import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; +export type TCertificateAuthority = TInternalCertificateAuthority; -export enum CaType { - ROOT = "root", - INTERMEDIATE = "intermediate" -} +export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput; -export enum CaStatus { - ACTIVE = "active", - DISABLED = "disabled", - PENDING_CERTIFICATE = "pending-certificate" -} - -export enum CaRenewalType { - EXISTING = "existing" -} - -export type TCreateCaDTO = { - projectSlug: string; +export type TCreateCertificateAuthorityDTO = Omit & { type: CaType; - friendlyName?: string; - commonName: string; - organization: string; - ou: string; - country: string; - province: string; - locality: string; - notBefore?: string; - notAfter?: string; - maxPathLength: number; - keyAlgorithm: CertKeyAlgorithm; - requireTemplateForIssuance: boolean; -} & Omit; - -export type TGetCaDTO = { - caId: string; -} & Omit; - -export type TUpdateCaDTO = { - caId: string; - status?: CaStatus; - requireTemplateForIssuance?: boolean; -} & Omit; - -export type TDeleteCaDTO = { - caId: string; -} & Omit; - -export type TGetCaCsrDTO = { - caId: string; -} & Omit; - -export type TRenewCaCertDTO = { - caId: string; - notAfter: string; - type: CaRenewalType; -} & Omit; - -export type TGetCaCertsDTO = { - caId: string; -} & Omit; - -export type TGetCaCertDTO = { - caId: string; -} & Omit; - -export type TSignIntermediateDTO = { - caId: string; - csr: string; - notBefore?: string; - notAfter: string; - maxPathLength: number; -} & Omit; - -export type TImportCertToCaDTO = { - caId: string; - certificate: string; - certificateChain: string; -} & Omit; - -export type TIssueCertFromCaDTO = { - caId?: string; - certificateTemplateId?: string; - pkiCollectionId?: string; - friendlyName?: string; - commonName: string; - altNames: string; - ttl: string; - notBefore?: string; - notAfter?: string; - keyUsages?: CertKeyUsage[]; - extendedKeyUsages?: CertExtendedKeyUsage[]; -} & Omit; - -export type TSignCertFromCaDTO = - | { - isInternal: true; - caId?: string; - csr: string; - certificateTemplateId?: string; - pkiCollectionId?: string; - friendlyName?: string; - commonName?: string; - altNames?: string; - ttl?: string; - notBefore?: string; - notAfter?: string; - keyUsages?: CertKeyUsage[]; - extendedKeyUsages?: CertExtendedKeyUsage[]; - } - | ({ - isInternal: false; - caId?: string; - csr: string; - certificateTemplateId?: string; - pkiCollectionId?: string; - friendlyName?: string; - commonName?: string; - altNames: string; - ttl: string; - notBefore?: string; - notAfter?: string; - keyUsages?: CertKeyUsage[]; - extendedKeyUsages?: CertExtendedKeyUsage[]; - } & Omit); - -export type TGetCaCertificateTemplatesDTO = { - caId: string; -} & Omit; - -export type TDNParts = { - commonName?: string; - organization?: string; - ou?: string; - country?: string; - province?: string; - locality?: string; }; -export type TGetCaCredentialsDTO = { - caId: string; - certificateAuthorityDAL: Pick; - certificateAuthoritySecretDAL: Pick; - projectDAL: Pick; - kmsService: Pick; -}; - -export type TGetCaCertChainsDTO = { - caId: string; - certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; - projectDAL: Pick; - kmsService: Pick; -}; - -export type TGetCaCertChainDTO = { - caCertId: string; - certificateAuthorityDAL: Pick; - certificateAuthorityCertDAL: Pick; - projectDAL: Pick; - kmsService: Pick; -}; - -export type TRebuildCaCrlDTO = { - caId: string; - certificateAuthorityDAL: Pick; - certificateAuthorityCrlDAL: Pick; - certificateAuthoritySecretDAL: Pick; - projectDAL: Pick; - certificateDAL: Pick; - kmsService: Pick; -}; - -export type TRotateCaCrlTriggerDTO = { - caId: string; - rotationIntervalDays: number; +export type TUpdateCertificateAuthorityDTO = Partial> & { + type: CaType; + id: string; }; diff --git a/backend/src/services/certificate-authority/internal-certificate-authority-dal.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-dal.ts similarity index 100% rename from backend/src/services/certificate-authority/internal-certificate-authority-dal.ts rename to backend/src/services/certificate-authority/internal/internal-certificate-authority-dal.ts diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts new file mode 100644 index 000000000..760456caa --- /dev/null +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-schemas.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; + +import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types"; + +import { CaType, InternalCaType } from "../certificate-authority-enums"; +import { + BaseCertificateAuthoritySchema, + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../certificate-authority-schemas"; +import { validateCaDateField } from "../certificate-authority-validators"; + +const InternalCertificateAuthorityConfigurationSchema = z + .object({ + type: z.nativeEnum(InternalCaType), + friendlyName: z.string().optional(), + commonName: z.string().trim(), + organization: z.string().trim(), + ou: z.string().trim(), + country: z.string().trim(), + province: z.string().trim(), + locality: z.string().trim(), + // format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format + notBefore: validateCaDateField.optional(), + notAfter: validateCaDateField.optional(), + maxPathLength: z.number().min(-1), + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm) + }) + .refine( + (data) => { + // Check that at least one of the specified fields is non-empty + return [data.commonName, data.organization, data.ou, data.country, data.province, data.locality].some( + (field) => field !== "" + ); + }, + { + message: + "At least one of the fields commonName, organization, ou, country, province, or locality must be non-empty", + path: [] + } + ); + +export const InternalCertificateAuthoritySchema = BaseCertificateAuthoritySchema(CaType.INTERNAL).extend({ + type: z.literal(CaType.INTERNAL), + configuration: InternalCertificateAuthorityConfigurationSchema +}); + +export const CreateInternalCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema( + CaType.INTERNAL +).extend({ + configuration: InternalCertificateAuthorityConfigurationSchema +}); + +export const UpdateInternalCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema( + CaType.INTERNAL +).extend({ + configuration: InternalCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts similarity index 95% rename from backend/src/services/certificate-authority/internal-certificate-authority-service.ts rename to backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 60aace487..d19cdc484 100644 --- a/backend/src/services/certificate-authority/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -30,19 +30,20 @@ import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-c import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; -import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; -import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal"; +import { TCertificateAuthorityCrlDALFactory } from "../../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TCertificateSecretDALFactory } from "../../certificate/certificate-secret-dal"; import { CertExtendedKeyUsage, CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, CertStatus -} from "../certificate/certificate-types"; -import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; -import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns"; -import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; -import { TCertificateAuthorityDALFactory, TCertificateAuthorityWithAssociatedCa } from "./certificate-authority-dal"; +} from "../../certificate/certificate-types"; +import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; +import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; +import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory, TCertificateAuthorityWithAssociatedCa } from "../certificate-authority-dal"; +import { CaStatus, InternalCaType } from "../certificate-authority-enums"; import { createDistinguishedName, createSerialNumber, @@ -52,12 +53,11 @@ import { getCaCredentials, keyAlgorithmToAlgCfg, parseDistinguishedName -} from "./certificate-authority-fns"; -import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; -import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; +} from "../certificate-authority-fns"; +import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue"; +import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; +import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate-authority-dal"; import { - CaStatus, - CaType, TCreateCaDTO, TDeleteCaDTO, TGetCaCertDTO, @@ -71,8 +71,7 @@ import { TSignCertFromCaDTO, TSignIntermediateDTO, TUpdateCaDTO -} from "./certificate-authority-types"; -import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate-authority-dal"; +} from "./internal-certificate-authority-types"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -130,7 +129,6 @@ export const internalCertificateAuthorityServiceFactory = ({ permissionService }: TInternalCertificateAuthorityServiceFactoryDep) => { const createCa = async ({ - projectSlug, type, friendlyName, commonName, @@ -144,37 +142,39 @@ export const internalCertificateAuthorityServiceFactory = ({ maxPathLength, keyAlgorithm, requireTemplateForIssuance, - actorId, - actorAuthMethod, - actor, - actorOrgId + ...dto }: TCreateCaDTO) => { - const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); - if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); - let projectId = project.id; + let projectId: string; + if (!dto.isInternal) { + const project = await projectDAL.findProjectBySlug(dto.projectSlug, dto.actorOrgId); + if (!project) throw new NotFoundError({ message: `Project with slug '${dto.projectSlug}' not found` }); + projectId = project.id; - const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( - projectId, - ProjectType.CertificateManager - ); - if (certManagerProjectFromSplit) { - projectId = certManagerProjectFromSplit.id; + const certManagerProjectFromSplit = await projectDAL.getProjectFromSplitId( + projectId, + ProjectType.CertificateManager + ); + if (certManagerProjectFromSplit) { + projectId = certManagerProjectFromSplit.id; + } + + const { permission } = await permissionService.getProjectPermission({ + actor: dto.actor, + actorId: dto.actorId, + projectId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.CertificateAuthorities + ); + } else { + projectId = dto.projectId; } - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); - - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - ProjectPermissionSub.CertificateAuthorities - ); - const dn = createDistinguishedName({ commonName, organization, @@ -216,10 +216,10 @@ export const internalCertificateAuthorityServiceFactory = ({ locality, friendlyName: friendlyName || dn, commonName, - status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, + status: type === InternalCaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, dn, keyAlgorithm, - ...(type === CaType.ROOT && { + ...(type === InternalCaType.ROOT && { maxPathLength, notBefore: notBeforeDate, notAfter: notAfterDate, @@ -256,7 +256,7 @@ export const internalCertificateAuthorityServiceFactory = ({ tx ); - if (type === CaType.ROOT) { + if (type === InternalCaType.ROOT) { // note: create self-signed cert only applicable for root CA const cert = await x509.X509CertificateGenerator.createSelfSigned({ name: dn, @@ -357,31 +357,25 @@ export const internalCertificateAuthorityServiceFactory = ({ * Update CA with id [caId]. * Note: Used to enable/disable CA */ - const updateCaById = async ({ - caId, - status, - requireTemplateForIssuance, - actorId, - actorAuthMethod, - actor, - actorOrgId - }: TUpdateCaDTO) => { + const updateCaById = async ({ caId, status, requireTemplateForIssuance, ...dto }: TUpdateCaDTO) => { const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); if (!ca.internalCa) throw new NotFoundError({ message: `CA with ID '${caId}' not found` }); - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId: ca.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.CertificateManager - }); + if (!dto.isInternal) { + const { permission } = await permissionService.getProjectPermission({ + actor: dto.actor, + actorId: dto.actorId, + projectId: ca.projectId, + actorAuthMethod: dto.actorAuthMethod, + actorOrgId: dto.actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - ProjectPermissionSub.CertificateAuthorities - ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + ProjectPermissionSub.CertificateAuthorities + ); + } const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { if (status !== undefined) { @@ -451,7 +445,8 @@ export const internalCertificateAuthorityServiceFactory = ({ ProjectPermissionSub.CertificateAuthorities ); - if (ca.internalCa.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); + if (ca.internalCa.type === InternalCaType.ROOT) + throw new BadRequestError({ message: "Root CA cannot generate CSR" }); const { caPrivateKey, caPublicKey } = await getCaCredentials({ caId, @@ -554,7 +549,7 @@ export const internalCertificateAuthorityServiceFactory = ({ let certificateChain = ""; switch (ca.internalCa.type) { - case CaType.ROOT: { + case InternalCaType.ROOT: { if (new Date(notAfter) <= new Date(caCertObj.notAfter)) { throw new BadRequestError({ message: @@ -623,7 +618,7 @@ export const internalCertificateAuthorityServiceFactory = ({ certificate = cert.toString("pem"); break; } - case CaType.INTERMEDIATE: { + case InternalCaType.INTERMEDIATE: { if (!ca.internalCa.parentCaId) { // TODO: look into optimal way to support renewal of intermediate CA with external parent CA throw new BadRequestError({ 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 new file mode 100644 index 000000000..207230d2a --- /dev/null +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-types.ts @@ -0,0 +1,209 @@ +import { z } from "zod"; + +import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; +import { TProjectPermission } from "@app/lib/types"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal"; +import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; +import { CaRenewalType, CaStatus, InternalCaType } from "../certificate-authority-enums"; +import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; +import { + CreateInternalCertificateAuthoritySchema, + InternalCertificateAuthoritySchema +} from "./internal-certificate-authority-schemas"; + +export type TInternalCertificateAuthority = z.infer; + +export type TInternalCertificateAuthorityInput = z.infer; + +export type TCreateCaDTO = + | { + isInternal: true; + projectId: string; + type: InternalCaType; + friendlyName?: string; + commonName: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; + notBefore?: string; + notAfter?: string; + maxPathLength: number; + keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; + } + | ({ + isInternal: false; + projectSlug: string; + type: InternalCaType; + friendlyName?: string; + commonName: string; + organization: string; + ou: string; + country: string; + province: string; + locality: string; + notBefore?: string; + notAfter?: string; + maxPathLength: number; + keyAlgorithm: CertKeyAlgorithm; + requireTemplateForIssuance: boolean; + } & Omit); + +export type TGetCaDTO = { + caId: string; +} & Omit; + +export type TUpdateCaDTO = + | { + isInternal: true; + caId: string; + status?: CaStatus; + requireTemplateForIssuance?: boolean; + } + | ({ + isInternal: false; + caId: string; + status?: CaStatus; + requireTemplateForIssuance?: boolean; + } & Omit); + +export type TDeleteCaDTO = { + caId: string; +} & Omit; + +export type TGetCaCsrDTO = { + caId: string; +} & Omit; + +export type TRenewCaCertDTO = { + caId: string; + notAfter: string; + type: CaRenewalType; +} & Omit; + +export type TGetCaCertsDTO = { + caId: string; +} & Omit; + +export type TGetCaCertDTO = { + caId: string; +} & Omit; + +export type TSignIntermediateDTO = { + caId: string; + csr: string; + notBefore?: string; + notAfter: string; + maxPathLength: number; +} & Omit; + +export type TImportCertToCaDTO = { + caId: string; + certificate: string; + certificateChain: string; +} & Omit; + +export type TIssueCertFromCaDTO = { + caId?: string; + certificateTemplateId?: string; + pkiCollectionId?: string; + friendlyName?: string; + commonName: string; + altNames: string; + ttl: string; + notBefore?: string; + notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; +} & Omit; + +export type TSignCertFromCaDTO = + | { + isInternal: true; + caId?: string; + csr: string; + certificateTemplateId?: string; + pkiCollectionId?: string; + friendlyName?: string; + commonName?: string; + altNames?: string; + ttl?: string; + notBefore?: string; + notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + } + | ({ + isInternal: false; + caId?: string; + csr: string; + certificateTemplateId?: string; + pkiCollectionId?: string; + friendlyName?: string; + commonName?: string; + altNames: string; + ttl: string; + notBefore?: string; + notAfter?: string; + keyUsages?: CertKeyUsage[]; + extendedKeyUsages?: CertExtendedKeyUsage[]; + } & Omit); + +export type TGetCaCertificateTemplatesDTO = { + caId: string; +} & Omit; + +export type TDNParts = { + commonName?: string; + organization?: string; + ou?: string; + country?: string; + province?: string; + locality?: string; +}; + +export type TGetCaCredentialsDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthoritySecretDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + +export type TGetCaCertChainsDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + +export type TGetCaCertChainDTO = { + caCertId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCertDAL: Pick; + projectDAL: Pick; + kmsService: Pick; +}; + +export type TRebuildCaCrlDTO = { + caId: string; + certificateAuthorityDAL: Pick; + certificateAuthorityCrlDAL: Pick; + certificateAuthoritySecretDAL: Pick; + projectDAL: Pick; + certificateDAL: Pick; + kmsService: Pick; +}; + +export type TRotateCaCrlTriggerDTO = { + caId: string; + rotationIntervalDays: number; +}; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index 7df3209e4..554c7e165 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -27,6 +27,7 @@ import { } from "@app/services/certificate/certificate-types"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { CaStatus } from "@app/services/certificate-authority/certificate-authority-enums"; import { createSerialNumber, expandInternalCa, @@ -36,7 +37,6 @@ import { parseDistinguishedName } from "@app/services/certificate-authority/certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; -import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";