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 d93f0217b..fd1c42f84 100644 --- a/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts +++ b/backend/src/db/migrations/20250512133213_add-external-ca-pki.ts @@ -67,6 +67,8 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("type").notNullable(); t.string("name").notNullable(); + t.string("projectId").notNullable(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.uuid("appConnectionId").nullable(); t.foreign("appConnectionId").references("id").inTable(TableName.AppConnection); t.uuid("dnsAppConnectionId").nullable(); @@ -79,6 +81,13 @@ export async function up(knex: Knex): Promise { t.binary("credentials"); t.json("configuration"); t.string("status").notNullable(); + t.unique(["projectId", "name"]); + }); + } + + if (await knex.schema.hasTable(TableName.PkiSubscriber)) { + await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { + t.string("ttl").nullable().alter(); }); } } diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 6bedf01ad..5b832bab4 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -25,8 +25,8 @@ export const CertificatesSchema = z.object({ certificateTemplateId: z.string().uuid().nullable().optional(), keyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(), - projectId: z.string(), - pkiSubscriberId: z.string().uuid().nullable().optional() + pkiSubscriberId: z.string().uuid().nullable().optional(), + projectId: z.string() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/external-certificate-authorities.ts b/backend/src/db/schemas/external-certificate-authorities.ts index 5613db355..f36c54770 100644 --- a/backend/src/db/schemas/external-certificate-authorities.ts +++ b/backend/src/db/schemas/external-certificate-authorities.ts @@ -13,6 +13,7 @@ export const ExternalCertificateAuthoritiesSchema = z.object({ id: z.string().uuid(), type: z.string(), name: z.string(), + projectId: z.string(), appConnectionId: z.string().uuid().nullable().optional(), dnsAppConnectionId: z.string().uuid().nullable().optional(), certificateAuthorityId: z.string().uuid(), diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts index 08db19806..920338327 100644 --- a/backend/src/db/schemas/pki-subscribers.ts +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -16,7 +16,7 @@ export const PkiSubscribersSchema = z.object({ name: z.string(), commonName: z.string(), subjectAlternativeNames: z.string().array(), - ttl: z.string(), + ttl: z.string().nullable().optional(), keyUsages: z.string().array(), extendedKeyUsages: z.string().array(), status: z.string() 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 dcaaf1bdc..17940ac55 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1997,7 +1997,7 @@ interface CreatePkiSubscriber { caId?: string; name: string; commonName: string; - ttl: string; + ttl?: string; subjectAlternativeNames: string[]; keyUsages: CertKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[]; 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 index d5907bf12..ddbbaee46 100644 --- 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 @@ -6,15 +6,9 @@ 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 { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; -import { - TCertificateAuthority, - TCertificateAuthorityInput -} from "@app/services/certificate-authority/certificate-authority-types"; +import { TCertificateAuthority } from "@app/services/certificate-authority/certificate-authority-types"; -export const registerCertificateAuthorityEndpoints = < - T extends TCertificateAuthority, - I extends TCertificateAuthorityInput ->({ +export const registerCertificateAuthorityEndpoints = ({ server, caType, createSchema, @@ -27,13 +21,13 @@ export const registerCertificateAuthorityEndpoints = < name: string; projectId: string; status: CaStatus; - configuration: I["configuration"]; + configuration: T["configuration"]; disableDirectIssuance: boolean; }>; updateSchema: z.ZodType<{ name?: string; status?: CaStatus; - configuration?: I["configuration"]; + configuration?: T["configuration"]; disableDirectIssuance?: boolean; }>; responseSchema: z.ZodTypeAny; diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index 93505c8b4..8de6b85e1 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -90,7 +90,8 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => ttl: z .string() .trim() - .refine((val) => ms(val) > 0, "TTL must be a positive number") + .refine((val) => !val || ms(val) > 0, "TTL must be a positive number") + .optional() .describe(PKI_SUBSCRIBERS.CREATE.ttl), subjectAlternativeNames: validateAltNameField .array() @@ -134,7 +135,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => caId: subscriber.caId ?? undefined, name: subscriber.name, commonName: subscriber.commonName, - ttl: subscriber.ttl, + ttl: subscriber.ttl ?? undefined, subjectAlternativeNames: subscriber.subjectAlternativeNames, keyUsages: subscriber.keyUsages as CertKeyUsage[], extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] @@ -219,7 +220,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => caId: subscriber.caId ?? undefined, name: subscriber.name, commonName: subscriber.commonName, - ttl: subscriber.ttl, + ttl: subscriber.ttl ?? undefined, subjectAlternativeNames: subscriber.subjectAlternativeNames, keyUsages: subscriber.keyUsages as CertKeyUsage[], extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[] 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 index b30d7c573..0e2361481 100644 --- a/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/acme/acme-certificate-authority-fns.ts @@ -203,32 +203,44 @@ export const AcmeCertificateAuthorityFns = ({ 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: dnsProviderConfig.provider, - hostedZoneId: dnsProviderConfig.hostedZoneId + try { + const ca = await certificateAuthorityDAL.create( + { + projectId, + disableDirectIssuance }, - status - }, - tx - ); + tx + ); - return certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); + await externalCertificateAuthorityDAL.create( + { + certificateAuthorityId: ca.id, + dnsAppConnectionId, + type: CaType.ACME, + name, + projectId, + configuration: { + directoryUrl, + accountEmail, + dnsProvider: dnsProviderConfig.provider, + hostedZoneId: dnsProviderConfig.hostedZoneId + }, + status + }, + tx + ); + + return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); + } catch (error) { + // @ts-expect-error We're expecting a database error + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (error?.error?.code === "23505") { + throw new BadRequestError({ + message: "Certificate authority with the same name already exists in your project" + }); + } + throw error; + } }); if (!caEntity.externalCa?.id) { diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index a08f0f360..2cb80bd0f 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -440,7 +440,7 @@ export const pkiSubscriberServiceFactory = ({ const caCertObj = new x509.X509Certificate(decryptedCaCert); const notBeforeDate = new Date(); - const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl)); + const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl ?? "0")); const caCertNotBeforeDate = new Date(caCertObj.notBefore); const caCertNotAfterDate = new Date(caCertObj.notAfter); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts index 83c45e11f..a26a36a4c 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-types.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -12,7 +12,7 @@ export type TCreatePkiSubscriberDTO = { name: string; commonName: string; status: PkiSubscriberStatus; - ttl: string; + ttl?: string; subjectAlternativeNames: string[]; keyUsages: CertKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[]; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts index 6b477f9bd..20f7bbcc0 100644 --- a/frontend/src/hooks/api/pkiSubscriber/types.ts +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -24,7 +24,7 @@ export type TCreatePkiSubscriberDTO = { caId: string; name: string; commonName: string; - ttl: string; + ttl?: string; subjectAlternativeNames: string[]; keyUsages: CertKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[]; diff --git a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx index 8171a95c9..c2c193e73 100644 --- a/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx +++ b/frontend/src/pages/cert-manager/PkiSubscribersPage/components/PkiSubscriberModal.tsx @@ -45,7 +45,7 @@ const schema = z caId: z.string().min(1, "Issuing CA is required"), commonName: z.string().trim().min(1, "Common Name is required"), subjectAlternativeNames: z.string(), - ttl: z.string().trim(), + ttl: z.string().trim().optional(), keyUsages: z.object({ [CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(), [CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(), @@ -90,6 +90,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { handleSubmit, reset, setValue, + watch, formState: { isSubmitting } } = useForm({ resolver: zodResolver(schema), @@ -107,6 +108,9 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { } }); + const selectedCaId = watch("caId"); + const selectedCa = cas?.find((ca) => ca.id === selectedCaId); + useEffect(() => { if (pkiSubscriber) { reset({ @@ -314,97 +318,101 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> - ( - - - - )} - /> - - - -
Key Usage
-
- - { - return ( - -
- {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} -
-
- ); - }} - /> - { - return ( - -
- {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { - return ( - { - onChange({ - ...value, - [optionValue]: state - }); - }} - > - {label} - - ); - })} -
-
- ); - }} - /> -
-
-
+ {selectedCa?.type !== CaType.ACME && ( + ( + + + + )} + /> + )} + {selectedCa?.type !== CaType.ACME && ( + + + +
Key Usage
+
+ + { + return ( + +
+ {KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> + { + return ( + +
+ {EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => { + return ( + { + onChange({ + ...value, + [optionValue]: state + }); + }} + > + {label} + + ); + })} +
+
+ ); + }} + /> +
+
+
+ )}