diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 318c6d67b..ef731f0c2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -17,9 +17,9 @@ import { TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate, TAccessApprovalRequestsUpdate, - TApiEnrollmentConfigs, - TApiEnrollmentConfigsInsert, - TApiEnrollmentConfigsUpdate, + TPkiApiEnrollmentConfigs, + TPkiApiEnrollmentConfigsInsert, + TPkiApiEnrollmentConfigsUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -80,9 +80,9 @@ import { TDynamicSecrets, TDynamicSecretsInsert, TDynamicSecretsUpdate, - TEstEnrollmentConfigs, - TEstEnrollmentConfigsInsert, - TEstEnrollmentConfigsUpdate, + TPkiEstEnrollmentConfigs, + TPkiEstEnrollmentConfigsInsert, + TPkiEstEnrollmentConfigsUpdate, TExternalCertificateAuthorities, TExternalCertificateAuthoritiesInsert, TExternalCertificateAuthoritiesUpdate, @@ -678,15 +678,15 @@ declare module "knex/types/tables" { TCertificateProfilesInsert, TCertificateProfilesUpdate >; - [TableName.EstEnrollmentConfig]: KnexOriginal.CompositeTableType< - TEstEnrollmentConfigs, - TEstEnrollmentConfigsInsert, - TEstEnrollmentConfigsUpdate + [TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType< + TPkiEstEnrollmentConfigs, + TPkiEstEnrollmentConfigsInsert, + TPkiEstEnrollmentConfigsUpdate >; - [TableName.ApiEnrollmentConfig]: KnexOriginal.CompositeTableType< - TApiEnrollmentConfigs, - TApiEnrollmentConfigsInsert, - TApiEnrollmentConfigsUpdate + [TableName.PkiApiEnrollmentConfig]: KnexOriginal.CompositeTableType< + TPkiApiEnrollmentConfigs, + TPkiApiEnrollmentConfigsInsert, + TPkiApiEnrollmentConfigsUpdate >; [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< TCertificateTemplateEstConfigs, diff --git a/backend/src/db/migrations/20251007133321_pki-v3-tables.ts b/backend/src/db/migrations/20251007133321_pki-v3-tables.ts index d8943405c..90b727f68 100644 --- a/backend/src/db/migrations/20251007133321_pki-v3-tables.ts +++ b/backend/src/db/migrations/20251007133321_pki-v3-tables.ts @@ -10,7 +10,7 @@ export async function up(knex: Knex): Promise { t.string("projectId").notNullable(); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); - t.string("name", 64).notNullable(); + t.string("slug").notNullable(); t.string("description"); t.jsonb("attributes"); @@ -22,13 +22,15 @@ export async function up(knex: Knex): Promise { t.jsonb("keyAlgorithm"); t.timestamps(true, true, true); + + t.unique(["slug", "projectId"]); }); await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2); } - if (!(await knex.schema.hasTable(TableName.EstEnrollmentConfig))) { - await knex.schema.createTable(TableName.EstEnrollmentConfig, (t) => { + if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) { + await knex.schema.createTable(TableName.PkiEstEnrollmentConfig, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.boolean("disableBootstrapCaValidation").defaultTo(false); @@ -38,11 +40,11 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); }); - await createOnUpdateTrigger(knex, TableName.EstEnrollmentConfig); + await createOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig); } - if (!(await knex.schema.hasTable(TableName.ApiEnrollmentConfig))) { - await knex.schema.createTable(TableName.ApiEnrollmentConfig, (t) => { + if (!(await knex.schema.hasTable(TableName.PkiApiEnrollmentConfig))) { + await knex.schema.createTable(TableName.PkiApiEnrollmentConfig, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.boolean("autoRenew").defaultTo(false); @@ -51,7 +53,7 @@ export async function up(knex: Knex): Promise { t.timestamps(true, true, true); }); - await createOnUpdateTrigger(knex, TableName.ApiEnrollmentConfig); + await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); } if (!(await knex.schema.hasTable(TableName.CertificateProfile))) { @@ -61,25 +63,24 @@ export async function up(knex: Knex): Promise { t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.uuid("caId").notNullable(); - t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority); t.uuid("certificateTemplateId").notNullable(); - t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2).onDelete("CASCADE"); + t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2); - t.string("name", 64).notNullable(); t.string("slug").notNullable(); t.string("description"); t.string("enrollmentType").notNullable().checkIn(["api", "est"]); t.uuid("estConfigId"); - t.foreign("estConfigId").references("id").inTable(TableName.EstEnrollmentConfig).onDelete("SET NULL"); + t.foreign("estConfigId").references("id").inTable(TableName.PkiEstEnrollmentConfig).onDelete("SET NULL"); t.uuid("apiConfigId"); - t.foreign("apiConfigId").references("id").inTable(TableName.ApiEnrollmentConfig).onDelete("SET NULL"); + t.foreign("apiConfigId").references("id").inTable(TableName.PkiApiEnrollmentConfig).onDelete("SET NULL"); t.timestamps(true, true, true); - t.unique(["slug", "projectId"], { indexName: "certificate_profiles_slug_project_id_unique" }); + t.unique(["slug", "projectId"]); }); await createOnUpdateTrigger(knex, TableName.CertificateProfile); @@ -89,7 +90,7 @@ export async function up(knex: Knex): Promise { await knex.schema.alterTable(TableName.Certificate, (t) => { t.uuid("profileId"); t.foreign("profileId").references("id").inTable(TableName.CertificateProfile).onDelete("SET NULL"); - t.index("profileId", "idx_certificates_profile_id"); + t.index("profileId"); }); } } @@ -98,7 +99,7 @@ export async function down(knex: Knex): Promise { if (await knex.schema.hasColumn(TableName.Certificate, "profileId")) { await knex.schema.alterTable(TableName.Certificate, (t) => { t.dropForeign(["profileId"]); - t.dropIndex("profileId", "idx_certificates_profile_id"); + t.dropIndex("profileId"); t.dropColumn("profileId"); }); } @@ -106,11 +107,11 @@ export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.CertificateProfile); await dropOnUpdateTrigger(knex, TableName.CertificateProfile); - await knex.schema.dropTableIfExists(TableName.ApiEnrollmentConfig); - await dropOnUpdateTrigger(knex, TableName.ApiEnrollmentConfig); + await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig); + await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); - await knex.schema.dropTableIfExists(TableName.EstEnrollmentConfig); - await dropOnUpdateTrigger(knex, TableName.EstEnrollmentConfig); + await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig); + await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig); await knex.schema.dropTableIfExists(TableName.CertificateTemplateV2); await dropOnUpdateTrigger(knex, TableName.CertificateTemplateV2); diff --git a/backend/src/db/schemas/certificate-profiles.ts b/backend/src/db/schemas/certificate-profiles.ts index 888bb1996..9e91706ed 100644 --- a/backend/src/db/schemas/certificate-profiles.ts +++ b/backend/src/db/schemas/certificate-profiles.ts @@ -12,7 +12,6 @@ export const CertificateProfilesSchema = z.object({ projectId: z.string(), caId: z.string().uuid(), certificateTemplateId: z.string().uuid(), - name: z.string(), slug: z.string(), description: z.string().nullable().optional(), enrollmentType: z.string(), diff --git a/backend/src/db/schemas/certificate-templates-v2.ts b/backend/src/db/schemas/certificate-templates-v2.ts index 0d91fb8da..68c3c3ca4 100644 --- a/backend/src/db/schemas/certificate-templates-v2.ts +++ b/backend/src/db/schemas/certificate-templates-v2.ts @@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models"; export const CertificateTemplatesV2Schema = z.object({ id: z.string().uuid(), projectId: z.string(), - name: z.string(), + slug: z.string(), description: z.string().nullable().optional(), attributes: z.unknown().nullable().optional(), keyUsages: z.unknown().nullable().optional(), diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index fec41f3e2..bdc8f9883 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -3,7 +3,6 @@ export * from "./access-approval-policies-approvers"; export * from "./access-approval-policies-bypassers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; -export * from "./api-enrollment-configs"; export * from "./api-keys"; export * from "./app-connections"; export * from "./audit-log-streams"; @@ -24,7 +23,6 @@ export * from "./certificate-templates-v2"; export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; -export * from "./est-enrollment-configs"; export * from "./external-certificate-authorities"; export * from "./external-group-org-role-mappings"; export * from "./external-kms"; @@ -92,8 +90,10 @@ export * from "./pam-folders"; export * from "./pam-resources"; export * from "./pam-sessions"; export * from "./pki-alerts"; +export * from "./pki-api-enrollment-configs"; export * from "./pki-collection-items"; export * from "./pki-collections"; +export * from "./pki-est-enrollment-configs"; export * from "./pki-subscribers"; export * from "./pki-syncs"; export * from "./project-bots"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 20baaa3c0..b3d1addd3 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -25,8 +25,8 @@ export enum TableName { CertificateTemplate = "certificate_templates", CertificateTemplateV2 = "certificate_templates_v2", CertificateProfile = "certificate_profiles", - EstEnrollmentConfig = "est_enrollment_configs", - ApiEnrollmentConfig = "api_enrollment_configs", + PkiEstEnrollmentConfig = "pki_est_enrollment_configs", + PkiApiEnrollmentConfig = "pki_api_enrollment_configs", PkiSubscriber = "pki_subscribers", PkiAlert = "pki_alerts", PkiCollection = "pki_collections", diff --git a/backend/src/db/schemas/api-enrollment-configs.ts b/backend/src/db/schemas/pki-api-enrollment-configs.ts similarity index 53% rename from backend/src/db/schemas/api-enrollment-configs.ts rename to backend/src/db/schemas/pki-api-enrollment-configs.ts index 37fdfc163..710b0dee4 100644 --- a/backend/src/db/schemas/api-enrollment-configs.ts +++ b/backend/src/db/schemas/pki-api-enrollment-configs.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { TImmutableDBKeys } from "./models"; -export const ApiEnrollmentConfigsSchema = z.object({ +export const PkiApiEnrollmentConfigsSchema = z.object({ id: z.string().uuid(), autoRenew: z.boolean().default(false).nullable().optional(), autoRenewDays: z.number().nullable().optional(), @@ -15,6 +15,8 @@ export const ApiEnrollmentConfigsSchema = z.object({ updatedAt: z.date() }); -export type TApiEnrollmentConfigs = z.infer; -export type TApiEnrollmentConfigsInsert = Omit, TImmutableDBKeys>; -export type TApiEnrollmentConfigsUpdate = Partial, TImmutableDBKeys>>; +export type TPkiApiEnrollmentConfigs = z.infer; +export type TPkiApiEnrollmentConfigsInsert = Omit, TImmutableDBKeys>; +export type TPkiApiEnrollmentConfigsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/est-enrollment-configs.ts b/backend/src/db/schemas/pki-est-enrollment-configs.ts similarity index 57% rename from backend/src/db/schemas/est-enrollment-configs.ts rename to backend/src/db/schemas/pki-est-enrollment-configs.ts index 8e60d58f4..f54eb0561 100644 --- a/backend/src/db/schemas/est-enrollment-configs.ts +++ b/backend/src/db/schemas/pki-est-enrollment-configs.ts @@ -9,7 +9,7 @@ import { zodBuffer } from "@app/lib/zod"; import { TImmutableDBKeys } from "./models"; -export const EstEnrollmentConfigsSchema = z.object({ +export const PkiEstEnrollmentConfigsSchema = z.object({ id: z.string().uuid(), disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(), hashedPassphrase: z.string(), @@ -18,6 +18,8 @@ export const EstEnrollmentConfigsSchema = z.object({ updatedAt: z.date() }); -export type TEstEnrollmentConfigs = z.infer; -export type TEstEnrollmentConfigsInsert = Omit, TImmutableDBKeys>; -export type TEstEnrollmentConfigsUpdate = Partial, TImmutableDBKeys>>; +export type TPkiEstEnrollmentConfigs = z.infer; +export type TPkiEstEnrollmentConfigsInsert = Omit, TImmutableDBKeys>; +export type TPkiEstEnrollmentConfigsUpdate = Partial< + Omit, TImmutableDBKeys> +>; 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 547967bdd..cb63cd0f7 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -352,14 +352,10 @@ export enum EventType { UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", GET_CERTIFICATE_TEMPLATE = "get-certificate-template", + LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates", CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", - CREATE_CERTIFICATE_TEMPLATE_V2 = "create-certificate-template-v2", - UPDATE_CERTIFICATE_TEMPLATE_V2 = "update-certificate-template-v2", - DELETE_CERTIFICATE_TEMPLATE_V2 = "delete-certificate-template-v2", - GET_CERTIFICATE_TEMPLATE_V2 = "get-certificate-template-v2", - LIST_CERTIFICATE_TEMPLATES_V2 = "list-certificate-templates-v2", CREATE_CERTIFICATE_PROFILE = "create-certificate-profile", UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile", DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile", @@ -2525,46 +2521,6 @@ interface LoadProjectKmsBackupEvent { metadata: Record; // no metadata yet } -interface CreateCertificateTemplate { - type: EventType.CREATE_CERTIFICATE_TEMPLATE; - metadata: { - certificateTemplateId: string; - caId: string; - pkiCollectionId?: string; - name: string; - commonName: string; - subjectAlternativeName: string; - ttl: string; - }; -} - -interface GetCertificateTemplate { - type: EventType.GET_CERTIFICATE_TEMPLATE; - metadata: { - certificateTemplateId: string; - }; -} - -interface UpdateCertificateTemplate { - type: EventType.UPDATE_CERTIFICATE_TEMPLATE; - metadata: { - certificateTemplateId: string; - caId: string; - pkiCollectionId?: string; - name: string; - commonName: string; - subjectAlternativeName: string; - ttl: string; - }; -} - -interface DeleteCertificateTemplate { - type: EventType.DELETE_CERTIFICATE_TEMPLATE; - metadata: { - certificateTemplateId: string; - }; -} - interface OrgAdminAccessProjectEvent { type: EventType.ORG_ADMIN_ACCESS_PROJECT; metadata: { @@ -2611,8 +2567,8 @@ interface GetCertificateTemplateEstConfig { }; } -interface CreateCertificateTemplateV2 { - type: EventType.CREATE_CERTIFICATE_TEMPLATE_V2; +interface CreateCertificateTemplate { + type: EventType.CREATE_CERTIFICATE_TEMPLATE; metadata: { certificateTemplateId: string; name: string; @@ -2620,30 +2576,32 @@ interface CreateCertificateTemplateV2 { }; } -interface UpdateCertificateTemplateV2 { - type: EventType.UPDATE_CERTIFICATE_TEMPLATE_V2; +interface UpdateCertificateTemplate { + type: EventType.UPDATE_CERTIFICATE_TEMPLATE; metadata: { certificateTemplateId: string; name: string; }; } -interface DeleteCertificateTemplateV2 { - type: EventType.DELETE_CERTIFICATE_TEMPLATE_V2; +interface DeleteCertificateTemplate { + type: EventType.DELETE_CERTIFICATE_TEMPLATE; metadata: { certificateTemplateId: string; + name: string; }; } -interface GetCertificateTemplateV2 { - type: EventType.GET_CERTIFICATE_TEMPLATE_V2; +interface GetCertificateTemplate { + type: EventType.GET_CERTIFICATE_TEMPLATE; metadata: { certificateTemplateId: string; + name: string; }; } -interface ListCertificateTemplatesV2 { - type: EventType.LIST_CERTIFICATE_TEMPLATES_V2; +interface ListCertificateTemplates { + type: EventType.LIST_CERTIFICATE_TEMPLATES; metadata: { projectId: string; }; @@ -2710,7 +2668,7 @@ interface OrderCertificateFromProfile { metadata: { certificateProfileId: string; orderId: string; - identifiers: string[]; + subjectAlternativeNames: string[]; }; } @@ -4167,18 +4125,14 @@ export type Event = | LoadProjectKmsBackupEvent | OrgAdminAccessProjectEvent | OrgAdminBypassSSOEvent - | CreateCertificateTemplate - | UpdateCertificateTemplate - | GetCertificateTemplate - | DeleteCertificateTemplate | CreateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig | GetCertificateTemplateEstConfig - | CreateCertificateTemplateV2 - | UpdateCertificateTemplateV2 - | DeleteCertificateTemplateV2 - | GetCertificateTemplateV2 - | ListCertificateTemplatesV2 + | CreateCertificateTemplate + | UpdateCertificateTemplate + | DeleteCertificateTemplate + | GetCertificateTemplate + | ListCertificateTemplates | CreateCertificateProfile | UpdateCertificateProfile | DeleteCertificateProfile diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 973d088f1..34876f739 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -455,6 +455,7 @@ const buildMemberPermissionRules = () => { // double check if all CRUD are needed for CA and Certificates can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); + can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates); can( [ diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index 98e8c30a6..70b1cf830 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -9,7 +9,6 @@ import { AuthMode } from "@app/services/auth/auth-type"; import { createCertificateProfileSchema, deleteCertificateProfileSchema, - getCertificateProfileByIdSchema, listCertificateProfilesSchema, updateCertificateProfileSchema } from "@app/services/certificate-profile/certificate-profile-schemas"; @@ -49,7 +48,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid type: EventType.CREATE_CERTIFICATE_PROFILE, metadata: { certificateProfileId: certificateProfile.id, - name: certificateProfile.name, + name: certificateProfile.slug, projectId: certificateProfile.projectId, enrollmentType: certificateProfile.enrollmentType } @@ -125,7 +124,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid schema: { hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], - params: getCertificateProfileByIdSchema, + params: z.object({ + id: z.string().min(1) + }), querystring: z.object({ includeMetrics: z.coerce.boolean().optional().default(false), expiringDays: z.coerce.number().min(1).max(365).optional().default(7) @@ -262,7 +263,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid schema: { hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], - params: getCertificateProfileByIdSchema, + params: z.object({ + id: z.string().min(1) + }), body: updateCertificateProfileSchema, response: { 200: z.object({ @@ -288,7 +291,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid type: EventType.UPDATE_CERTIFICATE_PROFILE, metadata: { certificateProfileId: certificateProfile.id, - name: certificateProfile.name + name: certificateProfile.slug } } }); @@ -347,7 +350,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid schema: { hide: false, tags: [ApiDocsTags.PkiCertificateProfiles], - params: getCertificateProfileByIdSchema, + params: z.object({ + id: z.string().min(1) + }), querystring: z.object({ offset: z.number().min(0).default(0), limit: z.number().min(1).max(100).default(20), diff --git a/backend/src/server/routes/v1/certificate-template-router.ts b/backend/src/server/routes/v1/certificate-template-router.ts index 17f564be4..cd615b673 100644 --- a/backend/src/server/routes/v1/certificate-template-router.ts +++ b/backend/src/server/routes/v1/certificate-template-router.ts @@ -52,7 +52,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid event: { type: EventType.GET_CERTIFICATE_TEMPLATE, metadata: { - certificateTemplateId: certificateTemplate.id + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name } } }); @@ -116,12 +117,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid type: EventType.CREATE_CERTIFICATE_TEMPLATE, metadata: { certificateTemplateId: certificateTemplate.id, - caId: certificateTemplate.caId, - pkiCollectionId: certificateTemplate.pkiCollectionId as string, name: certificateTemplate.name, - commonName: certificateTemplate.commonName, - subjectAlternativeName: certificateTemplate.subjectAlternativeName, - ttl: certificateTemplate.ttl + projectId: certificateTemplate.projectId } } }); @@ -184,12 +181,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid type: EventType.UPDATE_CERTIFICATE_TEMPLATE, metadata: { certificateTemplateId: certificateTemplate.id, - caId: certificateTemplate.caId, - pkiCollectionId: certificateTemplate.pkiCollectionId as string, - name: certificateTemplate.name, - commonName: certificateTemplate.commonName, - subjectAlternativeName: certificateTemplate.subjectAlternativeName, - ttl: certificateTemplate.ttl + name: certificateTemplate.name } } }); @@ -230,7 +222,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid event: { type: EventType.DELETE_CERTIFICATE_TEMPLATE, metadata: { - certificateTemplateId: certificateTemplate.id + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.name } } }); diff --git a/backend/src/server/routes/v2/certificate-templates-v2-router.ts b/backend/src/server/routes/v2/certificate-templates-v2-router.ts index a33a4b91c..6cf26800d 100644 --- a/backend/src/server/routes/v2/certificate-templates-v2-router.ts +++ b/backend/src/server/routes/v2/certificate-templates-v2-router.ts @@ -48,10 +48,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro ...req.auditLogInfo, projectId, event: { - type: EventType.CREATE_CERTIFICATE_TEMPLATE_V2, + type: EventType.CREATE_CERTIFICATE_TEMPLATE, metadata: { certificateTemplateId: certificateTemplate.id, - name: certificateTemplate.name, + name: certificateTemplate.slug, projectId: certificateTemplate.projectId } } @@ -92,7 +92,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro ...req.auditLogInfo, projectId: req.query.projectId, event: { - type: EventType.LIST_CERTIFICATE_TEMPLATES_V2, + type: EventType.LIST_CERTIFICATE_TEMPLATES, metadata: { projectId: req.query.projectId } @@ -133,9 +133,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro ...req.auditLogInfo, projectId: certificateTemplate.projectId, event: { - type: EventType.GET_CERTIFICATE_TEMPLATE_V2, + type: EventType.GET_CERTIFICATE_TEMPLATE, metadata: { - certificateTemplateId: certificateTemplate.id + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.slug } } }); @@ -176,10 +177,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro ...req.auditLogInfo, projectId: certificateTemplate.projectId, event: { - type: EventType.UPDATE_CERTIFICATE_TEMPLATE_V2, + type: EventType.UPDATE_CERTIFICATE_TEMPLATE, metadata: { certificateTemplateId: certificateTemplate.id, - name: certificateTemplate.name + name: certificateTemplate.slug } } }); @@ -218,9 +219,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro ...req.auditLogInfo, projectId: certificateTemplate.projectId, event: { - type: EventType.DELETE_CERTIFICATE_TEMPLATE_V2, + type: EventType.DELETE_CERTIFICATE_TEMPLATE, metadata: { - certificateTemplateId: certificateTemplate.id + certificateTemplateId: certificateTemplate.id, + name: certificateTemplate.slug } } }); diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index 568d418da..19fa131b6 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -196,7 +196,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => tags: [ApiDocsTags.PkiCertificates], body: z.object({ profileId: z.string().uuid(), - identifiers: z + subjectAlternativeNames: z .array( z.object({ type: z.enum(["dns", "ip"]), @@ -217,7 +217,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => 200: z.object({ orderId: z.string(), status: z.enum(["pending", "processing", "valid", "invalid"]), - identifiers: z.array( + subjectAlternativeNames: z.array( z.object({ type: z.enum(["dns", "ip"]), value: z.string(), @@ -256,7 +256,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => actorOrgId: req.permission.orgId, profileId: req.body.profileId, certificateOrder: { - identifiers: req.body.identifiers, + subjectAlternativeNames: req.body.subjectAlternativeNames, validity: { ttl: req.body.ttl }, @@ -286,7 +286,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => metadata: { certificateProfileId: req.body.profileId, orderId: data.orderId, - identifiers: req.body.identifiers.map((id) => `${id.type}:${id.value}`) + subjectAlternativeNames: req.body.subjectAlternativeNames.map((san) => `${san.type}:${san.value}`) } } }); diff --git a/backend/src/services/certificate-profile/certificate-profile-dal.ts b/backend/src/services/certificate-profile/certificate-profile-dal.ts index 2f692fba5..e8892c20c 100644 --- a/backend/src/services/certificate-profile/certificate-profile-dal.ts +++ b/backend/src/services/certificate-profile/certificate-profile-dal.ts @@ -68,18 +68,24 @@ export const certificateProfileDALFactory = (db: TDbClient) => { (tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"), (tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"), (tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"), - (tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"), + (tx || db).ref("slug").withSchema(TableName.CertificateTemplateV2).as("templateName"), (tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"), - (tx || db).ref("id").withSchema(TableName.EstEnrollmentConfig).as("estConfigId"), + (tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"), (tx || db) .ref("disableBootstrapCaValidation") - .withSchema(TableName.EstEnrollmentConfig) + .withSchema(TableName.PkiEstEnrollmentConfig) .as("estConfigDisableBootstrapCaValidation"), - (tx || db).ref("hashedPassphrase").withSchema(TableName.EstEnrollmentConfig).as("estConfigHashedPassphrase"), - (tx || db).ref("encryptedCaChain").withSchema(TableName.EstEnrollmentConfig).as("estConfigEncryptedCaChain"), - (tx || db).ref("id").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigId"), - (tx || db).ref("autoRenew").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenew"), - (tx || db).ref("autoRenewDays").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenewDays") + (tx || db) + .ref("hashedPassphrase") + .withSchema(TableName.PkiEstEnrollmentConfig) + .as("estConfigHashedPassphrase"), + (tx || db) + .ref("encryptedCaChain") + .withSchema(TableName.PkiEstEnrollmentConfig) + .as("estConfigEncryptedCaChain"), + (tx || db).ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"), + (tx || db).ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"), + (tx || db).ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays") ) .leftJoin( TableName.CertificateAuthority, @@ -92,14 +98,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => { `${TableName.CertificateTemplateV2}.id` ) .leftJoin( - TableName.EstEnrollmentConfig, + TableName.PkiEstEnrollmentConfig, `${TableName.CertificateProfile}.estConfigId`, - `${TableName.EstEnrollmentConfig}.id` + `${TableName.PkiEstEnrollmentConfig}.id` ) .leftJoin( - TableName.ApiEnrollmentConfig, + TableName.PkiApiEnrollmentConfig, `${TableName.CertificateProfile}.apiConfigId`, - `${TableName.ApiEnrollmentConfig}.id` + `${TableName.PkiApiEnrollmentConfig}.id` ) .where(`${TableName.CertificateProfile}.id`, id) .first(); @@ -151,9 +157,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => { if (search) { query = query.where((builder) => { void builder - .whereILike(`${TableName.CertificateProfile}.name`, `%${search}%`) - .orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`) - .orWhereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`); + .whereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`) + .orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`); }); } @@ -225,10 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => { if (search) { query = query.where((builder) => { - void builder - .whereILike("name", `%${search}%`) - .orWhereILike("description", `%${search}%`) - .orWhereILike("slug", `%${search}%`); + void builder.orWhereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`); }); } @@ -249,7 +251,9 @@ export const certificateProfileDALFactory = (db: TDbClient) => { const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { try { - const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ name, projectId }).first(); + const certificateProfile = await (tx || db)(TableName.CertificateProfile) + .where({ slug: name, projectId }) + .first(); return certificateProfile; } catch (error) { throw new DatabaseError({ error, name: "Find certificate profile by name and project id" }); diff --git a/backend/src/services/certificate-profile/certificate-profile-schemas.ts b/backend/src/services/certificate-profile/certificate-profile-schemas.ts index 978990d69..c8aac56de 100644 --- a/backend/src/services/certificate-profile/certificate-profile-schemas.ts +++ b/backend/src/services/certificate-profile/certificate-profile-schemas.ts @@ -8,7 +8,6 @@ export const createCertificateProfileSchema = z projectId: z.string().min(1), caId: z.string().uuid(), certificateTemplateId: z.string().uuid(), - name: z.string().min(1).max(255), slug: z .string() .min(1) @@ -46,7 +45,6 @@ export const createCertificateProfileSchema = z ); export const updateCertificateProfileSchema = z.object({ - name: z.string().min(1).max(255).optional(), slug: z .string() .min(1) diff --git a/backend/src/services/certificate-profile/certificate-profile-service.test.ts b/backend/src/services/certificate-profile/certificate-profile-service.test.ts index 0f527bf8f..2f72f559c 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -80,7 +80,6 @@ describe("CertificateProfileService", () => { const sampleProfile: TCertificateProfile = { id: "profile-123", projectId: "project-123", - name: "Test Profile", description: "Test certificate profile", slug: "test-profile", enrollmentType: EnrollmentType.API, @@ -174,9 +173,8 @@ describe("CertificateProfileService", () => { describe("createProfile", () => { const validProfileData = { - name: "New Profile", - description: "New test profile", slug: "new-profile", + description: "New test profile", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -204,9 +202,8 @@ describe("CertificateProfileService", () => { expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123"); expect(mockCertificateProfileDAL.findBySlugAndProjectId).toHaveBeenCalledWith("new-profile", "project-123"); expect(mockCertificateProfileDAL.create).toHaveBeenCalledWith({ - name: "New Profile", - description: "New test profile", slug: "new-profile", + description: "New test profile", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -273,9 +270,8 @@ describe("CertificateProfileService", () => { it("should throw ForbiddenRequestError for API enrollment without API config", async () => { const invalidData = { - name: "Invalid Profile", - description: "Invalid test profile", slug: "invalid-profile", + description: "Invalid test profile", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123" @@ -292,9 +288,8 @@ describe("CertificateProfileService", () => { it("should create profile with API enrollment", async () => { const apiProfileData = { - name: "API Profile", - description: "Profile with API enrollment", slug: "api-profile", + description: "Profile with API enrollment", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -317,7 +312,7 @@ describe("CertificateProfileService", () => { describe("updateProfile", () => { const updateData = { - name: "Updated Profile", + slug: "updated-profile", description: "Updated description" }; @@ -333,7 +328,7 @@ describe("CertificateProfileService", () => { data: updateData }); - expect(result.name).toBe("Updated Profile"); + expect(result.slug).toBe("updated-profile"); expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123"); expect(mockCertificateProfileDAL.updateById).toHaveBeenCalledWith("profile-123", updateData); }); @@ -697,9 +692,8 @@ describe("CertificateProfileService", () => { describe("profile configuration validation", () => { it("should validate EST enrollment configuration", async () => { const estProfileData = { - name: "EST Profile", - description: "Profile with EST enrollment", slug: "est-profile", + description: "Profile with EST enrollment", enrollmentType: EnrollmentType.EST, caId: "ca-123", certificateTemplateId: "template-123", @@ -737,9 +731,8 @@ describe("CertificateProfileService", () => { vi.clearAllMocks(); const duplicateSlugData = { - name: "Different Profile Name", + slug: "different-profile-name", description: "Profile with duplicate slug", - slug: "test-profile", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -763,9 +756,8 @@ describe("CertificateProfileService", () => { it("should validate auto-renewal configuration", async () => { const autoRenewData = { - name: "Auto Renew Profile", + slug: "auto-renew-profile", description: "Profile with auto-renewal", - slug: "auto-renew", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -987,9 +979,8 @@ describe("CertificateProfileService", () => { it("should handle invalid template reference during profile creation", async () => { const profileData = { - name: "Invalid Template Profile", + slug: "invalid-template-profile", description: "Profile with invalid template", - slug: "invalid-template", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "nonexistent-template", @@ -1013,9 +1004,8 @@ describe("CertificateProfileService", () => { it("should handle concurrent profile creation conflicts", async () => { const conflictingData = { - name: "Concurrent Profile", - description: "Profile created concurrently", slug: "concurrent-profile", + description: "Profile created concurrently", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", @@ -1042,9 +1032,8 @@ describe("CertificateProfileService", () => { describe("permission and security", () => { it("should validate project ownership for cross-project template access", async () => { const crossProjectData = { - name: "Cross Project Profile", + slug: "cross-project-profile", description: "Profile using template from different project", - slug: "cross-project", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-456", @@ -1056,7 +1045,7 @@ describe("CertificateProfileService", () => { const foreignTemplate = { id: "template-456", projectId: "different-project-456", - name: "Foreign Template" + slug: "foreign-template" }; (mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(foreignTemplate); @@ -1072,9 +1061,8 @@ describe("CertificateProfileService", () => { it("should validate slug format constraints", async () => { const invalidSlugData = { - name: "Invalid Slug Profile", + slug: "invalid-slug-profile", description: "Profile with invalid slug format", - slug: "Invalid_Slug_With_Underscores_And_Caps", enrollmentType: EnrollmentType.API, caId: "ca-123", certificateTemplateId: "template-123", diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 45b073db0..6937b9bcf 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -277,7 +277,28 @@ export const certificateProfileServiceFactory = ({ }); } - const updatedProfile = await certificateProfileDAL.updateById(profileId, data); + const { estConfig, apiConfig, ...profileUpdateData } = data; + + if (estConfig && existingProfile.estConfigId) { + await estEnrollmentConfigDAL.updateById(existingProfile.estConfigId, { + disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation, + ...(estConfig.passphrase && { + hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS) + }), + ...(estConfig.caChain && { + encryptedCaChain: Buffer.from(estConfig.caChain, "base64") + }) + }); + } + + if (apiConfig && existingProfile.apiConfigId) { + await apiEnrollmentConfigDAL.updateById(existingProfile.apiConfigId, { + autoRenew: apiConfig.autoRenew, + autoRenewDays: apiConfig.autoRenewDays + }); + } + + const updatedProfile = await certificateProfileDAL.updateById(profileId, profileUpdateData); return convertDalToService(updatedProfile); }; diff --git a/backend/src/services/certificate-profile/certificate-profile-types.ts b/backend/src/services/certificate-profile/certificate-profile-types.ts index 2cf70878c..6ed601b23 100644 --- a/backend/src/services/certificate-profile/certificate-profile-types.ts +++ b/backend/src/services/certificate-profile/certificate-profile-types.ts @@ -19,6 +19,15 @@ export type TCertificateProfileInsert = Omit & { enrollmentType?: EnrollmentType; + estConfig?: { + disableBootstrapCaValidation?: boolean; + passphrase?: string; + caChain?: string; + }; + apiConfig?: { + autoRenew?: boolean; + autoRenewDays?: number; + }; }; export type TCertificateProfileWithConfigs = TCertificateProfile & { diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts index a80258264..5307308b8 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-dal.ts @@ -139,7 +139,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { if (search) { query = query.where((builder) => { - void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`); + void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`); }); } @@ -165,7 +165,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { if (search) { query = query.where((builder) => { - void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`); + void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`); }); } @@ -176,10 +176,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { } }; - const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { + const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => { try { const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2) - .where({ name, projectId }) + .where({ slug, projectId }) .first(); if (!certificateTemplateV2) { @@ -188,7 +188,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { return parseJsonFields(certificateTemplateV2); } catch (error) { - throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" }); + throw new DatabaseError({ error, name: "Find certificate template v2 by slug and project id" }); } }; @@ -213,7 +213,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => { findById, findByProjectId, countByProjectId, - findByNameAndProjectId, + findBySlugAndProjectId, isTemplateInUse }; }; diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts index 068f58dcb..cfbf516f6 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-schemas.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; const attributeTypeSchema = z.enum(["common_name"]); @@ -87,7 +88,11 @@ export const templateV2KeyAlgorithmSchema = z.object({ export const createCertificateTemplateV2Schema = z.object({ projectId: z.string().min(1), - name: z.string().min(1).max(255), + slug: z + .string() + .min(1) + .max(255) + .regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"), description: z.string().max(1000).optional(), attributes: z.array(templateV2AttributeSchema).optional(), keyUsages: templateV2KeyUsagesSchema.optional(), @@ -99,7 +104,12 @@ export const createCertificateTemplateV2Schema = z.object({ }); export const updateCertificateTemplateV2Schema = z.object({ - name: z.string().min(1).max(255).optional(), + slug: z + .string() + .min(1) + .max(255) + .regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens") + .optional(), description: z.string().max(1000).optional(), attributes: z.array(templateV2AttributeSchema).optional(), keyUsages: templateV2KeyUsagesSchema.optional(), @@ -114,6 +124,11 @@ export const getCertificateTemplateV2ByIdSchema = z.object({ id: z.string().uuid() }); +export const getCertificateTemplateV2BySlugSchema = z.object({ + projectId: z.string().min(1), + slug: z.string().min(1) +}); + export const listCertificateTemplatesV2Schema = z.object({ projectId: z.string().min(1), offset: z.coerce.number().min(0).default(0), diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts index c578c0ae0..f99c8eed9 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.test.ts @@ -27,6 +27,7 @@ describe("CertificateTemplateV2Service", () => { let service: TCertificateTemplateV2ServiceFactory; const mockCertificateTemplateV2DAL = { + findBySlugAndProjectId: vi.fn(), create: vi.fn(), findById: vi.fn(), updateById: vi.fn(), @@ -99,7 +100,7 @@ describe("CertificateTemplateV2Service", () => { const sampleTemplate: TCertificateTemplateV2 = { id: "template-123", projectId: "project-123", - name: "Web Server Template", + slug: "web-server-template", description: "Template for web server certificates", ...samplePolicy, createdAt: new Date(), @@ -136,6 +137,7 @@ describe("CertificateTemplateV2Service", () => { }); mockCertificateTemplateV2DAL.findByNameAndProjectId.mockResolvedValue(null); + mockCertificateTemplateV2DAL.findBySlugAndProjectId.mockResolvedValue(null); service = certificateTemplateV2ServiceFactory({ certificateTemplateV2DAL: mockCertificateTemplateV2DAL as TCertificateTemplateV2DALFactory, @@ -149,7 +151,7 @@ describe("CertificateTemplateV2Service", () => { describe("createTemplateV2", () => { const createData: Omit = { - name: "Test Template", + slug: "test-template", description: "Test description", ...samplePolicy }; @@ -239,7 +241,7 @@ describe("CertificateTemplateV2Service", () => { describe("updateTemplateV2", () => { it("should update template with valid data", async () => { - const updateData = { name: "Updated Template Name" }; + const updateData = { slug: "updated-template-name" }; const updatedTemplate = { ...sampleTemplate, ...updateData }; mockCertificateTemplateV2DAL.findById.mockResolvedValue(sampleTemplate); @@ -263,7 +265,7 @@ describe("CertificateTemplateV2Service", () => { service.updateTemplateV2({ ...mockActor, templateId: "nonexistent-template", - data: { name: "Updated Name" } + data: { slug: "updated-name" } }) ).rejects.toThrow(NotFoundError); }); diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts index d098e4a42..727641ea7 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; import RE2 from "re2"; import { ActionProjectType } from "@app/db/schemas"; @@ -8,6 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal"; @@ -115,6 +117,28 @@ export const certificateTemplateV2ServiceFactory = ({ ); }; + const generateTemplateSlug = (baseSlug?: string): string => { + if (baseSlug) { + return slugify(baseSlug); + } + return slugify(alphaNumericNanoId(12)); + }; + + const ensureUniqueSlug = async (projectId: string, desiredSlug: string, templateId?: string): Promise => { + const existingTemplate = await certificateTemplateV2DAL.findBySlugAndProjectId(desiredSlug, projectId); + if (!existingTemplate || (templateId && existingTemplate.id === templateId)) { + return desiredSlug; + } + const alternativeSlug = `${desiredSlug}-${alphaNumericNanoId(8)}`; + const existingAlternative = await certificateTemplateV2DAL.findBySlugAndProjectId(alternativeSlug, projectId); + if (!existingAlternative) { + return alternativeSlug; + } + + const randomSlug = slugify(alphaNumericNanoId(12)); + return randomSlug; + }; + const validateRequestAgainstPolicy = ( template: TCertificateTemplateV2, request: TCertificateRequest @@ -361,8 +385,12 @@ export const certificateTemplateV2ServiceFactory = ({ keyAlgorithm: data.keyAlgorithm }); + const slug = data.slug || generateTemplateSlug(); + const uniqueSlug = await ensureUniqueSlug(projectId, slug); + const template = await certificateTemplateV2DAL.create({ ...data, + slug: uniqueSlug, projectId }); @@ -417,7 +445,13 @@ export const certificateTemplateV2ServiceFactory = ({ validateTemplatePolicy(mergedPolicy); } - const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, data); + const updateData = { ...data }; + if (data.slug && typeof data.slug === "string" && data.slug !== existingTemplate.slug) { + const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, data.slug, templateId); + updateData.slug = uniqueSlug; + } + + const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, updateData); if (!updatedTemplate) { throw new NotFoundError({ message: "Failed to update certificate template" }); } @@ -459,6 +493,43 @@ export const certificateTemplateV2ServiceFactory = ({ return template; }; + const getTemplateV2BySlug = async ({ + actor, + actorId, + actorAuthMethod, + actorOrgId, + projectId, + slug + }: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string; + projectId: string; + slug: string; + }): Promise => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPkiTemplateActions.Read, + ProjectPermissionSub.CertificateTemplates + ); + + const template = await certificateTemplateV2DAL.findBySlugAndProjectId(slug, projectId); + if (!template) { + throw new NotFoundError({ message: "Certificate template not found" }); + } + + return template; + }; + const listTemplatesV2 = async ({ actor, actorId, @@ -571,6 +642,7 @@ export const certificateTemplateV2ServiceFactory = ({ createTemplateV2, updateTemplateV2, getTemplateV2ById, + getTemplateV2BySlug, listTemplatesV2, deleteTemplateV2, validateCertificateRequest diff --git a/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts b/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts index cf9f52425..17cd96642 100644 --- a/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts +++ b/backend/src/services/certificate-template-v2/certificate-template-v2-types.ts @@ -74,7 +74,7 @@ export type TCertificateTemplateV2Insert = Omit< export type TCertificateTemplateV2Update = Partial< Pick< TCertificateTemplateV2, - | "name" + | "slug" | "description" | "attributes" | "keyUsages" diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index ce07621a7..b078bde1a 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -347,7 +347,7 @@ describe("CertificateV3Service", () => { describe("orderCertificateFromProfile", () => { const mockCertificateOrder = { - identifiers: [{ type: "dns" as const, value: "example.com" }], + subjectAlternativeNames: [{ type: "dns" as const, value: "example.com" }], validity: { ttl: "30d" }, commonName: "example.com", keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE], @@ -411,8 +411,8 @@ describe("CertificateV3Service", () => { expect(result).toHaveProperty("orderId"); expect(result).toHaveProperty("status", "valid"); expect(result).toHaveProperty("certificate"); - expect(result.identifiers).toHaveLength(1); - expect(result.identifiers[0]).toEqual({ + expect(result.subjectAlternativeNames).toHaveLength(1); + expect(result.subjectAlternativeNames[0]).toEqual({ type: "dns", value: "example.com", status: "valid" diff --git a/backend/src/services/certificate-v3/certificate-v3-service.ts b/backend/src/services/certificate-v3/certificate-v3-service.ts index 32ba7b958..cdf98c215 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.ts @@ -293,9 +293,9 @@ export const certificateV3ServiceFactory = ({ commonName: certificateOrder.commonName, keyUsages: certificateOrder.keyUsages, extendedKeyUsages: certificateOrder.extendedKeyUsages, - subjectAlternativeNames: certificateOrder.identifiers.map((id) => ({ - type: id.type === "dns" ? ("dns_name" as const) : ("ip_address" as const), - value: id.value + subjectAlternativeNames: certificateOrder.subjectAlternativeNames.map((san) => ({ + type: san.type === "dns" ? ("dns_name" as const) : ("ip_address" as const), + value: san.value })), validity: certificateOrder.validity, notBefore: certificateOrder.notBefore, @@ -334,16 +334,16 @@ export const certificateV3ServiceFactory = ({ }); const orderId = randomUUID(); - const identifiers = certificateOrder.identifiers.map((id) => ({ - type: id.type, - value: id.value, + const subjectAlternativeNames = certificateOrder.subjectAlternativeNames.map((san) => ({ + type: san.type, + value: san.value, status: "valid" as const })); - const authorizations = certificateOrder.identifiers.map((id) => ({ + const authorizations = certificateOrder.subjectAlternativeNames.map((san) => ({ identifier: { - type: id.type, - value: id.value + type: san.type, + value: san.value }, status: "valid" as const, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), @@ -360,7 +360,7 @@ export const certificateV3ServiceFactory = ({ return { orderId, status: "valid", - identifiers, + subjectAlternativeNames, authorizations, finalize: `/api/v3/certificates/orders/${orderId}/finalize`, certificate: certificateResult.certificate diff --git a/backend/src/services/certificate-v3/certificate-v3-types.ts b/backend/src/services/certificate-v3/certificate-v3-types.ts index bd0ec2e4d..c1d69b2f5 100644 --- a/backend/src/services/certificate-v3/certificate-v3-types.ts +++ b/backend/src/services/certificate-v3/certificate-v3-types.ts @@ -35,7 +35,7 @@ export type TSignCertificateFromProfileDTO = { export type TOrderCertificateFromProfileDTO = { profileId: string; certificateOrder: { - identifiers: Array<{ + subjectAlternativeNames: Array<{ type: "dns" | "ip"; value: string; }>; @@ -64,7 +64,7 @@ export type TCertificateFromProfileResponse = { export type TCertificateOrderResponse = { orderId: string; status: "pending" | "processing" | "valid" | "invalid"; - identifiers: Array<{ + subjectAlternativeNames: Array<{ type: "dns" | "ip"; value: string; status: "pending" | "processing" | "valid" | "invalid"; diff --git a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts index 558b5deda..d5b7f9c9c 100644 --- a/backend/src/services/enrollment-config/api-enrollment-config-dal.ts +++ b/backend/src/services/enrollment-config/api-enrollment-config-dal.ts @@ -10,11 +10,11 @@ import { TApiEnrollmentConfigInsert, TApiEnrollmentConfigUpdate } from "./enroll export type TApiEnrollmentConfigDALFactory = ReturnType; export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { - const apiEnrollmentConfigOrm = ormify(db, TableName.ApiEnrollmentConfig); + const apiEnrollmentConfigOrm = ormify(db, TableName.PkiApiEnrollmentConfig); const create = async (data: TApiEnrollmentConfigInsert, tx?: Knex) => { try { - const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).insert(data).returning("*"); + const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).insert(data).returning("*"); return apiConfig; } catch (error) { @@ -24,7 +24,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TApiEnrollmentConfigUpdate, tx?: Knex) => { try { - const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).update(data).returning("*"); + const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).update(data).returning("*"); return apiConfig; } catch (error) { @@ -34,7 +34,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const deleteById = async (id: string, tx?: Knex) => { try { - const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).del().returning("*"); + const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).del().returning("*"); return apiConfig; } catch (error) { @@ -44,7 +44,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const apiConfig = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).first(); + const apiConfig = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).first(); return apiConfig; } catch (error) { @@ -60,15 +60,15 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => { const profiles = await (tx || db)(TableName.CertificateProfile) .join( - TableName.ApiEnrollmentConfig, + TableName.PkiApiEnrollmentConfig, `${TableName.CertificateProfile}.apiConfigId`, - `${TableName.ApiEnrollmentConfig}.id` + `${TableName.PkiApiEnrollmentConfig}.id` ) - .where(`${TableName.ApiEnrollmentConfig}.autoRenew`, true) + .where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true) .where((query) => { void query - .whereNull(`${TableName.ApiEnrollmentConfig}.autoRenewDays`) - .orWhere(`${TableName.ApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); + .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) + .orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); }) .select((tx || db).ref("id").withSchema(TableName.CertificateProfile)) .select((tx || db).ref("name").withSchema(TableName.CertificateProfile)) diff --git a/backend/src/services/enrollment-config/enrollment-config-types.ts b/backend/src/services/enrollment-config/enrollment-config-types.ts index 1581f114c..329d98278 100644 --- a/backend/src/services/enrollment-config/enrollment-config-types.ts +++ b/backend/src/services/enrollment-config/enrollment-config-types.ts @@ -1,21 +1,21 @@ import { - TApiEnrollmentConfigs, - TApiEnrollmentConfigsInsert, - TApiEnrollmentConfigsUpdate -} from "@app/db/schemas/api-enrollment-configs"; + TPkiApiEnrollmentConfigs, + TPkiApiEnrollmentConfigsInsert, + TPkiApiEnrollmentConfigsUpdate +} from "@app/db/schemas/pki-api-enrollment-configs"; import { - TEstEnrollmentConfigs, - TEstEnrollmentConfigsInsert, - TEstEnrollmentConfigsUpdate -} from "@app/db/schemas/est-enrollment-configs"; + TPkiEstEnrollmentConfigs, + TPkiEstEnrollmentConfigsInsert, + TPkiEstEnrollmentConfigsUpdate +} from "@app/db/schemas/pki-est-enrollment-configs"; -export type TEstEnrollmentConfig = TEstEnrollmentConfigs; -export type TEstEnrollmentConfigInsert = TEstEnrollmentConfigsInsert; -export type TEstEnrollmentConfigUpdate = TEstEnrollmentConfigsUpdate; +export type TEstEnrollmentConfig = TPkiEstEnrollmentConfigs; +export type TEstEnrollmentConfigInsert = TPkiEstEnrollmentConfigsInsert; +export type TEstEnrollmentConfigUpdate = TPkiEstEnrollmentConfigsUpdate; -export type TApiEnrollmentConfig = TApiEnrollmentConfigs; -export type TApiEnrollmentConfigInsert = TApiEnrollmentConfigsInsert; -export type TApiEnrollmentConfigUpdate = TApiEnrollmentConfigsUpdate; +export type TApiEnrollmentConfig = TPkiApiEnrollmentConfigs; +export type TApiEnrollmentConfigInsert = TPkiApiEnrollmentConfigsInsert; +export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate; export interface TEstConfigData { disableBootstrapCaValidation: boolean; diff --git a/backend/src/services/enrollment-config/est-enrollment-config-dal.ts b/backend/src/services/enrollment-config/est-enrollment-config-dal.ts index 0da507225..b63fb0316 100644 --- a/backend/src/services/enrollment-config/est-enrollment-config-dal.ts +++ b/backend/src/services/enrollment-config/est-enrollment-config-dal.ts @@ -10,11 +10,11 @@ import { TEstEnrollmentConfigInsert, TEstEnrollmentConfigUpdate } from "./enroll export type TEstEnrollmentConfigDALFactory = ReturnType; export const estEnrollmentConfigDALFactory = (db: TDbClient) => { - const estEnrollmentConfigOrm = ormify(db, TableName.EstEnrollmentConfig); + const estEnrollmentConfigOrm = ormify(db, TableName.PkiEstEnrollmentConfig); const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => { try { - const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).insert(data).returning("*"); + const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*"); return estConfig; } catch (error) { @@ -24,7 +24,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => { try { - const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).update(data).returning("*"); + const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*"); return estConfig; } catch (error) { @@ -34,7 +34,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { const deleteById = async (id: string, tx?: Knex) => { try { - const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).del().returning("*"); + const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).del().returning("*"); return estConfig; } catch (error) { @@ -44,7 +44,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const estConfig = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).first(); + const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first(); return estConfig; } catch (error) { diff --git a/frontend/src/hooks/api/certificateProfiles/types.ts b/frontend/src/hooks/api/certificateProfiles/types.ts index 05307eb9a..e49a89cc7 100644 --- a/frontend/src/hooks/api/certificateProfiles/types.ts +++ b/frontend/src/hooks/api/certificateProfiles/types.ts @@ -3,7 +3,6 @@ export type TCertificateProfile = { projectId: string; caId: string; certificateTemplateId: string; - name: string; slug: string; description?: string; enrollmentType: "api" | "est"; @@ -24,7 +23,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & { certificateTemplate?: { id: string; projectId: string; - name: string; + slug: string; description?: string; }; estConfig?: { @@ -44,7 +43,6 @@ export type TCreateCertificateProfileDTO = { projectId: string; caId: string; certificateTemplateId: string; - name: string; slug: string; description?: string; enrollmentType: "api" | "est"; @@ -61,7 +59,7 @@ export type TCreateCertificateProfileDTO = { export type TUpdateCertificateProfileDTO = { profileId: string; - name?: string; + slug?: string; description?: string; estConfig?: { disableBootstrapCaValidation?: boolean; diff --git a/frontend/src/hooks/api/certificateTemplates/types.ts b/frontend/src/hooks/api/certificateTemplates/types.ts index f6ad1c8cb..a02856a0f 100644 --- a/frontend/src/hooks/api/certificateTemplates/types.ts +++ b/frontend/src/hooks/api/certificateTemplates/types.ts @@ -167,7 +167,7 @@ export type TCertificateTemplateV2Policy = { export type TCertificateTemplateV2New = { id: string; projectId: string; - name: string; + slug: string; description?: string; attributes: any; keyUsages: any; @@ -182,7 +182,7 @@ export type TCertificateTemplateV2New = { export type TCreateCertificateTemplateV2NewDTO = { projectId: string; - name: string; + slug: string; description?: string; attributes: TCertificateTemplateV2Policy["attributes"]; keyUsages: TCertificateTemplateV2Policy["keyUsages"]; @@ -195,7 +195,7 @@ export type TCreateCertificateTemplateV2NewDTO = { export type TUpdateCertificateTemplateV2NewDTO = { templateId: string; - name?: string; + slug?: string; description?: string; attributes?: TCertificateTemplateV2Policy["attributes"]; keyUsages?: TCertificateTemplateV2Policy["keyUsages"]; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index b17e394a3..ab020e27e 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -15,6 +15,7 @@ import { AccordionItem, AccordionTrigger, Button, + Checkbox, FormControl, FormLabel, IconButton, @@ -26,7 +27,7 @@ import { Tooltip } from "@app/components/v2"; import { useProject } from "@app/context"; -import { useCreateCertificateV3, useGetCert, useListWorkspacePkiCollections } from "@app/hooks/api"; +import { useCreateCertificateV3, useGetCert } from "@app/hooks/api"; import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles"; import { certKeyAlgorithms, @@ -44,53 +45,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; -type TriStateToggleProps = { - value: boolean | undefined; - onChange: (value: boolean | undefined) => void; - leftLabel: string; - rightLabel: string; -}; - -const TriStateToggle = ({ value, onChange, leftLabel, rightLabel }: TriStateToggleProps) => { - return ( -
- - -
- ); -}; - const schema = z.object({ profileId: z.string().min(1, "Profile is required"), - collectionId: z.string().optional(), friendlyName: z.string(), subjectAttributes: z .array( z.object({ type: z.enum(["common_name"]), - value: z.string().min(1, "Value is required") + value: z.string().min(1, "Value is required"), + include: z.enum(["mandatory", "optional", "prohibit"]).optional() }) ) .min(1, "At least one subject attribute is required"), @@ -98,7 +61,8 @@ const schema = z.object({ .array( z.object({ type: z.enum(["dns", "ip", "email", "uri"]), - value: z.string().min(1, "Value is required") + value: z.string().min(1, "Value is required"), + include: z.enum(["mandatory", "optional", "prohibit"]).optional() }) ) .default([]), @@ -159,10 +123,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => includeMetrics: false }); - const { data: collectionsData } = useListWorkspacePkiCollections({ - projectId: currentProject?.id || "" - }); - const { mutateAsync: createCertificate } = useCreateCertificateV3(); const { @@ -461,7 +421,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => const onFormSubmit = async ({ profileId, friendlyName, - collectionId, subjectAttributes, altNames, ttl, @@ -481,7 +440,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ profileId, projectSlug: currentProject.slug, - pkiCollectionId: collectionId, friendlyName, commonName: getAttributeValue("common_name"), altNames: altNames @@ -628,34 +586,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => > {profilesData?.certificateProfiles?.map((profile) => ( - {profile.name} - - ))} - - - )} - /> - - ( - - @@ -701,10 +632,25 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => onChange(newValue); }} className="w-48" - position="popper" > Common Name + { @@ -769,14 +715,29 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => }; onChange(newValue); }} - className="w-32" - position="popper" + className="w-24" > DNS IP Email URI + { @@ -908,11 +869,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => - + Key Usages -
+
{KEY_USAGES_OPTIONS.filter(({ value }) => { if (allowedKeyUsages.length === 0) return true; const templateToEnumMap = { @@ -933,15 +894,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => ( -
- {label} - + field.onChange(checked)} + /> +
)} @@ -954,7 +918,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => Extended Key Usages -
+
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => { if (allowedExtendedKeyUsages.length === 0) return true; const templateToEnumMap = { @@ -973,15 +937,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) => ( -
- {label} - + field.onChange(checked)} + /> +
)} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx index 54f5115df..5f8923da9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CertificateProfilesTab.tsx @@ -14,7 +14,6 @@ import { } from "@app/hooks/api/certificateProfiles"; import { CreateProfileModal } from "./CreateProfileModal"; -import { EditProfileModal } from "./EditProfileModal"; import { ProfileList } from "./ProfileList"; export const CertificateProfilesTab = () => { @@ -89,23 +88,24 @@ export const CertificateProfilesTab = () => { {selectedProfile && ( <> - { setIsEditModalOpen(false); setSelectedProfile(null); }} profile={selectedProfile} + mode="edit" /> { setIsDeleteModalOpen(isOpen); if (!isOpen) setSelectedProfile(null); }} - deleteKey={selectedProfile.name} + deleteKey={selectedProfile.slug} onDeleteApproved={handleDeleteConfirm} /> diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx index 5d28e8787..fc24b8c03 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ -import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -18,12 +16,15 @@ import { } from "@app/components/v2"; import { useProject } from "@app/context"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; -import { useCreateCertificateProfile } from "@app/hooks/api/certificateProfiles"; +import { + TCertificateProfileWithDetails, + useCreateCertificateProfile, + useUpdateCertificateProfile +} from "@app/hooks/api/certificateProfiles"; import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; -const schema = z +const createSchema = z .object({ - name: z.string().trim().min(1, "Profile name is required"), slug: z.string().trim().min(1, "Profile slug is required"), description: z.string().optional(), enrollmentType: z.enum(["api", "est"]), @@ -58,14 +59,52 @@ const schema = z } ); -export type FormData = z.infer; +const editSchema = z + .object({ + slug: z.string().trim().min(1, "Profile slug is required"), + description: z.string().optional(), + enrollmentType: z.enum(["api", "est"]), + certificateAuthorityId: z.string().optional(), + certificateTemplateId: z.string().optional(), + estConfig: z + .object({ + disableBootstrapCaValidation: z.boolean().optional(), + passphrase: z.string().optional(), + caChain: z.string().optional() + }) + .optional(), + apiConfig: z + .object({ + autoRenew: z.boolean().optional(), + autoRenewDays: z.number().min(1).max(365).optional() + }) + .optional() + }) + .refine( + (data) => { + if (data.enrollmentType === "est" && !data.estConfig) { + return false; + } + if (data.enrollmentType === "api" && !data.apiConfig) { + return false; + } + return true; + }, + { + message: "Configuration is required for selected enrollment type" + } + ); + +export type FormData = z.infer; interface Props { isOpen: boolean; onClose: () => void; + profile?: TCertificateProfileWithDetails; + mode?: "create" | "edit"; } -export const CreateProfileModal = ({ isOpen, onClose }: Props) => { +export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }: Props) => { const { currentProject } = useProject(); const { data: caData } = useListCasByProjectId(currentProject?.id || ""); @@ -76,80 +115,97 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { }); const createProfile = useCreateCertificateProfile(); + const updateProfile = useUpdateCertificateProfile(); + + const isEdit = mode === "edit" && profile; const certificateAuthorities = caData || []; const certificateTemplates = templateData?.certificateTemplates || []; - const { - control, - handleSubmit, - reset, - watch, - setValue, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: "", - slug: "", - description: "", - enrollmentType: "api", - certificateAuthorityId: "", - certificateTemplateId: "", - apiConfig: { - autoRenew: false, - autoRenewDays: 30 - } - } + const { control, handleSubmit, reset, watch, setValue } = useForm({ + resolver: zodResolver(isEdit ? editSchema : createSchema), + defaultValues: isEdit + ? { + slug: profile.slug, + description: profile.description || "", + enrollmentType: profile.enrollmentType, + certificateAuthorityId: profile.caId, + certificateTemplateId: profile.certificateTemplateId, + estConfig: { + disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false, + passphrase: "", + caChain: "" + }, + apiConfig: { + autoRenew: profile.apiConfig?.autoRenew || false, + autoRenewDays: profile.apiConfig?.autoRenewDays || 30 + } + } + : { + slug: "", + description: "", + enrollmentType: "api", + certificateAuthorityId: "", + certificateTemplateId: "", + apiConfig: { + autoRenew: false, + autoRenewDays: 30 + } + } }); - const watchedName = watch("name"); const watchedEnrollmentType = watch("enrollmentType"); const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation"); const watchedAutoRenew = watch("apiConfig.autoRenew"); - useEffect(() => { - if (watchedName && !watch("slug")) { - const slug = watchedName - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); - setValue("slug", slug); - } - }, [watchedName, setValue, watch]); - const onFormSubmit = async (data: FormData) => { try { - if (!currentProject?.id) return; + if (!currentProject?.id && !isEdit) return; - const payload: any = { - projectId: currentProject.id, - name: data.name, - slug: data.slug, - description: data.description, - enrollmentType: data.enrollmentType, - caId: data.certificateAuthorityId, - certificateTemplateId: data.certificateTemplateId - }; + if (isEdit) { + const updateData: any = { + profileId: profile.id, + name: data.slug, + description: data.description + }; - if (data.enrollmentType === "est" && data.estConfig) { - payload.estConfig = data.estConfig; - } else if (data.enrollmentType === "api" && data.apiConfig) { - payload.apiConfig = data.apiConfig; + if (data.enrollmentType === "est" && data.estConfig) { + updateData.estConfig = data.estConfig; + } else if (data.enrollmentType === "api" && data.apiConfig) { + updateData.apiConfig = data.apiConfig; + } + + await updateProfile.mutateAsync(updateData); + } else { + const createData: any = { + projectId: currentProject!.id, + slug: data.slug, + description: data.description, + enrollmentType: data.enrollmentType, + caId: data.certificateAuthorityId, + certificateTemplateId: data.certificateTemplateId + }; + + if (data.enrollmentType === "est" && data.estConfig) { + createData.estConfig = data.estConfig; + } else if (data.enrollmentType === "api" && data.apiConfig) { + createData.apiConfig = data.apiConfig; + } + + await createProfile.mutateAsync(createData); } - await createProfile.mutateAsync(payload); createNotification({ - text: "Certificate profile created successfully", + text: `Certificate profile ${isEdit ? "updated" : "created"} successfully`, type: "success" }); reset(); onClose(); } catch (error) { - console.error("Error creating profile:", error); + console.error(`Error ${isEdit ? "updating" : "creating"} profile:`, error); createNotification({ - text: "Failed to create certificate profile", + text: `Failed to ${isEdit ? "update" : "create"} certificate profile`, type: "error" }); } @@ -166,25 +222,14 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { }} >
- ( - - - - )} - /> - { isError={Boolean(error)} errorText={error?.message} > - + )} /> @@ -226,6 +271,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { placeholder="Select a certificate authority" className="w-full" position="popper" + isDisabled={Boolean(isEdit)} > {certificateAuthorities.map((ca: any) => ( @@ -269,10 +315,11 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { placeholder="Select a certificate template" className="w-full" position="popper" + isDisabled={Boolean(isEdit)} > {certificateTemplates.map((template) => ( - {template.name} + {template.slug} ))} @@ -290,7 +337,13 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { isError={Boolean(error)} errorText={error?.message} > - API EST @@ -314,12 +367,9 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { onCheckedChange={onChange} />
- +

Skip CA certificate validation during EST bootstrap phase

@@ -335,7 +385,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { render={({ field, fieldState: { error } }) => ( @@ -356,7 +406,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { render={({ field, fieldState: { error } }) => ( @@ -424,10 +474,18 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => { )}
- -
diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/EditProfileModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/EditProfileModal.tsx deleted file mode 100644 index 7ab072a86..000000000 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/EditProfileModal.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { useEffect, useState } from "react"; -import { faSave } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { - Button, - Checkbox, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem, - TextArea -} from "@app/components/v2"; -import { useProject } from "@app/context"; -import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; -import { - TCertificateProfileWithDetails, - useUpdateCertificateProfile -} from "@app/hooks/api/certificateProfiles"; -import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; - -interface Props { - isOpen: boolean; - onClose: () => void; - profile: TCertificateProfileWithDetails; -} - -export const EditProfileModal = ({ isOpen, onClose, profile }: Props) => { - const { currentProject } = useProject(); - const updateProfile = useUpdateCertificateProfile(); - - const { data: caData } = useListCasByProjectId(currentProject?.id || ""); - const { data: templateData } = useListCertificateTemplatesV2({ - projectId: currentProject?.id || "", - limit: 100, - offset: 0 - }); - - const certificateAuthorities = caData || []; - const certificateTemplates = templateData?.certificateTemplates || []; - - const [formData, setFormData] = useState({ - name: "", - slug: "", - description: "", - enrollmentType: "api" as "api" | "est", - certificateAuthorityId: "", - certificateTemplateId: "", - estConfig: { - disableBootstrapCaValidation: false, - passphrase: "", - caChain: "" - }, - apiConfig: { - autoRenew: false, - autoRenewDays: 30 - } - }); - - useEffect(() => { - if (profile) { - setFormData({ - name: profile.name, - slug: profile.slug, - description: profile.description || "", - enrollmentType: profile.enrollmentType, - certificateAuthorityId: profile.caId, - certificateTemplateId: profile.certificateTemplateId, - estConfig: { - disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false, - passphrase: "", - caChain: "" - }, - apiConfig: { - autoRenew: profile.apiConfig?.autoRenew || false, - autoRenewDays: profile.apiConfig?.autoRenewDays || 30 - } - }); - } - }, [profile]); - - const handleInputChange = (field: string, value: string | boolean | number) => { - if (field.includes(".")) { - const [parent, child] = field.split("."); - setFormData((prev) => ({ - ...prev, - [parent]: { - ...(prev as any)[parent], - [child]: value - } - })); - } else { - setFormData((prev) => ({ - ...prev, - [field]: value - })); - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!formData.name) { - return; - } - - try { - const payload: any = { - profileId: profile.id, - name: formData.name, - description: formData.description - }; - - if (formData.enrollmentType === "est") { - payload.estConfig = { - disableBootstrapCaValidation: formData.estConfig.disableBootstrapCaValidation, - passphrase: formData.estConfig.passphrase, - caChain: formData.estConfig.caChain - }; - } else if (formData.enrollmentType === "api") { - payload.apiConfig = { - autoRenew: formData.apiConfig.autoRenew, - autoRenewDays: formData.apiConfig.autoRenewDays - }; - } - - await updateProfile.mutateAsync(payload); - - createNotification({ - text: "Certificate profile updated successfully", - type: "success" - }); - - onClose(); - } catch (error) { - console.error("Error updating profile:", error); - createNotification({ - text: "Failed to update certificate profile", - type: "error" - }); - } - }; - - return ( - - - - - handleInputChange("name", e.target.value)} - /> - - - - handleInputChange("slug", e.target.value)} - disabled - /> - - - -