From e195afba112b1b691573535da297db724259036a Mon Sep 17 00:00:00 2001 From: Carlos Monastyrski Date: Fri, 17 Oct 2025 15:47:41 -0300 Subject: [PATCH] PKI revamp: general improvements --- .../routes/v1/certificate-profiles-router.ts | 3 +- .../server/routes/v3/certificates-router.ts | 30 +- .../internal-certificate-authority-service.ts | 31 +- .../certificate-profile-service.test.ts | 31 +- .../certificate-profile-service.ts | 12 +- .../certificate-template-v2-schemas.ts | 79 ++- .../services/certificate/certificate-types.ts | 1 + .../certificate-templates/create.mdx | 10 - .../certificate-templates/delete.mdx | 10 - .../certificate-templates/get-by-id.mdx | 10 - .../certificate-templates/update.mdx | 10 - .../endpoints/pki/subscribers/create.mdx | 10 - .../endpoints/pki/subscribers/delete.mdx | 10 - .../subscribers/get-latest-cert-bundle.mdx | 10 - .../endpoints/pki/subscribers/issue-cert.mdx | 10 - .../endpoints/pki/subscribers/list-certs.mdx | 10 - .../endpoints/pki/subscribers/order-cert.mdx | 10 - .../endpoints/pki/subscribers/read.mdx | 10 - .../endpoints/pki/subscribers/sign-cert.mdx | 10 - .../endpoints/pki/subscribers/update.mdx | 10 - docs/docs.json | 26 +- .../context/ProjectPermissionContext/types.ts | 4 +- frontend/src/hooks/api/ca/mutations.tsx | 4 + frontend/src/hooks/api/ca/types.ts | 8 +- .../src/hooks/api/certificates/mutations.tsx | 4 + .../components/CaModal.tsx | 22 +- .../components/CertificateIssuanceModal.tsx | 660 ++++++++++-------- .../PoliciesPage/PoliciesPage.tsx | 2 +- .../CreateProfileModal.tsx | 55 +- .../CertificateProfilesTab/ProfileRow.tsx | 2 +- .../CreateTemplateModal.tsx | 16 +- .../TemplateList.tsx | 2 +- .../shared/schemas.ts | 37 +- .../ProjectRoleModifySection.utils.tsx | 21 +- 34 files changed, 601 insertions(+), 579 deletions(-) delete mode 100644 docs/api-reference/endpoints/certificate-templates/create.mdx delete mode 100644 docs/api-reference/endpoints/certificate-templates/delete.mdx delete mode 100644 docs/api-reference/endpoints/certificate-templates/get-by-id.mdx delete mode 100644 docs/api-reference/endpoints/certificate-templates/update.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/create.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/delete.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/list-certs.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/order-cert.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/read.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx delete mode 100644 docs/api-reference/endpoints/pki/subscribers/update.mdx diff --git a/backend/src/server/routes/v1/certificate-profiles-router.ts b/backend/src/server/routes/v1/certificate-profiles-router.ts index cc39b5d48..a63a98d58 100644 --- a/backend/src/server/routes/v1/certificate-profiles-router.ts +++ b/backend/src/server/routes/v1/certificate-profiles-router.ts @@ -7,6 +7,7 @@ import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { CertStatus } from "@app/services/certificate/certificate-types"; import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types"; export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => { @@ -453,7 +454,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid querystring: z.object({ offset: z.coerce.number().min(0).default(0), limit: z.coerce.number().min(1).max(100).default(20), - status: z.enum(["active", "expired", "revoked"]).optional(), + status: z.nativeEnum(CertStatus).optional(), search: z.string().optional() }), response: { diff --git a/backend/src/server/routes/v3/certificates-router.ts b/backend/src/server/routes/v3/certificates-router.ts index 1e5b093a5..4cbc0d2eb 100644 --- a/backend/src/server/routes/v3/certificates-router.ts +++ b/backend/src/server/routes/v3/certificates-router.ts @@ -54,8 +54,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => body: z .object({ profileId: z.string().uuid(), - commonName: validateTemplateRegexField, - ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"), + commonName: validateTemplateRegexField.optional(), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), notBefore: validateCaDateField.optional(), @@ -162,8 +166,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => body: z .object({ profileId: z.string().uuid(), - csr: z.string().trim().min(1).max(4096), - ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"), + csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), notBefore: validateCaDateField.optional(), notAfter: validateCaDateField.optional() }) @@ -234,11 +242,19 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) => .array( z.object({ type: z.nativeEnum(ACMESANType), - value: z.string() + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") }) ) - .min(1), - ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"), + .min(1, "At least one subject alternative name must be provided"), + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .refine((val) => ms(val) > 0, "TTL must be a positive number"), keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(), extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(), notBefore: validateCaDateField.optional(), diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 8bf6ff075..3ed14a7ee 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -32,6 +32,7 @@ import { CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, + CertSignatureAlgorithm, CertStatus, TAltNameMapping } from "../../certificate/certificate-types"; @@ -1289,12 +1290,19 @@ export const internalCertificateAuthorityServiceFactory = ({ const caKeyAlgorithm = ca.internalCa.keyAlgorithm; const requestedKeyType = signatureAlgorithm.split("-")[0]; - const isRsaCa = caKeyAlgorithm.startsWith("RSA"); - const isEcdsaCa = caKeyAlgorithm.startsWith("EC"); + const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]); + const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]); - if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) { + if ( + (requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) || + (requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa) + ) { // eslint-disable-next-line no-nested-ternary - const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown"; + const supportedType = isRsaCa + ? CertSignatureAlgorithm.RSA_SHA256.split("-")[0] + : isEcdsaCa + ? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] + : "unknown"; throw new BadRequestError({ message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` }); @@ -1655,12 +1663,19 @@ export const internalCertificateAuthorityServiceFactory = ({ const caKeyAlgorithm = ca.internalCa.keyAlgorithm; const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA) - const isRsaCa = caKeyAlgorithm.startsWith("RSA"); - const isEcdsaCa = caKeyAlgorithm.startsWith("EC"); + const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]); + const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]); - if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) { + if ( + (requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) || + (requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa) + ) { // eslint-disable-next-line no-nested-ternary - const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "unknown"; + const supportedType = isRsaCa + ? CertSignatureAlgorithm.RSA_SHA256.split("-")[0] + : isEcdsaCa + ? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] + : "unknown"; throw new BadRequestError({ message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.` }); 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 7f390e580..b03d85128 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.test.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.test.ts @@ -559,7 +559,6 @@ describe("CertificateProfileService", () => { expect(result).toEqual(sampleProfile); expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123"); - expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123"); expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123"); }); @@ -573,18 +572,6 @@ describe("CertificateProfileService", () => { }) ).rejects.toThrow(NotFoundError); }); - - it("should throw ForbiddenRequestError when profile is in use", async () => { - (mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(true); - - await expect( - service.deleteProfile({ - ...mockActor, - profileId: "profile-123" - }) - ).rejects.toThrow(ForbiddenRequestError); - expect(mockCertificateProfileDAL.deleteById).not.toHaveBeenCalled(); - }); }); describe("getProfileCertificates", () => { @@ -841,23 +828,8 @@ describe("CertificateProfileService", () => { expect(result.enrollmentType).toBe(EnrollmentType.EST); }); - it("should prevent deletion of profiles with active certificates", async () => { + it("should allow deletion of profiles", async () => { (mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile); - (mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(true); - - await expect( - service.deleteProfile({ - ...mockActor, - profileId: "profile-123" - }) - ).rejects.toThrow(ForbiddenRequestError); - - expect(mockCertificateProfileDAL.deleteById).not.toHaveBeenCalled(); - }); - - it("should allow deletion of unused profiles", async () => { - (mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile); - (mockCertificateProfileDAL.isProfileInUse as any).mockResolvedValue(false); (mockCertificateProfileDAL.deleteById as any).mockResolvedValue(sampleProfile); const result = await service.deleteProfile({ @@ -866,7 +838,6 @@ describe("CertificateProfileService", () => { }); expect(result).toEqual(sampleProfile); - expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123"); expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123"); }); }); diff --git a/backend/src/services/certificate-profile/certificate-profile-service.ts b/backend/src/services/certificate-profile/certificate-profile-service.ts index 35c390af6..ccad3fccb 100644 --- a/backend/src/services/certificate-profile/certificate-profile-service.ts +++ b/backend/src/services/certificate-profile/certificate-profile-service.ts @@ -116,7 +116,7 @@ export const certificateProfileServiceFactory = ({ const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId); if (existingSlugProfile) { throw new ForbiddenRequestError({ - message: "Certificate profile with this slug already exists in project" + message: "Certificate profile with this name already exists in project" }); } @@ -245,7 +245,7 @@ export const certificateProfileServiceFactory = ({ ); if (conflictingProfile && conflictingProfile.id !== profileId) { throw new ForbiddenRequestError({ - message: "Certificate profile with this slug already exists in project" + message: "Certificate profile with this name already exists in project" }); } } @@ -521,14 +521,6 @@ export const certificateProfileServiceFactory = ({ ProjectPermissionSub.CertificateProfiles ); - // Check if profile is in use by any certificates - const isInUse = await certificateProfileDAL.isProfileInUse(profileId); - if (isInUse) { - throw new ForbiddenRequestError({ - message: "Cannot delete certificate profile that has issued certificates" - }); - } - const deletedProfile = await certificateProfileDAL.deleteById(profileId); if (!deletedProfile) { throw new NotFoundError({ message: "Failed to delete certificate profile" }); 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 5e653f626..9fcc51a57 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"; import { @@ -13,9 +14,9 @@ const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType); const templateV2SubjectSchema = z .object({ type: attributeTypeSchema, - allowed: z.array(z.string()).optional(), - required: z.array(z.string()).optional(), - denied: z.array(z.string()).optional() + allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(), + required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(), + denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional() }) .refine( (data) => { @@ -68,9 +69,9 @@ const templateV2ExtendedKeyUsagesSchema = z const templateV2SanSchema = z .object({ type: sanTypeSchema, - allowed: z.array(z.string()).optional(), - required: z.array(z.string()).optional(), - denied: z.array(z.string()).optional() + allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(), + required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(), + denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional() }) .refine( (data) => { @@ -87,22 +88,33 @@ const templateV2SanSchema = z const templateV2ValiditySchema = z.object({ max: z .string() - .regex(/^\d+[dhmy]$/, { + .regex(new RE2("^\\d+[dhmy]$"), { message: "Max validity must be in format like '365d', '12m', '1y', or '24h'" }) .optional() }); const templateV2AlgorithmsSchema = z.object({ - signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(), - keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional() + signature: z + .array(z.string().trim().min(1, "Algorithm cannot be empty")) + .min(1, "At least one signature algorithm must be provided") + .optional(), + keyAlgorithm: z + .array(z.string().trim().min(1, "Algorithm cannot be empty")) + .min(1, "At least one key algorithm must be provided") + .optional() }); export const certificateTemplateV2ResponseSchema = z.object({ id: z.string().uuid(), projectId: z.string().uuid("Project ID must be valid"), - name: z.string(), - description: z.string().nullable().optional(), + name: z + .string() + .trim() + .min(1, "Template name is required") + .max(255, "Template name must be less than 255 characters") + .regex(new RE2("^[a-zA-Z0-9-_]+$"), "Template name must contain only letters, numbers, hyphens, and underscores"), + description: z.string().trim().max(1000, "Description must be less than 1000 characters").nullable().optional(), subject: z.array(templateV2SubjectSchema).optional(), sans: z.array(templateV2SanSchema).optional(), keyUsages: templateV2KeyUsagesSchema.optional(), @@ -114,26 +126,53 @@ export const certificateTemplateV2ResponseSchema = z.object({ }); export const certificateRequestSchema = z.object({ - commonName: z.string().optional(), - organization: z.string().optional(), - country: z.string().optional(), - keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(), - extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(), + commonName: z + .string() + .trim() + .min(1, "Common name cannot be empty") + .max(64, "Common name must be less than 64 characters") + .optional(), + organization: z + .string() + .trim() + .min(1, "Organization cannot be empty") + .max(64, "Organization must be less than 64 characters") + .optional(), + country: z + .string() + .trim() + .min(2, "Country code must be 2 characters") + .max(2, "Country code must be 2 characters") + .optional(), + keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).min(1, "At least one key usage must be provided").optional(), + extendedKeyUsages: z + .array(z.nativeEnum(CertExtendedKeyUsageType)) + .min(1, "At least one extended key usage must be provided") + .optional(), subjectAlternativeNames: z .array( z.object({ type: sanTypeSchema, - value: z.string() + value: z + .string() + .trim() + .min(1, "SAN value cannot be empty") + .max(255, "SAN value must be less than 255 characters") }) ) + .min(1, "At least one SAN must be provided") .optional(), validity: z .object({ - ttl: z.string() + ttl: z + .string() + .trim() + .min(1, "TTL cannot be empty") + .regex(new RE2("^\\d+[dhmy]$"), "TTL must be in format like '365d', '12m', '1y', or '24h'") }) .optional(), - signatureAlgorithm: z.string().optional(), - keyAlgorithm: z.string().optional() + signatureAlgorithm: z.string().trim().min(1, "Signature algorithm cannot be empty").optional(), + keyAlgorithm: z.string().trim().min(1, "Key algorithm cannot be empty").optional() }); export const validateCertificateRequestSchema = z.object({ diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index b747314b3..9a09945c6 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -8,6 +8,7 @@ import { TCertificateSecretDALFactory } from "./certificate-secret-dal"; export enum CertStatus { ACTIVE = "active", + EXPIRED = "expired", REVOKED = "revoked" } diff --git a/docs/api-reference/endpoints/certificate-templates/create.mdx b/docs/api-reference/endpoints/certificate-templates/create.mdx deleted file mode 100644 index f159aff34..000000000 --- a/docs/api-reference/endpoints/certificate-templates/create.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Create" -openapi: "POST /api/v1/pki/certificate-templates" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Templates V2 API](/api-reference/endpoints/certificate-templates-v2) instead. - diff --git a/docs/api-reference/endpoints/certificate-templates/delete.mdx b/docs/api-reference/endpoints/certificate-templates/delete.mdx deleted file mode 100644 index 48c3fd755..000000000 --- a/docs/api-reference/endpoints/certificate-templates/delete.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Delete" -openapi: "DELETE /api/v1/pki/certificate-templates/{certificateTemplateId}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Templates V2 API](/api-reference/endpoints/certificate-templates-v2) instead. - diff --git a/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx b/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx deleted file mode 100644 index 09fcbd028..000000000 --- a/docs/api-reference/endpoints/certificate-templates/get-by-id.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Get by ID" -openapi: "GET /api/v1/pki/certificate-templates/{certificateTemplateId}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Templates V2 API](/api-reference/endpoints/certificate-templates-v2) instead. - diff --git a/docs/api-reference/endpoints/certificate-templates/update.mdx b/docs/api-reference/endpoints/certificate-templates/update.mdx deleted file mode 100644 index 9db27e5e6..000000000 --- a/docs/api-reference/endpoints/certificate-templates/update.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Update" -openapi: "PATCH /api/v1/pki/certificate-templates/{certificateTemplateId}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Templates V2 API](/api-reference/endpoints/certificate-templates-v2) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/create.mdx b/docs/api-reference/endpoints/pki/subscribers/create.mdx deleted file mode 100644 index 8285e2bdc..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/create.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Create" -openapi: "POST /api/v1/pki/subscribers" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/delete.mdx b/docs/api-reference/endpoints/pki/subscribers/delete.mdx deleted file mode 100644 index a553c2f9e..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/delete.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Delete" -openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle.mdx b/docs/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle.mdx deleted file mode 100644 index 1b6cfe19a..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/get-latest-cert-bundle.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Retrieve latest certificate bundle" -openapi: "GET /api/v1/pki/subscribers/{subscriberName}/latest-certificate-bundle" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx deleted file mode 100644 index 2ecd4bf50..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/issue-cert.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Issue Certificate" -openapi: "POST /api/v1/pki/subscribers/{subscriberName}/issue-certificate" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx b/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx deleted file mode 100644 index 1afb5e32a..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/list-certs.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "List Certificates" -openapi: "GET /api/v1/pki/subscribers/{subscriberName}/certificates" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/order-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/order-cert.mdx deleted file mode 100644 index ae512b071..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/order-cert.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Order Certificate" -openapi: "POST /api/v1/pki/subscribers/{subscriberName}/order-certificate" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/read.mdx b/docs/api-reference/endpoints/pki/subscribers/read.mdx deleted file mode 100644 index f18690c5e..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/read.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Retrieve" -openapi: "GET /api/v1/pki/subscribers/{subscriberName}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx b/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx deleted file mode 100644 index 9672bfb82..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/sign-cert.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Sign Certificate" -openapi: "POST /api/v1/pki/subscribers/{subscriberName}/sign-certificate" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/api-reference/endpoints/pki/subscribers/update.mdx b/docs/api-reference/endpoints/pki/subscribers/update.mdx deleted file mode 100644 index e85c6e28c..000000000 --- a/docs/api-reference/endpoints/pki/subscribers/update.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "Update" -openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}" ---- - - -**Deprecated API Endpoint** - -This endpoint is deprecated and will be removed in a future version. Please use the new [Certificate Profiles API](/api-reference/endpoints/certificate-profiles) instead. - diff --git a/docs/docs.json b/docs/docs.json index 9d2516e70..b2d514aed 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -2451,20 +2451,6 @@ { "group": "Infisical PKI", "pages": [ - { - "group": "Subscribers", - "pages": [ - "api-reference/endpoints/pki/subscribers/list-certs", - "api-reference/endpoints/pki/subscribers/create", - "api-reference/endpoints/pki/subscribers/read", - "api-reference/endpoints/pki/subscribers/update", - "api-reference/endpoints/pki/subscribers/delete", - "api-reference/endpoints/pki/subscribers/issue-cert", - "api-reference/endpoints/pki/subscribers/sign-cert", - "api-reference/endpoints/pki/subscribers/order-cert", - "api-reference/endpoints/pki/subscribers/get-latest-cert-bundle" - ] - }, { "group": "Certificate Authorities", "pages": [ @@ -2525,17 +2511,7 @@ "api-reference/endpoints/certificate-templates-v2/create", "api-reference/endpoints/certificate-templates-v2/update", "api-reference/endpoints/certificate-templates-v2/get-by-id", - "api-reference/endpoints/certificate-templates-v2/delete", - { - "group": "Legacy", - "pages": [ - "api-reference/endpoints/certificate-templates/list", - "api-reference/endpoints/certificate-templates/create", - "api-reference/endpoints/certificate-templates/update", - "api-reference/endpoints/certificate-templates/get-by-id", - "api-reference/endpoints/certificate-templates/delete" - ] - } + "api-reference/endpoints/certificate-templates-v2/delete" ] }, { diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 36685d542..0236113eb 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -129,8 +129,7 @@ export enum ProjectPermissionCertificateProfileActions { Create = "create", Edit = "edit", Delete = "delete", - IssueCert = "issue-cert", - ListCerts = "list-certs" + IssueCert = "issue-cert" } export enum ProjectPermissionSecretRotationActions { @@ -226,7 +225,6 @@ export type ConditionalProjectPermissionSubject = | ProjectPermissionSub.SshHosts | ProjectPermissionSub.PkiSubscribers | ProjectPermissionSub.CertificateTemplates - | ProjectPermissionSub.CertificateProfiles | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretRotation diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index c0bd8abf1..fa422054c 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -166,6 +166,10 @@ export const useCreateCertificateV3 = () => { queryClient.invalidateQueries({ queryKey: projectKeys.forProjectCertificates(projectSlug) }); + + queryClient.invalidateQueries({ + queryKey: ["certificate-profiles"] + }); } }); }; diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index d4a62a710..696e72494 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -176,7 +176,7 @@ export type TCreateCertificateV3DTO = { profileId: string; pkiCollectionId?: string; friendlyName?: string; - commonName: string; + commonName?: string; organization?: string; organizationUnit?: string; locality?: string; @@ -195,7 +195,11 @@ export type TCreateCertificateV3DTO = { keyAlgorithm?: string; }; -export type TCreateCertificateV3Response = TCreateCertificateResponse; +export type TCreateCertificateV3Response = TCreateCertificateResponse & { + projectId: string; + profileName: string; + certificateId: string; +}; export type TOrderCertificateDTO = { projectSlug: string; diff --git a/frontend/src/hooks/api/certificates/mutations.tsx b/frontend/src/hooks/api/certificates/mutations.tsx index 77a3dab72..388295b0a 100644 --- a/frontend/src/hooks/api/certificates/mutations.tsx +++ b/frontend/src/hooks/api/certificates/mutations.tsx @@ -52,6 +52,10 @@ export const useRevokeCert = () => { queryClient.invalidateQueries({ queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates() }); + + queryClient.invalidateQueries({ + queryKey: ["certificate-profiles", "list"] + }); } }); }; diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx index 5bc606c45..a19a3a9c7 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/CaModal.tsx @@ -12,8 +12,7 @@ import { Modal, ModalContent, Select, - SelectItem, - Switch + SelectItem // DatePicker } from "@app/components/v2"; import { useProject } from "@app/context"; @@ -152,7 +151,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { type: CaType.INTERNAL, name: "", status: CaStatus.ACTIVE, - enableDirectIssuance: true, + enableDirectIssuance: false, configuration: { type: InternalCaType.ROOT, organization: "", @@ -456,23 +455,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> - { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Enable Direct Issuance

-
-
- ); - }} - />
+
+ + )} + /> + )} + + {shouldShowSanSection && ( + ( + +
+ {value.map((san, index) => ( +
- Common Name - - { - const newValue = [...value]; - newValue[index] = { ...attr, value: e.target.value }; - onChange(newValue); - }} - placeholder="example.com" - className="flex-1" - /> - {value.length > 1 && ( + + { + const newValue = [...value]; + newValue[index] = { ...san, value: e.target.value }; + onChange(newValue); + }} + placeholder={getSanPlaceholder(san.type)} + className="flex-1" + /> { @@ -567,98 +713,30 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } > - )} -
- ))} - -
-
- )} - /> - - ( - -
- {value.map((san, index) => ( -
+ ))} +
- ))} - -
-
- )} - /> + Add SAN + + + + )} + /> + )} - - Key Usages - -
- {filteredKeyUsages.map(({ label, value }) => { - const isRequired = requiredKeyUsages.includes(value); - return ( - ( -
- { - if (!isRequired) { - field.onChange(checked); - } - }} - isDisabled={isRequired} - /> -
- 0 && ( + + Key Usages + +
+ {filteredKeyUsages.map(({ label, value }) => { + const isRequired = requiredKeyUsages.includes(value); + return ( + ( +
+ { + if (!isRequired) { + field.onChange(checked); + } + }} + isDisabled={isRequired} /> - {isRequired && (Required)} +
+ + {isRequired && (Required)} +
-
- )} - /> - ); - })} -
- - + )} + /> + ); + })} +
+ + + )} - - Extended Key Usages - -
- {filteredExtendedKeyUsages.map(({ label, value }) => { - const isRequired = requiredExtendedKeyUsages.includes(value); - return ( - ( -
- { - if (!isRequired) { - field.onChange(checked); - } - }} - isDisabled={isRequired} - /> -
- 0 && ( + + Extended Key Usages + +
+ {filteredExtendedKeyUsages.map(({ label, value }) => { + const isRequired = requiredExtendedKeyUsages.includes(value); + return ( + ( +
+ { + if (!isRequired) { + field.onChange(checked); + } + }} + isDisabled={isRequired} /> - {isRequired && (Required)} +
+ + {isRequired && (Required)} +
-
- )} - /> - ); - })} -
- - + )} + /> + ); + })} +
+ + + )} )} diff --git a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx index d2dfc414d..44deedf7d 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/PoliciesPage.tsx @@ -52,7 +52,7 @@ export const PoliciesPage = () => { /> setActiveTab(value as TabSections)}> - +
Certificate Profiles Certificate Templates 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 884e663de..55262439a 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateProfilesTab/CreateProfileModal.tsx @@ -27,8 +27,20 @@ import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplat const createSchema = z .object({ - slug: z.string().trim().min(1, "Profile slug is required"), - description: z.string().optional(), + slug: z + .string() + .trim() + .min(1, "Profile slug is required") + .max(255, "Profile slug must be less than 255 characters") + .regex( + /^[a-zA-Z0-9-_]+$/, + "Profile slug must contain only letters, numbers, hyphens, and underscores" + ), + description: z + .string() + .trim() + .max(1000, "Description must be less than 1000 characters") + .optional(), enrollmentType: z.enum(["api", "est"]), certificateAuthorityId: z.string().min(1, "Certificate Authority is required"), certificateTemplateId: z.string().min(1, "Certificate Template is required"), @@ -36,8 +48,19 @@ const createSchema = z .object({ disableBootstrapCaValidation: z.boolean().optional(), passphrase: z.string().min(1, "EST passphrase is required"), - caChain: z.string().min(1, "EST CA chain is required") + caChain: z.string().min(1, "EST CA chain is required").optional() }) + .refine( + (data) => { + if (!data.disableBootstrapCaValidation && !data.caChain) { + return false; + } + return true; + }, + { + message: "EST CA chain is required" + } + ) .optional(), apiConfig: z .object({ @@ -63,8 +86,20 @@ const createSchema = z const editSchema = z .object({ - slug: z.string().trim().min(1, "Profile slug is required"), - description: z.string().optional(), + slug: z + .string() + .trim() + .min(1, "Profile slug is required") + .max(255, "Profile slug must be less than 255 characters") + .regex( + /^[a-zA-Z0-9-_]+$/, + "Profile slug must contain only letters, numbers, hyphens, and underscores" + ), + description: z + .string() + .trim() + .max(1000, "Description must be less than 1000 characters") + .optional(), enrollmentType: z.enum(["api", "est"]), certificateAuthorityId: z.string().optional(), certificateTemplateId: z.string().optional(), @@ -136,7 +171,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } estConfig: { disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false, passphrase: "", - caChain: "" + caChain: undefined }, apiConfig: { autoRenew: profile.apiConfig?.autoRenew || false, @@ -193,7 +228,11 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } }; if (data.enrollmentType === "est" && data.estConfig) { - createData.estConfig = data.estConfig; + createData.estConfig = { + passphrase: data.estConfig.passphrase, + caChain: data.estConfig.caChain || "", + disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation + }; } else if (data.enrollmentType === "api" && data.apiConfig) { createData.apiConfig = data.apiConfig; } @@ -241,7 +280,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" } name="slug" render={({ field, fieldState: { error } }) => (
-
{profile.slug}
+
{profile.slug}
{profile.description && ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx index 4abe390a7..909a2e8ac 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/CreateTemplateModal.tsx @@ -362,8 +362,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" return { name: data.name, description: data.description, - subject: subject.length > 0 ? subject : undefined, - sans: sans.length > 0 ? sans : undefined, + subject, + sans, keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined, extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined, algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined, @@ -614,7 +614,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" required /> - {watchedAttributes.length > 1 && ( + {watchedAttributes.length > 0 && ( {SAN_TYPE_OPTIONS.map((type) => ( @@ -719,7 +719,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" required /> - {watchedSans.length > 1 && ( + {watchedSans.length > 0 && (

- Signature Algorithms + Allowed Signature Algorithms

-

Key Algorithms

+

+ Allowed Key Algorithms +

{ >
-
{template.name}
+
{template.name}
{template.description && ( diff --git a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts index b4f6fc9eb..861f4e5d9 100644 --- a/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts +++ b/frontend/src/pages/cert-manager/PoliciesPage/components/CertificateTemplatesV2Tab/shared/schemas.ts @@ -50,8 +50,20 @@ export const uiKeyAlgorithmSchema = z.object({ }); export const templateSchema = z.object({ - name: z.string().trim().min(1, "Template name is required"), - description: z.string().optional(), + name: z + .string() + .trim() + .min(1, "Template name is required") + .max(255, "Template name must be less than 255 characters") + .regex( + /^[a-zA-Z0-9-_]+$/, + "Template name must contain only letters, numbers, hyphens, and underscores" + ), + description: z + .string() + .trim() + .max(1000, "Description must be less than 1000 characters") + .optional(), attributes: z.array(uiAttributeSchema).optional(), subjectAlternativeNames: z.array(uiSanSchema).optional(), keyUsages: uiKeyUsagesSchema.optional(), @@ -86,8 +98,20 @@ export const apiSanSchema = z }); export const apiTemplateSchema = z.object({ - name: z.string().trim().min(1, "Template name is required"), - description: z.string().optional(), + name: z + .string() + .trim() + .min(1, "Template name is required") + .max(255, "Template name must be less than 255 characters") + .regex( + /^[a-zA-Z0-9-_]+$/, + "Template name must contain only letters, numbers, hyphens, and underscores" + ), + description: z + .string() + .trim() + .max(1000, "Description must be less than 1000 characters") + .optional(), subject: z.array(apiSubjectSchema).optional(), sans: z.array(apiSanSchema).optional(), keyUsages: z @@ -106,14 +130,15 @@ export const apiTemplateSchema = z.object({ .optional(), algorithms: z .object({ - signature: z.array(z.string()).optional(), - keyAlgorithm: z.array(z.string()).optional() + signature: z.array(z.string().trim().min(1, "Algorithm cannot be empty")).optional(), + keyAlgorithm: z.array(z.string().trim().min(1, "Algorithm cannot be empty")).optional() }) .optional(), validity: z .object({ max: z .string() + .trim() .regex(/^[1-9]\d*[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'") .optional() }) diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index ec5bfc62a..efd7b123b 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -393,12 +393,8 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), - [ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({ - inverted: z.boolean().optional(), - conditions: ConditionSchema - }) - .array() - .default([]), + [ProjectPermissionSub.CertificateProfiles]: + CertificateProfilePolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( [] ), @@ -479,7 +475,6 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.PkiSubscribers || subject === ProjectPermissionSub.CertificateTemplates || - subject === ProjectPermissionSub.CertificateProfiles || subject === ProjectPermissionSub.SecretSyncs || subject === ProjectPermissionSub.PkiSyncs || subject === ProjectPermissionSub.SecretEvents || @@ -1176,9 +1171,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ), [ProjectPermissionCertificateProfileActions.IssueCert]: action.includes( ProjectPermissionCertificateProfileActions.IssueCert - ), - conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [], - inverted + ) }); return; @@ -2195,6 +2188,10 @@ export const RoleTemplates: Record = { { subject: ProjectPermissionSub.PkiSyncs, actions: [ProjectPermissionPkiSyncActions.Read] + }, + { + subject: ProjectPermissionSub.CertificateProfiles, + actions: [ProjectPermissionCertificateProfileActions.Read] } ] }, @@ -2226,6 +2223,10 @@ export const RoleTemplates: Record = { { subject: ProjectPermissionSub.PkiSyncs, actions: Object.values(ProjectPermissionPkiSyncActions) + }, + { + subject: ProjectPermissionSub.CertificateProfiles, + actions: Object.values(ProjectPermissionCertificateProfileActions) } ] },