mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 22:28:15 +00:00
PKI revamp: general improvements
This commit is contained in:
@@ -7,6 +7,7 @@ import { ApiDocsTags } from "@app/lib/api-docs";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
|
||||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -453,7 +454,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
offset: z.coerce.number().min(0).default(0),
|
offset: z.coerce.number().min(0).default(0),
|
||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
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()
|
search: z.string().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
commonName: validateTemplateRegexField,
|
commonName: validateTemplateRegexField.optional(),
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
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(),
|
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
@@ -162,8 +166,12 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
csr: z.string().trim().min(1).max(4096),
|
csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"),
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "TTL cannot be empty")
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional()
|
notAfter: validateCaDateField.optional()
|
||||||
})
|
})
|
||||||
@@ -234,11 +242,19 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.nativeEnum(ACMESANType),
|
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),
|
.min(1, "At least one subject alternative name must be provided"),
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
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(),
|
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
|
|||||||
+23
-8
@@ -32,6 +32,7 @@ import {
|
|||||||
CertExtendedKeyUsageOIDToName,
|
CertExtendedKeyUsageOIDToName,
|
||||||
CertKeyAlgorithm,
|
CertKeyAlgorithm,
|
||||||
CertKeyUsage,
|
CertKeyUsage,
|
||||||
|
CertSignatureAlgorithm,
|
||||||
CertStatus,
|
CertStatus,
|
||||||
TAltNameMapping
|
TAltNameMapping
|
||||||
} from "../../certificate/certificate-types";
|
} from "../../certificate/certificate-types";
|
||||||
@@ -1289,12 +1290,19 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||||
const requestedKeyType = signatureAlgorithm.split("-")[0];
|
const requestedKeyType = signatureAlgorithm.split("-")[0];
|
||||||
|
|
||||||
const isRsaCa = caKeyAlgorithm.startsWith("RSA");
|
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
|
||||||
const isEcdsaCa = caKeyAlgorithm.startsWith("EC");
|
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
|
// 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({
|
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.`
|
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 caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||||
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
|
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
|
||||||
|
|
||||||
const isRsaCa = caKeyAlgorithm.startsWith("RSA");
|
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
|
||||||
const isEcdsaCa = caKeyAlgorithm.startsWith("EC");
|
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
|
// 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({
|
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.`
|
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -559,7 +559,6 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
expect(result).toEqual(sampleProfile);
|
expect(result).toEqual(sampleProfile);
|
||||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||||
expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123");
|
|
||||||
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -573,18 +572,6 @@ describe("CertificateProfileService", () => {
|
|||||||
})
|
})
|
||||||
).rejects.toThrow(NotFoundError);
|
).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", () => {
|
describe("getProfileCertificates", () => {
|
||||||
@@ -841,23 +828,8 @@ describe("CertificateProfileService", () => {
|
|||||||
expect(result.enrollmentType).toBe(EnrollmentType.EST);
|
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.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);
|
(mockCertificateProfileDAL.deleteById as any).mockResolvedValue(sampleProfile);
|
||||||
|
|
||||||
const result = await service.deleteProfile({
|
const result = await service.deleteProfile({
|
||||||
@@ -866,7 +838,6 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual(sampleProfile);
|
expect(result).toEqual(sampleProfile);
|
||||||
expect(mockCertificateProfileDAL.isProfileInUse).toHaveBeenCalledWith("profile-123");
|
|
||||||
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
expect(mockCertificateProfileDAL.deleteById).toHaveBeenCalledWith("profile-123");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const certificateProfileServiceFactory = ({
|
|||||||
const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId);
|
const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId);
|
||||||
if (existingSlugProfile) {
|
if (existingSlugProfile) {
|
||||||
throw new ForbiddenRequestError({
|
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) {
|
if (conflictingProfile && conflictingProfile.id !== profileId) {
|
||||||
throw new ForbiddenRequestError({
|
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
|
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);
|
const deletedProfile = await certificateProfileDAL.deleteById(profileId);
|
||||||
if (!deletedProfile) {
|
if (!deletedProfile) {
|
||||||
throw new NotFoundError({ message: "Failed to delete certificate profile" });
|
throw new NotFoundError({ message: "Failed to delete certificate profile" });
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,9 +14,9 @@ const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
|||||||
const templateV2SubjectSchema = z
|
const templateV2SubjectSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: attributeTypeSchema,
|
type: attributeTypeSchema,
|
||||||
allowed: z.array(z.string()).optional(),
|
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||||
required: z.array(z.string()).optional(),
|
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||||
denied: z.array(z.string()).optional()
|
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -68,9 +69,9 @@ const templateV2ExtendedKeyUsagesSchema = z
|
|||||||
const templateV2SanSchema = z
|
const templateV2SanSchema = z
|
||||||
.object({
|
.object({
|
||||||
type: sanTypeSchema,
|
type: sanTypeSchema,
|
||||||
allowed: z.array(z.string()).optional(),
|
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||||
required: z.array(z.string()).optional(),
|
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
|
||||||
denied: z.array(z.string()).optional()
|
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -87,22 +88,33 @@ const templateV2SanSchema = z
|
|||||||
const templateV2ValiditySchema = z.object({
|
const templateV2ValiditySchema = z.object({
|
||||||
max: z
|
max: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^\d+[dhmy]$/, {
|
.regex(new RE2("^\\d+[dhmy]$"), {
|
||||||
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateV2AlgorithmsSchema = z.object({
|
const templateV2AlgorithmsSchema = z.object({
|
||||||
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
|
signature: z
|
||||||
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
|
.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({
|
export const certificateTemplateV2ResponseSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
projectId: z.string().uuid("Project ID must be valid"),
|
projectId: z.string().uuid("Project ID must be valid"),
|
||||||
name: z.string(),
|
name: z
|
||||||
description: z.string().nullable().optional(),
|
.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(),
|
subject: z.array(templateV2SubjectSchema).optional(),
|
||||||
sans: z.array(templateV2SanSchema).optional(),
|
sans: z.array(templateV2SanSchema).optional(),
|
||||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
@@ -114,26 +126,53 @@ export const certificateTemplateV2ResponseSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const certificateRequestSchema = z.object({
|
export const certificateRequestSchema = z.object({
|
||||||
commonName: z.string().optional(),
|
commonName: z
|
||||||
organization: z.string().optional(),
|
.string()
|
||||||
country: z.string().optional(),
|
.trim()
|
||||||
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
.min(1, "Common name cannot be empty")
|
||||||
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
.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
|
subjectAlternativeNames: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: sanTypeSchema,
|
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(),
|
.optional(),
|
||||||
validity: z
|
validity: z
|
||||||
.object({
|
.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(),
|
.optional(),
|
||||||
signatureAlgorithm: z.string().optional(),
|
signatureAlgorithm: z.string().trim().min(1, "Signature algorithm cannot be empty").optional(),
|
||||||
keyAlgorithm: z.string().optional()
|
keyAlgorithm: z.string().trim().min(1, "Key algorithm cannot be empty").optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const validateCertificateRequestSchema = z.object({
|
export const validateCertificateRequestSchema = z.object({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
|
|||||||
|
|
||||||
export enum CertStatus {
|
export enum CertStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
|
EXPIRED = "expired",
|
||||||
REVOKED = "revoked"
|
REVOKED = "revoked"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Create"
|
|
||||||
openapi: "POST /api/v1/pki/certificate-templates"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Delete"
|
|
||||||
openapi: "DELETE /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Get by ID"
|
|
||||||
openapi: "GET /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Update"
|
|
||||||
openapi: "PATCH /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Create"
|
|
||||||
openapi: "POST /api/v1/pki/subscribers"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Delete"
|
|
||||||
openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Retrieve latest certificate bundle"
|
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/latest-certificate-bundle"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Issue Certificate"
|
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/issue-certificate"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "List Certificates"
|
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/certificates"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Order Certificate"
|
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/order-certificate"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Retrieve"
|
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Sign Certificate"
|
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/sign-certificate"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Update"
|
|
||||||
openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}"
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**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.
|
|
||||||
</Warning>
|
|
||||||
+1
-25
@@ -2451,20 +2451,6 @@
|
|||||||
{
|
{
|
||||||
"group": "Infisical PKI",
|
"group": "Infisical PKI",
|
||||||
"pages": [
|
"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",
|
"group": "Certificate Authorities",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -2525,17 +2511,7 @@
|
|||||||
"api-reference/endpoints/certificate-templates-v2/create",
|
"api-reference/endpoints/certificate-templates-v2/create",
|
||||||
"api-reference/endpoints/certificate-templates-v2/update",
|
"api-reference/endpoints/certificate-templates-v2/update",
|
||||||
"api-reference/endpoints/certificate-templates-v2/get-by-id",
|
"api-reference/endpoints/certificate-templates-v2/get-by-id",
|
||||||
"api-reference/endpoints/certificate-templates-v2/delete",
|
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ export enum ProjectPermissionCertificateProfileActions {
|
|||||||
Create = "create",
|
Create = "create",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
Delete = "delete",
|
Delete = "delete",
|
||||||
IssueCert = "issue-cert",
|
IssueCert = "issue-cert"
|
||||||
ListCerts = "list-certs"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretRotationActions {
|
export enum ProjectPermissionSecretRotationActions {
|
||||||
@@ -226,7 +225,6 @@ export type ConditionalProjectPermissionSubject =
|
|||||||
| ProjectPermissionSub.SshHosts
|
| ProjectPermissionSub.SshHosts
|
||||||
| ProjectPermissionSub.PkiSubscribers
|
| ProjectPermissionSub.PkiSubscribers
|
||||||
| ProjectPermissionSub.CertificateTemplates
|
| ProjectPermissionSub.CertificateTemplates
|
||||||
| ProjectPermissionSub.CertificateProfiles
|
|
||||||
| ProjectPermissionSub.SecretFolders
|
| ProjectPermissionSub.SecretFolders
|
||||||
| ProjectPermissionSub.SecretImports
|
| ProjectPermissionSub.SecretImports
|
||||||
| ProjectPermissionSub.SecretRotation
|
| ProjectPermissionSub.SecretRotation
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ export const useCreateCertificateV3 = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["certificate-profiles"]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export type TCreateCertificateV3DTO = {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
pkiCollectionId?: string;
|
pkiCollectionId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
organizationUnit?: string;
|
organizationUnit?: string;
|
||||||
locality?: string;
|
locality?: string;
|
||||||
@@ -195,7 +195,11 @@ export type TCreateCertificateV3DTO = {
|
|||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateCertificateV3Response = TCreateCertificateResponse;
|
export type TCreateCertificateV3Response = TCreateCertificateResponse & {
|
||||||
|
projectId: string;
|
||||||
|
profileName: string;
|
||||||
|
certificateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TOrderCertificateDTO = {
|
export type TOrderCertificateDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export const useRevokeCert = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
|
queryKey: pkiSubscriberKeys.allPkiSubscriberCertificates()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["certificate-profiles", "list"]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem
|
||||||
Switch
|
|
||||||
// DatePicker
|
// DatePicker
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
@@ -152,7 +151,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
type: CaType.INTERNAL,
|
type: CaType.INTERNAL,
|
||||||
name: "",
|
name: "",
|
||||||
status: CaStatus.ACTIVE,
|
status: CaStatus.ACTIVE,
|
||||||
enableDirectIssuance: true,
|
enableDirectIssuance: false,
|
||||||
configuration: {
|
configuration: {
|
||||||
type: InternalCaType.ROOT,
|
type: InternalCaType.ROOT,
|
||||||
organization: "",
|
organization: "",
|
||||||
@@ -456,23 +455,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="enableDirectIssuance"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
|
||||||
<Switch
|
|
||||||
id="enable-direct-issuance"
|
|
||||||
onCheckedChange={(value) => field.onChange(value)}
|
|
||||||
isChecked={field.value}
|
|
||||||
>
|
|
||||||
<p className="w-full">Enable Direct Issuance</p>
|
|
||||||
</Switch>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
+124
-42
@@ -27,10 +27,8 @@ import { useProject } from "@app/context";
|
|||||||
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
|
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
|
||||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||||
import {
|
import {
|
||||||
certKeyAlgorithms,
|
|
||||||
EXTENDED_KEY_USAGES_OPTIONS,
|
EXTENDED_KEY_USAGES_OPTIONS,
|
||||||
KEY_USAGES_OPTIONS,
|
KEY_USAGES_OPTIONS
|
||||||
SIGNATURE_ALGORITHMS_OPTIONS
|
|
||||||
} from "@app/hooks/api/certificates/constants";
|
} from "@app/hooks/api/certificates/constants";
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
@@ -42,16 +40,26 @@ import {
|
|||||||
|
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
|
||||||
const schema = z.object({
|
const createSchema = (shouldShowSubjectSection: boolean) => {
|
||||||
|
return z.object({
|
||||||
profileId: z.string().min(1, "Profile is required"),
|
profileId: z.string().min(1, "Profile is required"),
|
||||||
subjectAttributes: z
|
subjectAttributes: shouldShowSubjectSection
|
||||||
|
? z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["common_name"]),
|
type: z.enum(["common_name"]),
|
||||||
value: z.string().min(1, "Value is required")
|
value: z.string().min(1, "Value is required")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1, "At least one subject attribute is required"),
|
.min(1, "At least one subject attribute is required")
|
||||||
|
: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.enum(["common_name"]),
|
||||||
|
value: z.string().min(1, "Value is required")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
subjectAltNames: z
|
subjectAltNames: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -61,8 +69,8 @@ const schema = z.object({
|
|||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
ttl: z.string().trim().min(1, "TTL is required"),
|
ttl: z.string().trim().min(1, "TTL is required"),
|
||||||
signatureAlgorithm: z.string().optional(),
|
signatureAlgorithm: z.string().min(1, "Signature algorithm is required"),
|
||||||
keyAlgorithm: z.string().optional(),
|
keyAlgorithm: z.string().min(1, "Key algorithm is required"),
|
||||||
keyUsages: z.object({
|
keyUsages: z.object({
|
||||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||||
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
@@ -82,9 +90,10 @@ const schema = z.object({
|
|||||||
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||||
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<ReturnType<typeof createSchema>>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["certificateIssuance"]>;
|
popUp: UsePopUpState<["certificateIssuance"]>;
|
||||||
@@ -110,6 +119,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
const [requiredExtendedKeyUsages, setRequiredExtendedKeyUsages] = useState<string[]>([]);
|
const [requiredExtendedKeyUsages, setRequiredExtendedKeyUsages] = useState<string[]>([]);
|
||||||
const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState<string[]>([]);
|
const [allowedSignatureAlgorithms, setAllowedSignatureAlgorithms] = useState<string[]>([]);
|
||||||
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
|
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
|
||||||
|
const [allowedSanTypes, setAllowedSanTypes] = useState<string[]>(["dns", "ip", "email", "uri"]);
|
||||||
|
const [shouldShowSanSection, setShouldShowSanSection] = useState<boolean>(true);
|
||||||
|
const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState<boolean>(true);
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
const inputSerialNumber =
|
const inputSerialNumber =
|
||||||
@@ -133,10 +145,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(createSchema(shouldShowSubjectSection)),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
profileId: profileId || "",
|
profileId: profileId || "",
|
||||||
subjectAttributes: [{ type: "common_name", value: "" }],
|
subjectAttributes: shouldShowSubjectSection
|
||||||
|
? [{ type: "common_name", value: "" }]
|
||||||
|
: undefined,
|
||||||
subjectAltNames: [],
|
subjectAltNames: [],
|
||||||
ttl: "30d",
|
ttl: "30d",
|
||||||
signatureAlgorithm: "",
|
signatureAlgorithm: "",
|
||||||
@@ -154,6 +168,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
setRequiredExtendedKeyUsages([]);
|
setRequiredExtendedKeyUsages([]);
|
||||||
setAllowedSignatureAlgorithms([]);
|
setAllowedSignatureAlgorithms([]);
|
||||||
setAllowedKeyAlgorithms([]);
|
setAllowedKeyAlgorithms([]);
|
||||||
|
setAllowedSanTypes(["dns", "ip", "email", "uri"]);
|
||||||
|
setShouldShowSanSection(true);
|
||||||
|
setShouldShowSubjectSection(true);
|
||||||
reset();
|
reset();
|
||||||
}, [reset]);
|
}, [reset]);
|
||||||
|
|
||||||
@@ -168,21 +185,31 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filteredKeyUsages = useMemo(() => {
|
const filteredKeyUsages = useMemo(() => {
|
||||||
if (allowedKeyUsages.length === 0) return KEY_USAGES_OPTIONS;
|
|
||||||
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
|
return KEY_USAGES_OPTIONS.filter(({ value }) => allowedKeyUsages.includes(value));
|
||||||
}, [allowedKeyUsages]);
|
}, [allowedKeyUsages]);
|
||||||
|
|
||||||
const filteredExtendedKeyUsages = useMemo(() => {
|
const filteredExtendedKeyUsages = useMemo(() => {
|
||||||
if (allowedExtendedKeyUsages.length === 0) return EXTENDED_KEY_USAGES_OPTIONS;
|
|
||||||
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
return EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) =>
|
||||||
allowedExtendedKeyUsages.includes(value)
|
allowedExtendedKeyUsages.includes(value)
|
||||||
);
|
);
|
||||||
}, [allowedExtendedKeyUsages]);
|
}, [allowedExtendedKeyUsages]);
|
||||||
|
|
||||||
const availableSignatureAlgorithms = useMemo(() => {
|
const mapBackendSanTypeToFrontend = (backendType: string): string => {
|
||||||
if (allowedSignatureAlgorithms.length === 0) {
|
switch (backendType) {
|
||||||
return SIGNATURE_ALGORITHMS_OPTIONS;
|
case "dns_name":
|
||||||
|
return "dns";
|
||||||
|
case "ip_address":
|
||||||
|
return "ip";
|
||||||
|
case "email":
|
||||||
|
return "email";
|
||||||
|
case "uri":
|
||||||
|
return "uri";
|
||||||
|
default:
|
||||||
|
return backendType;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableSignatureAlgorithms = useMemo(() => {
|
||||||
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
return allowedSignatureAlgorithms.map((templateAlgorithm) => {
|
||||||
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
const apiAlgorithm = mapTemplateSignatureAlgorithmToApi(templateAlgorithm);
|
||||||
return {
|
return {
|
||||||
@@ -193,9 +220,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}, [allowedSignatureAlgorithms]);
|
}, [allowedSignatureAlgorithms]);
|
||||||
|
|
||||||
const availableKeyAlgorithms = useMemo(() => {
|
const availableKeyAlgorithms = useMemo(() => {
|
||||||
if (allowedKeyAlgorithms.length === 0) {
|
|
||||||
return certKeyAlgorithms;
|
|
||||||
}
|
|
||||||
return allowedKeyAlgorithms.map((templateAlgorithm) => {
|
return allowedKeyAlgorithms.map((templateAlgorithm) => {
|
||||||
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
const apiAlgorithm = mapTemplateKeyAlgorithmToApi(templateAlgorithm);
|
||||||
return {
|
return {
|
||||||
@@ -247,6 +271,33 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
setRequiredKeyUsages(templateData.keyUsages?.required || []);
|
setRequiredKeyUsages(templateData.keyUsages?.required || []);
|
||||||
setRequiredExtendedKeyUsages(templateData.extendedKeyUsages?.required || []);
|
setRequiredExtendedKeyUsages(templateData.extendedKeyUsages?.required || []);
|
||||||
|
|
||||||
|
if (templateData.sans && templateData.sans.length > 0) {
|
||||||
|
const sanTypes: string[] = [];
|
||||||
|
templateData.sans.forEach((sanPolicy) => {
|
||||||
|
const frontendType = mapBackendSanTypeToFrontend(sanPolicy.type);
|
||||||
|
if (!sanTypes.includes(frontendType)) {
|
||||||
|
sanTypes.push(frontendType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAllowedSanTypes(sanTypes);
|
||||||
|
setShouldShowSanSection(true);
|
||||||
|
} else {
|
||||||
|
setAllowedSanTypes([]);
|
||||||
|
setShouldShowSanSection(false);
|
||||||
|
setValue("subjectAltNames", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateData.subject && templateData.subject.length > 0) {
|
||||||
|
setShouldShowSubjectSection(true);
|
||||||
|
const currentSubjectAttrs = watch("subjectAttributes");
|
||||||
|
if (!currentSubjectAttrs || currentSubjectAttrs.length === 0) {
|
||||||
|
setValue("subjectAttributes", [{ type: "common_name", value: "" }]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setShouldShowSubjectSection(false);
|
||||||
|
setValue("subjectAttributes", undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const initialKeyUsages: Record<string, boolean> = {};
|
const initialKeyUsages: Record<string, boolean> = {};
|
||||||
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
const initialExtendedKeyUsages: Record<string, boolean> = {};
|
||||||
|
|
||||||
@@ -261,7 +312,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
setValue("keyUsages", initialKeyUsages);
|
setValue("keyUsages", initialKeyUsages);
|
||||||
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
setValue("extendedKeyUsages", initialExtendedKeyUsages);
|
||||||
}
|
}
|
||||||
}, [templateData, selectedProfile, setValue, popUp?.certificateIssuance?.isOpen]);
|
}, [templateData, selectedProfile, setValue, watch, popUp?.certificateIssuance?.isOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cert) {
|
if (cert) {
|
||||||
@@ -299,22 +350,19 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
}, [popUp?.certificateIssuance?.isOpen, profileId, cert, setValue]);
|
||||||
|
|
||||||
const getAttributeValue = useCallback(
|
const getAttributeValue = useCallback(
|
||||||
(subjectAttributes: typeof schema._type.subjectAttributes, type: string) => {
|
(subjectAttributes: FormData["subjectAttributes"], type: string) => {
|
||||||
const foundAttr = subjectAttributes.find((attr) => attr.type === type);
|
const foundAttr = subjectAttributes?.find((attr) => attr.type === type);
|
||||||
return foundAttr?.value || "";
|
return foundAttr?.value || "";
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatSubjectAltNames = useCallback(
|
const formatSubjectAltNames = useCallback((subjectAltNames: FormData["subjectAltNames"]) => {
|
||||||
(subjectAltNames: typeof schema._type.subjectAltNames) => {
|
|
||||||
return subjectAltNames
|
return subjectAltNames
|
||||||
.filter((san) => san.value.trim())
|
.filter((san) => san.value.trim())
|
||||||
.map((san) => san.value.trim())
|
.map((san) => san.value.trim())
|
||||||
.join(", ");
|
.join(", ");
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
|
const filterUsages = useCallback(<T extends Record<string, boolean>>(usages: T) => {
|
||||||
return Object.entries(usages)
|
return Object.entries(usages)
|
||||||
@@ -350,7 +398,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonName = getAttributeValue(subjectAttributes, "common_name");
|
let commonName = "";
|
||||||
|
if (shouldShowSubjectSection && subjectAttributes && subjectAttributes.length > 0) {
|
||||||
|
commonName = getAttributeValue(subjectAttributes, "common_name");
|
||||||
if (!commonName.trim()) {
|
if (!commonName.trim()) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Common name is required.",
|
text: "Common name is required.",
|
||||||
@@ -358,20 +408,30 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate(
|
const certificateRequest: any = {
|
||||||
{
|
|
||||||
profileId: formProfileId,
|
profileId: formProfileId,
|
||||||
projectSlug: currentProject.slug,
|
projectSlug: currentProject.slug,
|
||||||
commonName,
|
|
||||||
subjectAltNames: formatSubjectAltNames(subjectAltNames),
|
|
||||||
ttl,
|
ttl,
|
||||||
signatureAlgorithm,
|
signatureAlgorithm,
|
||||||
keyAlgorithm,
|
keyAlgorithm,
|
||||||
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
keyUsages: filterUsages(keyUsages) as CertKeyUsage[],
|
||||||
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
extendedKeyUsages: filterUsages(extendedKeyUsages) as CertExtendedKeyUsage[]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldShowSubjectSection && commonName) {
|
||||||
|
certificateRequest.commonName = commonName;
|
||||||
}
|
}
|
||||||
);
|
if (shouldShowSanSection && subjectAltNames && subjectAltNames.length > 0) {
|
||||||
|
const formattedSans = formatSubjectAltNames(subjectAltNames);
|
||||||
|
if (formattedSans) {
|
||||||
|
certificateRequest.subjectAltNames = formattedSans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serialNumber, certificate, certificateChain, privateKey } =
|
||||||
|
await createCertificate(certificateRequest);
|
||||||
|
|
||||||
setCertificateDetails({
|
setCertificateDetails({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
@@ -399,7 +459,8 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
[
|
[
|
||||||
currentProject?.slug,
|
currentProject?.slug,
|
||||||
createCertificate,
|
createCertificate,
|
||||||
reset,
|
shouldShowSubjectSection,
|
||||||
|
shouldShowSanSection,
|
||||||
getAttributeValue,
|
getAttributeValue,
|
||||||
formatSubjectAltNames,
|
formatSubjectAltNames,
|
||||||
filterUsages
|
filterUsages
|
||||||
@@ -517,6 +578,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
|
|
||||||
{(selectedProfile || profileId) && (
|
{(selectedProfile || profileId) && (
|
||||||
<>
|
<>
|
||||||
|
{shouldShowSubjectSection && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="subjectAttributes"
|
name="subjectAttributes"
|
||||||
@@ -528,13 +590,13 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{value.map((attr, index) => (
|
{(value || []).map((attr, index) => (
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
|
<div key={`subject-attr-${index}`} className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={attr.type}
|
value={attr.type}
|
||||||
onValueChange={(newType) => {
|
onValueChange={(newType) => {
|
||||||
const newValue = [...value];
|
const newValue = [...(value || [])];
|
||||||
newValue[index] = {
|
newValue[index] = {
|
||||||
...attr,
|
...attr,
|
||||||
type: newType as typeof attr.type
|
type: newType as typeof attr.type
|
||||||
@@ -548,20 +610,20 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
<Input
|
<Input
|
||||||
value={attr.value}
|
value={attr.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = [...value];
|
const newValue = [...(value || [])];
|
||||||
newValue[index] = { ...attr, value: e.target.value };
|
newValue[index] = { ...attr, value: e.target.value };
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}}
|
}}
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
{value.length > 1 && (
|
{(value || []).length > 1 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="Remove Subject Attribute"
|
ariaLabel="Remove Subject Attribute"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newValue = value.filter((_, i) => i !== index);
|
const newValue = (value || []).filter((_, i) => i !== index);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -576,7 +638,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
size="xs"
|
size="xs"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange([...value, { type: "common_name", value: "" }]);
|
onChange([...(value || []), { type: "common_name", value: "" }]);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
@@ -586,7 +648,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldShowSanSection && (
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="subjectAltNames"
|
name="subjectAltNames"
|
||||||
@@ -615,10 +679,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
}}
|
}}
|
||||||
className="w-24"
|
className="w-24"
|
||||||
>
|
>
|
||||||
|
{allowedSanTypes.includes("dns") && (
|
||||||
<SelectItem value="dns">DNS</SelectItem>
|
<SelectItem value="dns">DNS</SelectItem>
|
||||||
|
)}
|
||||||
|
{allowedSanTypes.includes("ip") && (
|
||||||
<SelectItem value="ip">IP</SelectItem>
|
<SelectItem value="ip">IP</SelectItem>
|
||||||
|
)}
|
||||||
|
{allowedSanTypes.includes("email") && (
|
||||||
<SelectItem value="email">Email</SelectItem>
|
<SelectItem value="email">Email</SelectItem>
|
||||||
|
)}
|
||||||
|
{allowedSanTypes.includes("uri") && (
|
||||||
<SelectItem value="uri">URI</SelectItem>
|
<SelectItem value="uri">URI</SelectItem>
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
value={san.value}
|
value={san.value}
|
||||||
@@ -649,7 +721,12 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
size="xs"
|
size="xs"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange([...value, { type: "dns", value: "" }]);
|
const defaultType =
|
||||||
|
allowedSanTypes.length > 0 ? allowedSanTypes[0] : "dns";
|
||||||
|
onChange([
|
||||||
|
...value,
|
||||||
|
{ type: defaultType as "dns" | "ip" | "email" | "uri", value: "" }
|
||||||
|
]);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
@@ -659,6 +736,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -744,6 +822,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
{filteredKeyUsages.length > 0 && (
|
||||||
<AccordionItem value="key-usages">
|
<AccordionItem value="key-usages">
|
||||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
@@ -783,7 +862,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredExtendedKeyUsages.length > 0 && (
|
||||||
<AccordionItem value="extended-key-usages">
|
<AccordionItem value="extended-key-usages">
|
||||||
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
@@ -823,6 +904,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
|
|||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
)}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const PoliciesPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
|
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
|
||||||
<TabList className="mb-6 w-full">
|
<TabList className="w-full">
|
||||||
<div className="flex w-full border-b border-mineshaft-600">
|
<div className="flex w-full border-b border-mineshaft-600">
|
||||||
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
|
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
|
||||||
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
|
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
|
||||||
|
|||||||
+47
-8
@@ -27,8 +27,20 @@ import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplat
|
|||||||
|
|
||||||
const createSchema = z
|
const createSchema = z
|
||||||
.object({
|
.object({
|
||||||
slug: z.string().trim().min(1, "Profile slug is required"),
|
slug: z
|
||||||
description: z.string().optional(),
|
.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"]),
|
enrollmentType: z.enum(["api", "est"]),
|
||||||
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
|
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
|
||||||
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
||||||
@@ -36,8 +48,19 @@ const createSchema = z
|
|||||||
.object({
|
.object({
|
||||||
disableBootstrapCaValidation: z.boolean().optional(),
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
passphrase: z.string().min(1, "EST passphrase is required"),
|
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(),
|
.optional(),
|
||||||
apiConfig: z
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
@@ -63,8 +86,20 @@ const createSchema = z
|
|||||||
|
|
||||||
const editSchema = z
|
const editSchema = z
|
||||||
.object({
|
.object({
|
||||||
slug: z.string().trim().min(1, "Profile slug is required"),
|
slug: z
|
||||||
description: z.string().optional(),
|
.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"]),
|
enrollmentType: z.enum(["api", "est"]),
|
||||||
certificateAuthorityId: z.string().optional(),
|
certificateAuthorityId: z.string().optional(),
|
||||||
certificateTemplateId: z.string().optional(),
|
certificateTemplateId: z.string().optional(),
|
||||||
@@ -136,7 +171,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
estConfig: {
|
estConfig: {
|
||||||
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
passphrase: "",
|
passphrase: "",
|
||||||
caChain: ""
|
caChain: undefined
|
||||||
},
|
},
|
||||||
apiConfig: {
|
apiConfig: {
|
||||||
autoRenew: profile.apiConfig?.autoRenew || false,
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
@@ -193,7 +228,11 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
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) {
|
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||||
createData.apiConfig = data.apiConfig;
|
createData.apiConfig = data.apiConfig;
|
||||||
}
|
}
|
||||||
@@ -241,7 +280,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
name="slug"
|
name="slug"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Profile Slug"
|
label="Name"
|
||||||
isRequired
|
isRequired
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
|||||||
+1
-1
@@ -150,7 +150,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
<Tr key={profile.id} className="h-10 transition-colors duration-100 hover:bg-mineshaft-700">
|
<Tr key={profile.id} className="h-10 transition-colors duration-100 hover:bg-mineshaft-700">
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium text-mineshaft-100">{profile.slug}</div>
|
<div className="text-mineshaft-300">{profile.slug}</div>
|
||||||
{profile.description && (
|
{profile.description && (
|
||||||
<Tooltip content={profile.description}>
|
<Tooltip content={profile.description}>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
|||||||
+9
-7
@@ -362,8 +362,8 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
subject: subject.length > 0 ? subject : undefined,
|
subject,
|
||||||
sans: sans.length > 0 ? sans : undefined,
|
sans,
|
||||||
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
|
keyUsages: Object.keys(keyUsages).length > 0 ? keyUsages : undefined,
|
||||||
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
|
extendedKeyUsages: Object.keys(extendedKeyUsages).length > 0 ? extendedKeyUsages : undefined,
|
||||||
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
|
algorithms: Object.keys(algorithms).length > 0 ? algorithms : undefined,
|
||||||
@@ -614,7 +614,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{watchedAttributes.length > 1 && (
|
{watchedAttributes.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="Remove Attribute"
|
ariaLabel="Remove Attribute"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -672,7 +672,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
};
|
};
|
||||||
setValue("subjectAlternativeNames", newSans);
|
setValue("subjectAlternativeNames", newSans);
|
||||||
}}
|
}}
|
||||||
className="w-24"
|
className="w-36"
|
||||||
>
|
>
|
||||||
{SAN_TYPE_OPTIONS.map((type) => (
|
{SAN_TYPE_OPTIONS.map((type) => (
|
||||||
<SelectItem key={type} value={type}>
|
<SelectItem key={type} value={type}>
|
||||||
@@ -719,7 +719,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{watchedSans.length > 1 && (
|
{watchedSans.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="Remove SAN"
|
ariaLabel="Remove SAN"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -755,7 +755,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">
|
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">
|
||||||
Signature Algorithms
|
Allowed Signature Algorithms
|
||||||
</h4>
|
</h4>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -799,7 +799,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">Key Algorithms</h4>
|
<h4 className="mb-3 text-sm font-medium text-mineshaft-200">
|
||||||
|
Allowed Key Algorithms
|
||||||
|
</h4>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="keyAlgorithm.allowedKeyTypes"
|
name="keyAlgorithm.allowedKeyTypes"
|
||||||
|
|||||||
+1
-1
@@ -89,7 +89,7 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
|||||||
>
|
>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium">{template.name}</div>
|
<div className="text-mineshaft-300">{template.name}</div>
|
||||||
{template.description && (
|
{template.description && (
|
||||||
<Tooltip content={template.description}>
|
<Tooltip content={template.description}>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
|||||||
+31
-6
@@ -50,8 +50,20 @@ export const uiKeyAlgorithmSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const templateSchema = z.object({
|
export const templateSchema = z.object({
|
||||||
name: z.string().trim().min(1, "Template name is required"),
|
name: z
|
||||||
description: z.string().optional(),
|
.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(),
|
attributes: z.array(uiAttributeSchema).optional(),
|
||||||
subjectAlternativeNames: z.array(uiSanSchema).optional(),
|
subjectAlternativeNames: z.array(uiSanSchema).optional(),
|
||||||
keyUsages: uiKeyUsagesSchema.optional(),
|
keyUsages: uiKeyUsagesSchema.optional(),
|
||||||
@@ -86,8 +98,20 @@ export const apiSanSchema = z
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const apiTemplateSchema = z.object({
|
export const apiTemplateSchema = z.object({
|
||||||
name: z.string().trim().min(1, "Template name is required"),
|
name: z
|
||||||
description: z.string().optional(),
|
.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(),
|
subject: z.array(apiSubjectSchema).optional(),
|
||||||
sans: z.array(apiSanSchema).optional(),
|
sans: z.array(apiSanSchema).optional(),
|
||||||
keyUsages: z
|
keyUsages: z
|
||||||
@@ -106,14 +130,15 @@ export const apiTemplateSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
algorithms: z
|
algorithms: z
|
||||||
.object({
|
.object({
|
||||||
signature: z.array(z.string()).optional(),
|
signature: z.array(z.string().trim().min(1, "Algorithm cannot be empty")).optional(),
|
||||||
keyAlgorithm: z.array(z.string()).optional()
|
keyAlgorithm: z.array(z.string().trim().min(1, "Algorithm cannot be empty")).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
validity: z
|
validity: z
|
||||||
.object({
|
.object({
|
||||||
max: z
|
max: z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
.regex(/^[1-9]\d*[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
|
.regex(/^[1-9]\d*[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'")
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-10
@@ -393,12 +393,8 @@ export const projectRoleFormSchema = z.object({
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
[ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({
|
[ProjectPermissionSub.CertificateProfiles]:
|
||||||
inverted: z.boolean().optional(),
|
CertificateProfilePolicyActionSchema.array().default([]),
|
||||||
conditions: ConditionSchema
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
.default([]),
|
|
||||||
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
@@ -479,7 +475,6 @@ export const isConditionalSubjects = (
|
|||||||
subject === ProjectPermissionSub.SecretRotation ||
|
subject === ProjectPermissionSub.SecretRotation ||
|
||||||
subject === ProjectPermissionSub.PkiSubscribers ||
|
subject === ProjectPermissionSub.PkiSubscribers ||
|
||||||
subject === ProjectPermissionSub.CertificateTemplates ||
|
subject === ProjectPermissionSub.CertificateTemplates ||
|
||||||
subject === ProjectPermissionSub.CertificateProfiles ||
|
|
||||||
subject === ProjectPermissionSub.SecretSyncs ||
|
subject === ProjectPermissionSub.SecretSyncs ||
|
||||||
subject === ProjectPermissionSub.PkiSyncs ||
|
subject === ProjectPermissionSub.PkiSyncs ||
|
||||||
subject === ProjectPermissionSub.SecretEvents ||
|
subject === ProjectPermissionSub.SecretEvents ||
|
||||||
@@ -1176,9 +1171,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
),
|
),
|
||||||
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
|
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
|
||||||
ProjectPermissionCertificateProfileActions.IssueCert
|
ProjectPermissionCertificateProfileActions.IssueCert
|
||||||
),
|
)
|
||||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
|
||||||
inverted
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -2195,6 +2188,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.PkiSyncs,
|
subject: ProjectPermissionSub.PkiSyncs,
|
||||||
actions: [ProjectPermissionPkiSyncActions.Read]
|
actions: [ProjectPermissionPkiSyncActions.Read]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.CertificateProfiles,
|
||||||
|
actions: [ProjectPermissionCertificateProfileActions.Read]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2226,6 +2223,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.PkiSyncs,
|
subject: ProjectPermissionSub.PkiSyncs,
|
||||||
actions: Object.values(ProjectPermissionPkiSyncActions)
|
actions: Object.values(ProjectPermissionPkiSyncActions)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.CertificateProfiles,
|
||||||
|
actions: Object.values(ProjectPermissionCertificateProfileActions)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user