mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 23:27:14 +00:00
Addressed PR comments
This commit is contained in:
@@ -8,7 +8,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.createTable(TableName.CertificateTemplateV2, (t) => {
|
await knex.schema.createTable(TableName.CertificateTemplateV2, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("projectId").notNullable();
|
t.string("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project);
|
||||||
|
|
||||||
t.string("slug").notNullable();
|
t.string("slug").notNullable();
|
||||||
t.string("description");
|
t.string("description");
|
||||||
@@ -60,7 +60,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.createTable(TableName.CertificateProfile, (t) => {
|
await knex.schema.createTable(TableName.CertificateProfile, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("projectId").notNullable();
|
t.string("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project);
|
||||||
|
|
||||||
t.uuid("caId").notNullable();
|
t.uuid("caId").notNullable();
|
||||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority);
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority);
|
||||||
|
|||||||
@@ -2569,19 +2569,41 @@ interface GetCertificateTemplateEstConfig {
|
|||||||
|
|
||||||
interface CreateCertificateTemplate {
|
interface CreateCertificateTemplate {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata:
|
||||||
certificateTemplateId: string;
|
| {
|
||||||
name: string;
|
certificateTemplateId: string;
|
||||||
projectId: string;
|
name: string;
|
||||||
};
|
projectId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
caId: string;
|
||||||
|
pkiCollectionId: string;
|
||||||
|
name: string;
|
||||||
|
commonName: string;
|
||||||
|
subjectAlternativeName: string;
|
||||||
|
ttl: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateCertificateTemplate {
|
interface UpdateCertificateTemplate {
|
||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata:
|
||||||
certificateTemplateId: string;
|
| {
|
||||||
name: string;
|
certificateTemplateId: string;
|
||||||
};
|
name: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
certificateTemplateId: string;
|
||||||
|
caId: string;
|
||||||
|
pkiCollectionId: string;
|
||||||
|
name: string;
|
||||||
|
commonName: string;
|
||||||
|
subjectAlternativeName: string;
|
||||||
|
ttl: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteCertificateTemplate {
|
interface DeleteCertificateTemplate {
|
||||||
@@ -2629,6 +2651,7 @@ interface DeleteCertificateProfile {
|
|||||||
type: EventType.DELETE_CERTIFICATE_PROFILE;
|
type: EventType.DELETE_CERTIFICATE_PROFILE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2636,6 +2659,7 @@ interface GetCertificateProfile {
|
|||||||
type: EventType.GET_CERTIFICATE_PROFILE;
|
type: EventType.GET_CERTIFICATE_PROFILE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2652,6 +2676,7 @@ interface IssueCertificateFromProfile {
|
|||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
|
profileName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2660,6 +2685,8 @@ interface SignCertificateFromProfile {
|
|||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
|
profileName: string;
|
||||||
|
commonName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2668,7 +2695,7 @@ interface OrderCertificateFromProfile {
|
|||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
subjectAlternativeNames: string[];
|
profileName: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1119,6 +1119,13 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
"When specified, only matching conditions will be allowed to access given resource."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.CertificateProfiles).describe("The entity this permission pertains to."),
|
||||||
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
...GeneralPermissionSchema
|
...GeneralPermissionSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CertificateProfilesSchema } from "@app/db/schemas";
|
import { CertificateProfilesSchema } from "@app/db/schemas";
|
||||||
@@ -6,12 +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 {
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
createCertificateProfileSchema,
|
|
||||||
deleteCertificateProfileSchema,
|
|
||||||
listCertificateProfilesSchema,
|
|
||||||
updateCertificateProfileSchema
|
|
||||||
} from "@app/services/certificate-profile/certificate-profile-schemas";
|
|
||||||
|
|
||||||
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -23,7 +19,57 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
body: createCertificateProfileSchema,
|
body: z
|
||||||
|
.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
caId: z.string().uuid(),
|
||||||
|
certificateTemplateId: z.string().uuid(),
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(255)
|
||||||
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
enrollmentType: z.nativeEnum(EnrollmentType),
|
||||||
|
estConfig: z
|
||||||
|
.object({
|
||||||
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
|
passphrase: z.string().min(1),
|
||||||
|
encryptedCaChain: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiConfig: z
|
||||||
|
.object({
|
||||||
|
autoRenew: z.boolean().default(false),
|
||||||
|
autoRenewDays: z.number().min(1).max(365).optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
if (!data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
if (!data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: CertificateProfilesSchema
|
certificateProfile: CertificateProfilesSchema
|
||||||
@@ -68,7 +114,13 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
querystring: listCertificateProfilesSchema.extend({
|
querystring: z.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
offset: z.coerce.number().min(0).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
|
search: z.string().optional(),
|
||||||
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
caId: z.string().uuid().optional(),
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||||
}),
|
}),
|
||||||
@@ -209,7 +261,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERTIFICATE_PROFILE,
|
type: EventType.GET_CERTIFICATE_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: certificateProfile.id
|
certificateProfileId: certificateProfile.id,
|
||||||
|
name: certificateProfile.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -266,7 +319,48 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().min(1)
|
id: z.string().min(1)
|
||||||
}),
|
}),
|
||||||
body: updateCertificateProfileSchema,
|
body: z
|
||||||
|
.object({
|
||||||
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(255)
|
||||||
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
||||||
|
.optional(),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
|
estConfig: z
|
||||||
|
.object({
|
||||||
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
|
passphrase: z.string().min(1),
|
||||||
|
encryptedCaChain: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiConfig: z
|
||||||
|
.object({
|
||||||
|
autoRenew: z.boolean().default(false),
|
||||||
|
autoRenewDays: z.number().min(1).max(365).optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
if (data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
if (data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: CertificateProfilesSchema
|
certificateProfile: CertificateProfilesSchema
|
||||||
@@ -309,7 +403,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
params: deleteCertificateProfileSchema,
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: CertificateProfilesSchema
|
certificateProfile: CertificateProfilesSchema
|
||||||
@@ -332,7 +428,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERTIFICATE_PROFILE,
|
type: EventType.DELETE_CERTIFICATE_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: certificateProfile.id
|
certificateProfileId: certificateProfile.id,
|
||||||
|
name: certificateProfile.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
caId: certificateTemplate.caId,
|
||||||
|
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||||
name: certificateTemplate.name,
|
name: certificateTemplate.name,
|
||||||
|
commonName: certificateTemplate.commonName,
|
||||||
|
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||||
|
ttl: certificateTemplate.ttl,
|
||||||
projectId: certificateTemplate.projectId
|
projectId: certificateTemplate.projectId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +186,12 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
name: certificateTemplate.name
|
name: certificateTemplate.name,
|
||||||
|
caId: certificateTemplate.caId,
|
||||||
|
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||||
|
commonName: certificateTemplate.commonName,
|
||||||
|
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||||
|
ttl: certificateTemplate.ttl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,18 +4,107 @@ import { CertificateTemplatesV2Schema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
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 {
|
import {
|
||||||
certificateRequestSchema,
|
CertDurationUnit,
|
||||||
createCertificateTemplateV2Schema,
|
CertExtendedKeyUsageType,
|
||||||
deleteCertificateTemplateV2Schema,
|
CertIncludeType,
|
||||||
getCertificateTemplateV2ByIdSchema,
|
CertKeyUsageType,
|
||||||
listCertificateTemplatesV2Schema,
|
CertSubjectAlternativeNameType,
|
||||||
updateCertificateTemplateV2Schema
|
CertSubjectAttributeType
|
||||||
} from "@app/services/certificate-template-v2/certificate-template-v2-schemas";
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
|
||||||
export const registerCertificateTemplatesV2Router = async (server: FastifyZodProvider) => {
|
export const registerCertificateTemplatesV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
const templateV2AttributeSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(CertSubjectAttributeType),
|
||||||
|
include: z.nativeEnum(CertIncludeType),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.type === CertSubjectAttributeType.COMMON_NAME && data.value && data.value.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const templateV2KeyUsagesSchema = z.object({
|
||||||
|
requiredUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
optionalUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateV2ExtendedKeyUsagesSchema = z.object({
|
||||||
|
requiredUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
optionalUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateV2SanSchema = z
|
||||||
|
.object({
|
||||||
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
|
include: z.nativeEnum(CertIncludeType),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.include === CertIncludeType.MANDATORY && (!data.value || data.value.length > 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Mandatory SANs can only have one value or no value (empty)"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const templateV2ValiditySchema = z.object({
|
||||||
|
maxDuration: z.object({
|
||||||
|
value: z.number().positive(),
|
||||||
|
unit: z.nativeEnum(CertDurationUnit)
|
||||||
|
}),
|
||||||
|
minDuration: z
|
||||||
|
.object({
|
||||||
|
value: z.number().positive(),
|
||||||
|
unit: z.nativeEnum(CertDurationUnit)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateV2SignatureAlgorithmSchema = z.object({
|
||||||
|
allowedAlgorithms: z.array(z.string()).min(1),
|
||||||
|
defaultAlgorithm: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateV2KeyAlgorithmSchema = z.object({
|
||||||
|
allowedKeyTypes: z.array(z.string()).min(1),
|
||||||
|
defaultKeyType: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -25,7 +114,36 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||||
body: createCertificateTemplateV2Schema,
|
body: z
|
||||||
|
.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
slug: slugSchema({ min: 1, max: 255 }),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||||
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
|
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||||
|
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
||||||
|
validity: templateV2ValiditySchema.optional(),
|
||||||
|
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
|
||||||
|
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const hasConstraints =
|
||||||
|
(data.attributes && data.attributes.length > 0) ||
|
||||||
|
(data.subjectAlternativeNames && data.subjectAlternativeNames.length > 0) ||
|
||||||
|
data.keyUsages ||
|
||||||
|
data.extendedKeyUsages ||
|
||||||
|
data.validity ||
|
||||||
|
data.signatureAlgorithm ||
|
||||||
|
data.keyAlgorithm;
|
||||||
|
return hasConstraints;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Certificate template must define at least one constraint (attributes, SANs, key usages, validity, or algorithms)"
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateTemplate: CertificateTemplatesV2Schema
|
certificateTemplate: CertificateTemplatesV2Schema
|
||||||
@@ -70,7 +188,12 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||||
querystring: listCertificateTemplatesV2Schema,
|
querystring: z.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
offset: z.coerce.number().min(0).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
|
search: z.string().optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateTemplates: CertificateTemplatesV2Schema.array(),
|
certificateTemplates: CertificateTemplatesV2Schema.array(),
|
||||||
@@ -112,7 +235,9 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||||
params: getCertificateTemplateV2ByIdSchema,
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateTemplate: CertificateTemplatesV2Schema
|
certificateTemplate: CertificateTemplatesV2Schema
|
||||||
@@ -154,8 +279,20 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||||
params: getCertificateTemplateV2ByIdSchema,
|
params: z.object({
|
||||||
body: updateCertificateTemplateV2Schema,
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: slugSchema({ min: 1, max: 255 }).optional(),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||||
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
|
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||||
|
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
||||||
|
validity: templateV2ValiditySchema.optional(),
|
||||||
|
signatureAlgorithm: templateV2SignatureAlgorithmSchema.optional(),
|
||||||
|
keyAlgorithm: templateV2KeyAlgorithmSchema.optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateTemplate: CertificateTemplatesV2Schema
|
certificateTemplate: CertificateTemplatesV2Schema
|
||||||
@@ -198,7 +335,9 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
tags: [ApiDocsTags.PkiCertificateTemplates],
|
||||||
params: deleteCertificateTemplateV2Schema,
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateTemplate: CertificateTemplatesV2Schema
|
certificateTemplate: CertificateTemplatesV2Schema
|
||||||
@@ -230,38 +369,4 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
return { certificateTemplate };
|
return { certificateTemplate };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/:id/validate",
|
|
||||||
config: {
|
|
||||||
rateLimit: readLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
hide: false,
|
|
||||||
tags: [ApiDocsTags.PkiCertificateTemplates],
|
|
||||||
params: getCertificateTemplateV2ByIdSchema,
|
|
||||||
body: z.object({
|
|
||||||
request: certificateRequestSchema
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
valid: z.boolean(),
|
|
||||||
errors: z.array(z.string()).optional()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const result = await server.services.certificateTemplateV2.validateCertificateRequest(
|
|
||||||
req.params.id,
|
|
||||||
req.body.request
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: result.isValid,
|
|
||||||
errors: result.errors.length > 0 ? result.errors : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ import { ms } from "@app/lib/ms";
|
|||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { 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 { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
import {
|
||||||
|
ACMESANType,
|
||||||
|
CertificateOrderStatus,
|
||||||
|
CertKeyAlgorithm,
|
||||||
|
CertSignatureAlgorithm
|
||||||
|
} from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
validateAltNamesField,
|
validateAltNamesField,
|
||||||
validateAndMapAltNameType,
|
validateAndMapAltNameType,
|
||||||
validateCaDateField
|
validateCaDateField
|
||||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType
|
||||||
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
@@ -25,18 +35,43 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
body: z.object({
|
body: z
|
||||||
profileId: z.string().uuid(),
|
.object({
|
||||||
commonName: validateTemplateRegexField,
|
profileId: z.string().uuid(),
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
commonName: validateTemplateRegexField,
|
||||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional(),
|
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||||
notBefore: validateCaDateField.optional(),
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
altNames: validateAltNamesField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
signatureAlgorithm: z.string().optional(),
|
subjectAltNames: validateAltNamesField.optional(),
|
||||||
keyAlgorithm: z.string().optional()
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||||
}),
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const hasDateFields = data.notBefore || data.notAfter;
|
||||||
|
const hasTtl = data.ttl;
|
||||||
|
return !(hasDateFields && hasTtl);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.notBefore && data.notAfter) {
|
||||||
|
const notBefore = new Date(data.notBefore);
|
||||||
|
const notAfter = new Date(data.notAfter);
|
||||||
|
return notBefore < notAfter;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "notBefore must be earlier than notAfter"
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim(),
|
certificate: z.string().trim(),
|
||||||
@@ -54,8 +89,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
commonName: req.body.commonName,
|
commonName: req.body.commonName,
|
||||||
keyUsages: req.body.keyUsages,
|
keyUsages: req.body.keyUsages,
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
subjectAlternativeNames: req.body.altNames
|
altNames: req.body.subjectAltNames
|
||||||
? req.body.altNames
|
? req.body.subjectAltNames
|
||||||
.split(", ")
|
.split(", ")
|
||||||
.map((name) => name.trim())
|
.map((name) => name.trim())
|
||||||
.map((name) => {
|
.map((name) => {
|
||||||
@@ -68,7 +103,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
url: "uri"
|
url: "uri"
|
||||||
} as const;
|
} as const;
|
||||||
return {
|
return {
|
||||||
type: typeMapping[mappedType.type] as "dns_name" | "ip_address" | "email" | "uri",
|
type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType,
|
||||||
value: mappedType.value
|
value: mappedType.value
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -94,23 +129,16 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateRequest: mappedCertificateRequest
|
certificateRequest: mappedCertificateRequest
|
||||||
});
|
});
|
||||||
|
|
||||||
const profile = await server.services.certificateProfile.getProfileById({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: req.body.profileId
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: profile.projectId,
|
projectId: data.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
commonName: req.body.commonName || ""
|
commonName: req.body.commonName || "",
|
||||||
|
profileName: data.profileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,13 +156,38 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
body: z.object({
|
body: z
|
||||||
profileId: z.string().uuid(),
|
.object({
|
||||||
csr: z.string().trim().min(1).max(4096),
|
profileId: z.string().uuid(),
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
csr: z.string().trim().min(1).max(4096),
|
||||||
notBefore: validateCaDateField.optional(),
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notAfter: validateCaDateField.optional()
|
notBefore: validateCaDateField.optional(),
|
||||||
}),
|
notAfter: validateCaDateField.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const hasDateFields = data.notBefore || data.notAfter;
|
||||||
|
const hasTtl = data.ttl;
|
||||||
|
return !(hasDateFields && hasTtl);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.notBefore && data.notAfter) {
|
||||||
|
const notBefore = new Date(data.notBefore);
|
||||||
|
const notAfter = new Date(data.notAfter);
|
||||||
|
return notBefore < notAfter;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "notBefore must be earlier than notAfter"
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificate: z.string().trim(),
|
certificate: z.string().trim(),
|
||||||
@@ -161,22 +214,16 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const profile = await server.services.certificateProfile.getProfileById({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: req.body.profileId
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: profile.projectId,
|
projectId: data.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId
|
certificateId: data.certificateId,
|
||||||
|
profileName: data.profileName,
|
||||||
|
commonName: req.body.csr || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -194,48 +241,73 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
body: z.object({
|
body: z
|
||||||
profileId: z.string().uuid(),
|
.object({
|
||||||
subjectAlternativeNames: z
|
profileId: z.string().uuid(),
|
||||||
.array(
|
subjectAlternativeNames: z
|
||||||
z.object({
|
.array(
|
||||||
type: z.enum(["dns", "ip"]),
|
z.object({
|
||||||
value: z.string()
|
type: z.nativeEnum(ACMESANType),
|
||||||
})
|
value: z.string()
|
||||||
)
|
})
|
||||||
.min(1),
|
)
|
||||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
.min(1),
|
||||||
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional(),
|
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
|
||||||
notBefore: validateCaDateField.optional(),
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
commonName: validateTemplateRegexField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
signatureAlgorithm: z.string().optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
keyAlgorithm: z.string().optional()
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||||
}),
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const hasDateFields = data.notBefore || data.notAfter;
|
||||||
|
const hasTtl = data.ttl;
|
||||||
|
return !(hasDateFields && hasTtl);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.notBefore && data.notAfter) {
|
||||||
|
const notBefore = new Date(data.notBefore);
|
||||||
|
const notAfter = new Date(data.notAfter);
|
||||||
|
return notBefore < notAfter;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "notBefore must be earlier than notAfter"
|
||||||
|
}
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
orderId: z.string(),
|
orderId: z.string(),
|
||||||
status: z.enum(["pending", "processing", "valid", "invalid"]),
|
status: z.nativeEnum(CertificateOrderStatus),
|
||||||
subjectAlternativeNames: z.array(
|
subjectAlternativeNames: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["dns", "ip"]),
|
type: z.nativeEnum(ACMESANType),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
status: z.enum(["pending", "processing", "valid", "invalid"])
|
status: z.nativeEnum(CertificateOrderStatus)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
authorizations: z.array(
|
authorizations: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
identifier: z.object({
|
identifier: z.object({
|
||||||
type: z.enum(["dns", "ip"]),
|
type: z.nativeEnum(ACMESANType),
|
||||||
value: z.string()
|
value: z.string()
|
||||||
}),
|
}),
|
||||||
status: z.enum(["pending", "processing", "valid", "invalid"]),
|
status: z.nativeEnum(CertificateOrderStatus),
|
||||||
expires: z.string().optional(),
|
expires: z.string().optional(),
|
||||||
challenges: z.array(
|
challenges: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
status: z.enum(["pending", "processing", "valid", "invalid"]),
|
status: z.nativeEnum(CertificateOrderStatus),
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
token: z.string()
|
token: z.string()
|
||||||
})
|
})
|
||||||
@@ -256,7 +328,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
profileId: req.body.profileId,
|
profileId: req.body.profileId,
|
||||||
certificateOrder: {
|
certificateOrder: {
|
||||||
subjectAlternativeNames: req.body.subjectAlternativeNames,
|
altNames: req.body.subjectAlternativeNames,
|
||||||
validity: {
|
validity: {
|
||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
@@ -270,23 +342,15 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const profile = await server.services.certificateProfile.getProfileById({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
profileId: req.body.profileId
|
|
||||||
});
|
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: profile.projectId,
|
projectId: data.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
orderId: data.orderId,
|
orderId: data.orderId,
|
||||||
subjectAlternativeNames: req.body.subjectAlternativeNames.map((san) => `${san.type}:${san.value}`)
|
profileName: data.profileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import { signatureAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||||
|
|
||||||
|
describe("signatureAlgorithmToAlgCfg", () => {
|
||||||
|
describe("RSA algorithms", () => {
|
||||||
|
it("should handle RSA-SHA256 correctly", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", CertKeyAlgorithm.RSA_2048);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 2048
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle RSA-SHA384 correctly", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA384", CertKeyAlgorithm.RSA_4096);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-384",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 4096
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle RSA-SHA512 correctly", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA512", CertKeyAlgorithm.RSA_2048);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-512",
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
modulusLength: 2048
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ECDSA algorithms", () => {
|
||||||
|
it("should handle ECDSA-SHA256 with P-256 curve", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", CertKeyAlgorithm.ECDSA_P256);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
hash: "SHA-256"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ECDSA-SHA384 with P-384 curve", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", CertKeyAlgorithm.ECDSA_P384);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384",
|
||||||
|
hash: "SHA-384"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ECDSA-SHA256 with EC_prime256v1 string format", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_prime256v1");
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
hash: "SHA-256"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle ECDSA-SHA384 with EC_secp384r1 string format", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", "EC_secp384r1");
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384",
|
||||||
|
hash: "SHA-384"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hash format normalization", () => {
|
||||||
|
it("should normalize SHA256 to SHA-256", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", CertKeyAlgorithm.RSA_2048);
|
||||||
|
expect(result.hash).toBe("SHA-256");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize SHA384 to SHA-384", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", CertKeyAlgorithm.ECDSA_P384);
|
||||||
|
expect(result.hash).toBe("SHA-384");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize SHA512 to SHA-512", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA512", CertKeyAlgorithm.RSA_4096);
|
||||||
|
expect(result.hash).toBe("SHA-512");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle SHA1 format", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA1", CertKeyAlgorithm.RSA_2048);
|
||||||
|
expect(result.hash).toBe("SHA-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle SHA224 format", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA224", CertKeyAlgorithm.ECDSA_P256);
|
||||||
|
expect(result.hash).toBe("SHA-224");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle case insensitive hash normalization", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-sha256", CertKeyAlgorithm.RSA_2048);
|
||||||
|
expect(result.hash).toBe("SHA-256");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle already normalized hash formats", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", CertKeyAlgorithm.ECDSA_P256);
|
||||||
|
expect(result.hash).toBe("SHA-256");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle SHA-3 family hashes", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA3256", CertKeyAlgorithm.RSA_2048);
|
||||||
|
expect(result.hash).toBe("SHA3-256");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("dynamic key algorithm support", () => {
|
||||||
|
it("should support future RSA key sizes", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", "RSA_8192");
|
||||||
|
|
||||||
|
expect(result.name).toBe("RSASSA-PKCS1-v1_5");
|
||||||
|
expect(result.hash).toBe("SHA-256");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support future EC curves", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_secp521r1");
|
||||||
|
|
||||||
|
expect(result.name).toBe("ECDSA");
|
||||||
|
expect(result.namedCurve).toBe("P-256");
|
||||||
|
expect(result.hash).toBe("SHA-256");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support EC_P384 string format", () => {
|
||||||
|
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", "EC_P384");
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-384",
|
||||||
|
hash: "SHA-384"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -99,29 +99,52 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgorithm: CertKeyAlgorithm) => {
|
export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgorithm: CertKeyAlgorithm | string) => {
|
||||||
// Parse signature algorithm like "RSA-SHA256", "ECDSA-SHA256" etc.
|
// Parse signature algorithm like "RSA-SHA256", "ECDSA-SHA256" etc.
|
||||||
const [keyType, hashType] = signatureAlgorithm.split("-");
|
const [keyType, hashType] = signatureAlgorithm.split("-");
|
||||||
|
|
||||||
|
const normalizeHashType = (hash: string) => {
|
||||||
|
const upperHash = hash.toUpperCase();
|
||||||
|
|
||||||
|
if (upperHash === "SHA1" || upperHash === "SHA-1") return "SHA-1";
|
||||||
|
|
||||||
|
if (upperHash === "SHA224" || upperHash === "SHA-224") return "SHA-224";
|
||||||
|
if (upperHash === "SHA256" || upperHash === "SHA-256") return "SHA-256";
|
||||||
|
if (upperHash === "SHA384" || upperHash === "SHA-384") return "SHA-384";
|
||||||
|
if (upperHash === "SHA512" || upperHash === "SHA-512") return "SHA-512";
|
||||||
|
|
||||||
|
if (upperHash === "SHA3224" || upperHash === "SHA3-224") return "SHA3-224";
|
||||||
|
if (upperHash === "SHA3256" || upperHash === "SHA3-256") return "SHA3-256";
|
||||||
|
if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384";
|
||||||
|
if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512";
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedHash = hashType ? normalizeHashType(hashType) : undefined;
|
||||||
|
|
||||||
switch (keyType) {
|
switch (keyType) {
|
||||||
case "RSA":
|
case "RSA":
|
||||||
return {
|
return {
|
||||||
name: "RSASSA-PKCS1-v1_5",
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
hash: hashType || "SHA-256",
|
hash: normalizedHash || "SHA-256",
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
modulusLength: keyAlgorithm === CertKeyAlgorithm.RSA_4096 ? 4096 : 2048
|
modulusLength: keyAlgorithm === CertKeyAlgorithm.RSA_4096 ? 4096 : 2048
|
||||||
};
|
};
|
||||||
case "ECDSA":
|
case "ECDSA":
|
||||||
// eslint-disable-next-line no-case-declarations
|
// eslint-disable-next-line no-case-declarations
|
||||||
const namedCurve = keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 ? "P-384" : "P-256";
|
const is384Curve =
|
||||||
|
keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384";
|
||||||
|
// eslint-disable-next-line no-case-declarations
|
||||||
|
const namedCurve = is384Curve ? "P-384" : "P-256";
|
||||||
return {
|
return {
|
||||||
name: "ECDSA",
|
name: "ECDSA",
|
||||||
namedCurve,
|
namedCurve,
|
||||||
hash: hashType || (namedCurve === "P-384" ? "SHA-384" : "SHA-256")
|
hash: normalizedHash || (namedCurve === "P-384" ? "SHA-384" : "SHA-256")
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
// Fallback to key algorithm default
|
// Fallback to key algorithm default
|
||||||
return keyAlgorithmToAlgCfg(keyAlgorithm);
|
return keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+41
-5
@@ -1280,16 +1280,31 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use provided keyAlgorithm if available, otherwise fall back to CA's algorithm
|
|
||||||
const effectiveKeyAlgorithm =
|
const effectiveKeyAlgorithm =
|
||||||
(keyAlgorithm as CertKeyAlgorithm) || (ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
(keyAlgorithm as CertKeyAlgorithm) || (ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||||
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
|
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
|
||||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
|
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
|
||||||
|
|
||||||
|
if (signatureAlgorithm) {
|
||||||
|
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
|
||||||
|
const requestedKeyType = signatureAlgorithm.split("-")[0];
|
||||||
|
|
||||||
|
const isRsaCa = caKeyAlgorithm.startsWith("RSA");
|
||||||
|
const isEcdsaCa = caKeyAlgorithm.startsWith("EC");
|
||||||
|
|
||||||
|
if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) {
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "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.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine signing algorithm for certificate signing
|
// Determine signing algorithm for certificate signing
|
||||||
const signingAlg = signatureAlgorithm
|
const signingAlg = signatureAlgorithm
|
||||||
? signatureAlgorithmToAlgCfg(signatureAlgorithm, effectiveKeyAlgorithm)
|
? signatureAlgorithmToAlgCfg(signatureAlgorithm, ca.internalCa.keyAlgorithm as CertKeyAlgorithm)
|
||||||
: keyGenAlg;
|
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||||
name: `CN=${commonName}`,
|
name: `CN=${commonName}`,
|
||||||
@@ -1528,7 +1543,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages
|
extendedKeyUsages,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm
|
||||||
} = dto;
|
} = dto;
|
||||||
|
|
||||||
let collectionId = pkiCollectionId;
|
let collectionId = pkiCollectionId;
|
||||||
@@ -1633,7 +1650,26 @@ export const internalCertificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
if (signatureAlgorithm) {
|
||||||
|
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");
|
||||||
|
|
||||||
|
if ((requestedKeyType === "RSA" && !isRsaCa) || (requestedKeyType === "ECDSA" && !isEcdsaCa)) {
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
const supportedType = isRsaCa ? "RSA" : isEcdsaCa ? "ECDSA" : "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.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm;
|
||||||
|
const alg = signatureAlgorithm
|
||||||
|
? signatureAlgorithmToAlgCfg(signatureAlgorithm, effectiveKeyAlgorithm)
|
||||||
|
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||||
|
|
||||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||||
|
|
||||||
|
|||||||
+4
@@ -150,6 +150,8 @@ export type TSignCertFromCaDTO =
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
isInternal: false;
|
isInternal: false;
|
||||||
@@ -165,6 +167,8 @@ export type TSignCertFromCaDTO =
|
|||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">);
|
} & Omit<TProjectPermission, "projectId">);
|
||||||
|
|
||||||
export type TGetCaCertificateTemplatesDTO = {
|
export type TGetCaCertificateTemplatesDTO = {
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
export enum CertSubjectAlternativeNameType {
|
||||||
|
DNS_NAME = "dns_name",
|
||||||
|
IP_ADDRESS = "ip_address",
|
||||||
|
EMAIL = "email",
|
||||||
|
URI = "uri"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertKeyUsageType {
|
||||||
|
DIGITAL_SIGNATURE = "digital_signature",
|
||||||
|
KEY_ENCIPHERMENT = "key_encipherment",
|
||||||
|
NON_REPUDIATION = "non_repudiation",
|
||||||
|
DATA_ENCIPHERMENT = "data_encipherment",
|
||||||
|
KEY_AGREEMENT = "key_agreement",
|
||||||
|
KEY_CERT_SIGN = "key_cert_sign",
|
||||||
|
CRL_SIGN = "crl_sign",
|
||||||
|
ENCIPHER_ONLY = "encipher_only",
|
||||||
|
DECIPHER_ONLY = "decipher_only"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertExtendedKeyUsageType {
|
||||||
|
CLIENT_AUTH = "client_auth",
|
||||||
|
SERVER_AUTH = "server_auth",
|
||||||
|
CODE_SIGNING = "code_signing",
|
||||||
|
EMAIL_PROTECTION = "email_protection",
|
||||||
|
OCSP_SIGNING = "ocsp_signing",
|
||||||
|
TIME_STAMPING = "time_stamping"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertIncludeType {
|
||||||
|
MANDATORY = "mandatory",
|
||||||
|
OPTIONAL = "optional",
|
||||||
|
PROHIBIT = "prohibit"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertDurationUnit {
|
||||||
|
DAYS = "days",
|
||||||
|
MONTHS = "months",
|
||||||
|
YEARS = "years"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertSubjectAttributeType {
|
||||||
|
COMMON_NAME = "common_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return "dns";
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return "ip";
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return "email";
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return "uri";
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
|
||||||
|
switch (type) {
|
||||||
|
case "dns":
|
||||||
|
case "dns_name":
|
||||||
|
return CertSubjectAlternativeNameType.DNS_NAME;
|
||||||
|
case "ip":
|
||||||
|
case "ip_address":
|
||||||
|
return CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||||
|
case "email":
|
||||||
|
return CertSubjectAlternativeNameType.EMAIL;
|
||||||
|
case "uri":
|
||||||
|
case "url":
|
||||||
|
return CertSubjectAlternativeNameType.URI;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown SAN type: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapKeyUsageToLegacy = (usage: CertKeyUsageType): string => {
|
||||||
|
switch (usage) {
|
||||||
|
case CertKeyUsageType.DIGITAL_SIGNATURE:
|
||||||
|
return "digitalSignature";
|
||||||
|
case CertKeyUsageType.KEY_ENCIPHERMENT:
|
||||||
|
return "keyEncipherment";
|
||||||
|
case CertKeyUsageType.NON_REPUDIATION:
|
||||||
|
return "nonRepudiation";
|
||||||
|
case CertKeyUsageType.DATA_ENCIPHERMENT:
|
||||||
|
return "dataEncipherment";
|
||||||
|
case CertKeyUsageType.KEY_AGREEMENT:
|
||||||
|
return "keyAgreement";
|
||||||
|
case CertKeyUsageType.KEY_CERT_SIGN:
|
||||||
|
return "keyCertSign";
|
||||||
|
case CertKeyUsageType.CRL_SIGN:
|
||||||
|
return "cRLSign";
|
||||||
|
case CertKeyUsageType.ENCIPHER_ONLY:
|
||||||
|
return "encipherOnly";
|
||||||
|
case CertKeyUsageType.DECIPHER_ONLY:
|
||||||
|
return "decipherOnly";
|
||||||
|
default:
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapLegacyKeyUsageToStandard = (usage: string): CertKeyUsageType => {
|
||||||
|
switch (usage) {
|
||||||
|
case "digitalSignature":
|
||||||
|
case "digital_signature":
|
||||||
|
return CertKeyUsageType.DIGITAL_SIGNATURE;
|
||||||
|
case "keyEncipherment":
|
||||||
|
case "key_encipherment":
|
||||||
|
return CertKeyUsageType.KEY_ENCIPHERMENT;
|
||||||
|
case "nonRepudiation":
|
||||||
|
case "non_repudiation":
|
||||||
|
return CertKeyUsageType.NON_REPUDIATION;
|
||||||
|
case "dataEncipherment":
|
||||||
|
case "data_encipherment":
|
||||||
|
return CertKeyUsageType.DATA_ENCIPHERMENT;
|
||||||
|
case "keyAgreement":
|
||||||
|
case "key_agreement":
|
||||||
|
return CertKeyUsageType.KEY_AGREEMENT;
|
||||||
|
case "keyCertSign":
|
||||||
|
case "key_cert_sign":
|
||||||
|
return CertKeyUsageType.KEY_CERT_SIGN;
|
||||||
|
case "cRLSign":
|
||||||
|
case "crl_sign":
|
||||||
|
return CertKeyUsageType.CRL_SIGN;
|
||||||
|
case "encipherOnly":
|
||||||
|
case "encipher_only":
|
||||||
|
return CertKeyUsageType.ENCIPHER_ONLY;
|
||||||
|
case "decipherOnly":
|
||||||
|
case "decipher_only":
|
||||||
|
return CertKeyUsageType.DECIPHER_ONLY;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown key usage: ${usage}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapExtendedKeyUsageToLegacy = (usage: CertExtendedKeyUsageType): string => {
|
||||||
|
switch (usage) {
|
||||||
|
case CertExtendedKeyUsageType.CLIENT_AUTH:
|
||||||
|
return "clientAuth";
|
||||||
|
case CertExtendedKeyUsageType.SERVER_AUTH:
|
||||||
|
return "serverAuth";
|
||||||
|
case CertExtendedKeyUsageType.CODE_SIGNING:
|
||||||
|
return "codeSigning";
|
||||||
|
case CertExtendedKeyUsageType.EMAIL_PROTECTION:
|
||||||
|
return "emailProtection";
|
||||||
|
case CertExtendedKeyUsageType.OCSP_SIGNING:
|
||||||
|
return "ocspSigning";
|
||||||
|
case CertExtendedKeyUsageType.TIME_STAMPING:
|
||||||
|
return "timeStamping";
|
||||||
|
default:
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapLegacyExtendedKeyUsageToStandard = (usage: string): CertExtendedKeyUsageType => {
|
||||||
|
switch (usage) {
|
||||||
|
case "clientAuth":
|
||||||
|
case "client_auth":
|
||||||
|
return CertExtendedKeyUsageType.CLIENT_AUTH;
|
||||||
|
case "serverAuth":
|
||||||
|
case "server_auth":
|
||||||
|
return CertExtendedKeyUsageType.SERVER_AUTH;
|
||||||
|
case "codeSigning":
|
||||||
|
case "code_signing":
|
||||||
|
return CertExtendedKeyUsageType.CODE_SIGNING;
|
||||||
|
case "emailProtection":
|
||||||
|
case "email_protection":
|
||||||
|
return CertExtendedKeyUsageType.EMAIL_PROTECTION;
|
||||||
|
case "ocspSigning":
|
||||||
|
case "ocsp_signing":
|
||||||
|
return CertExtendedKeyUsageType.OCSP_SIGNING;
|
||||||
|
case "timeStamping":
|
||||||
|
case "time_stamping":
|
||||||
|
return CertExtendedKeyUsageType.TIME_STAMPING;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown extended key usage: ${usage}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
|
||||||
|
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
|
||||||
|
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
|
||||||
|
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
|
||||||
|
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
|
||||||
|
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
|
||||||
@@ -1,34 +1,39 @@
|
|||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
mapExtendedKeyUsageToLegacy,
|
||||||
|
mapKeyUsageToLegacy,
|
||||||
|
mapLegacyExtendedKeyUsageToStandard,
|
||||||
|
mapLegacyKeyUsageToStandard
|
||||||
|
} from "./certificate-constants";
|
||||||
|
|
||||||
interface CertificateRequestInput {
|
interface CertificateRequestInput {
|
||||||
keyUsages?: string[];
|
keyUsages?: string[];
|
||||||
extendedKeyUsages?: string[];
|
extendedKeyUsages?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapEnumsForValidation = <T extends CertificateRequestInput>(request: T): T => {
|
export const mapEnumsForValidation = <T extends CertificateRequestInput>(request: T): T => {
|
||||||
const keyUsageMapping: Record<string, string> = {
|
const mapKeyUsage = (usage: string): string => {
|
||||||
digitalSignature: "digital_signature",
|
try {
|
||||||
keyEncipherment: "key_encipherment",
|
return mapLegacyKeyUsageToStandard(usage);
|
||||||
nonRepudiation: "non_repudiation",
|
} catch {
|
||||||
dataEncipherment: "data_encipherment",
|
return usage;
|
||||||
keyAgreement: "key_agreement",
|
}
|
||||||
keyCertSign: "key_cert_sign",
|
|
||||||
cRLSign: "crl_sign",
|
|
||||||
encipherOnly: "encipher_only",
|
|
||||||
decipherOnly: "decipher_only"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const extendedKeyUsageMapping: Record<string, string> = {
|
const mapExtendedKeyUsage = (usage: string): string => {
|
||||||
serverAuth: "server_auth",
|
try {
|
||||||
clientAuth: "client_auth",
|
return mapLegacyExtendedKeyUsageToStandard(usage);
|
||||||
codeSigning: "code_signing",
|
} catch {
|
||||||
emailProtection: "email_protection",
|
return usage;
|
||||||
timeStamping: "time_stamping",
|
}
|
||||||
ocspSigning: "ocsp_signing"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...request,
|
...request,
|
||||||
keyUsages: request.keyUsages?.map((usage: string) => keyUsageMapping[usage] || usage),
|
keyUsages: request.keyUsages?.map(mapKeyUsage),
|
||||||
extendedKeyUsages: request.extendedKeyUsages?.map((usage: string) => extendedKeyUsageMapping[usage] || usage)
|
extendedKeyUsages: request.extendedKeyUsages?.map(mapExtendedKeyUsage)
|
||||||
} as T;
|
} as T;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,27 +56,28 @@ export const buildCertificateSubjectFromTemplate = (
|
|||||||
): Record<string, string | undefined> => {
|
): Record<string, string | undefined> => {
|
||||||
const subject: Record<string, string> = {};
|
const subject: Record<string, string> = {};
|
||||||
const attributeMap: Record<string, string> = {
|
const attributeMap: Record<string, string> = {
|
||||||
common_name: "commonName",
|
common_name: "commonName"
|
||||||
organization_name: "organization",
|
|
||||||
organization_unit: "organizationUnit",
|
|
||||||
locality: "locality",
|
|
||||||
state: "state",
|
|
||||||
country: "country",
|
|
||||||
email: "email",
|
|
||||||
street_address: "streetAddress",
|
|
||||||
postal_code: "postalCode"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!templateAttributes || templateAttributes.length === 0) {
|
if (!templateAttributes || templateAttributes.length === 0) {
|
||||||
Object.entries(attributeMap).forEach(([templateKey, requestKey]) => {
|
throw new Error(
|
||||||
const value = request[requestKey];
|
"Template must define allowed certificate attributes. Cannot issue certificate without template attribute constraints."
|
||||||
if (value && typeof value === "string") {
|
);
|
||||||
subject[templateKey] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return subject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowedAttributes = new Set(templateAttributes.map((attr) => attributeMap[attr.type]));
|
||||||
|
|
||||||
|
Object.keys(attributeMap).forEach((templateType) => {
|
||||||
|
const requestKey = attributeMap[templateType];
|
||||||
|
const value = request[requestKey];
|
||||||
|
|
||||||
|
if (value && !allowedAttributes.has(requestKey)) {
|
||||||
|
throw new Error(
|
||||||
|
`Certificate attribute '${requestKey}' is not allowed by the template. Template must define constraints for all requested attributes.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
templateAttributes.forEach((attr) => {
|
templateAttributes.forEach((attr) => {
|
||||||
if (attr.include === "prohibit") {
|
if (attr.include === "prohibit") {
|
||||||
return;
|
return;
|
||||||
@@ -101,12 +107,28 @@ export const buildSubjectAlternativeNamesFromTemplate = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!templateSans || templateSans.length === 0) {
|
if (!templateSans || templateSans.length === 0) {
|
||||||
return request.subjectAlternativeNames.map((san) => san.value).join(",");
|
if (request.subjectAlternativeNames.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Template must define allowed subject alternative names. Cannot issue certificate with SANs when template has no SAN constraints."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedSans: string[] = [];
|
const templateSanTypes = new Set(templateSans.map((san) => san.type));
|
||||||
const prohibitedTypes = new Set(templateSans.filter((san) => san.include === "prohibit").map((san) => san.type));
|
const prohibitedTypes = new Set(templateSans.filter((san) => san.include === "prohibit").map((san) => san.type));
|
||||||
|
|
||||||
|
request.subjectAlternativeNames.forEach((san) => {
|
||||||
|
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
|
||||||
|
if (!templateSanTypes.has(sanType)) {
|
||||||
|
throw new Error(
|
||||||
|
`Subject Alternative Name type '${sanType}' is not allowed by the template. Template must define constraints for all requested SAN types.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedSans: string[] = [];
|
||||||
|
|
||||||
request.subjectAlternativeNames.forEach((san) => {
|
request.subjectAlternativeNames.forEach((san) => {
|
||||||
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
|
const sanType = san.type === "dns_name" ? "dns_name" : san.type;
|
||||||
if (!prohibitedTypes.has(sanType)) {
|
if (!prohibitedTypes.has(sanType)) {
|
||||||
@@ -116,3 +138,39 @@ export const buildSubjectAlternativeNamesFromTemplate = (
|
|||||||
|
|
||||||
return allowedSans.join(",");
|
return allowedSans.join(",");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertLegacyKeyUsage = (usage: CertKeyUsage): CertKeyUsageType => {
|
||||||
|
return mapLegacyKeyUsageToStandard(usage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToLegacyKeyUsage = (usage: CertKeyUsageType): CertKeyUsage => {
|
||||||
|
return mapKeyUsageToLegacy(usage) as CertKeyUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsage): CertExtendedKeyUsageType => {
|
||||||
|
return mapLegacyExtendedKeyUsageToStandard(usage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsageType): CertExtendedKeyUsage => {
|
||||||
|
return mapExtendedKeyUsageToLegacy(usage) as CertExtendedKeyUsage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertKeyUsageArrayFromLegacy = (usages?: CertKeyUsage[]): CertKeyUsageType[] | undefined => {
|
||||||
|
return usages?.map(convertLegacyKeyUsage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertKeyUsageArrayToLegacy = (usages?: CertKeyUsageType[]): CertKeyUsage[] | undefined => {
|
||||||
|
return usages?.map(convertToLegacyKeyUsage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertExtendedKeyUsageArrayFromLegacy = (
|
||||||
|
usages?: CertExtendedKeyUsage[]
|
||||||
|
): CertExtendedKeyUsageType[] | undefined => {
|
||||||
|
return usages?.map(convertLegacyExtendedKeyUsage);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertExtendedKeyUsageArrayToLegacy = (
|
||||||
|
usages?: CertExtendedKeyUsageType[]
|
||||||
|
): CertExtendedKeyUsage[] | undefined => {
|
||||||
|
return usages?.map(convertToLegacyExtendedKeyUsage);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TCertificateAuthorityDALFactory } from "@app/services/certificate-autho
|
|||||||
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
|
||||||
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
|
||||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||||
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
@@ -56,6 +57,10 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||||
|
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!profile.estConfigId) {
|
if (!profile.estConfigId) {
|
||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
@@ -141,6 +146,10 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||||
|
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!profile.estConfigId) {
|
if (!profile.estConfigId) {
|
||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
@@ -237,6 +246,10 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile.enrollmentType !== EnrollmentType.EST) {
|
||||||
|
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
|
||||||
|
}
|
||||||
|
|
||||||
if (!profile.estConfigId) {
|
if (!profile.estConfigId) {
|
||||||
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
|
||||||
}
|
}
|
||||||
@@ -280,8 +293,14 @@ export const certificateEstV3ServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
|
const certificateChain = extractX509CertFromChain(caCertChain);
|
||||||
|
if (!certificateChain || certificateChain.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid CA certificate chain: unable to extract certificates"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const certificates = certificateChain.map((cert) => new x509.X509Certificate(cert));
|
||||||
const caCertificate = new x509.X509Certificate(caCert);
|
const caCertificate = new x509.X509Certificate(caCert);
|
||||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,42 +31,72 @@ export const createCertificateProfileSchema = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
return false;
|
if (!data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data.enrollmentType === EnrollmentType.API && !data.apiConfig) {
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
return false;
|
if (!data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Config must be provided based on enrollment type"
|
message:
|
||||||
|
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateCertificateProfileSchema = z.object({
|
export const updateCertificateProfileSchema = z
|
||||||
slug: z
|
.object({
|
||||||
.string()
|
slug: z
|
||||||
.min(1)
|
.string()
|
||||||
.max(255)
|
.min(1)
|
||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
.max(255)
|
||||||
.optional(),
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
||||||
description: z.string().max(1000).optional(),
|
.optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
estConfig: z
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
.object({
|
estConfig: z
|
||||||
disableBootstrapCaValidation: z.boolean().default(false),
|
.object({
|
||||||
passphrase: z.string().min(1),
|
disableBootstrapCaValidation: z.boolean().default(false),
|
||||||
encryptedCaChain: z.string()
|
passphrase: z.string().min(1),
|
||||||
})
|
encryptedCaChain: z.string()
|
||||||
.optional(),
|
})
|
||||||
apiConfig: z
|
.optional(),
|
||||||
.object({
|
apiConfig: z
|
||||||
autoRenew: z.boolean().default(false),
|
.object({
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
autoRenew: z.boolean().default(false),
|
||||||
})
|
autoRenewDays: z.number().min(1).max(365).optional()
|
||||||
.optional()
|
})
|
||||||
});
|
.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === EnrollmentType.EST) {
|
||||||
|
if (data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.enrollmentType === EnrollmentType.API) {
|
||||||
|
if (data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getCertificateProfileByIdSchema = z.object({
|
export const getCertificateProfileByIdSchema = z.object({
|
||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
|
|||||||
@@ -122,9 +122,7 @@ describe("CertificateProfileService", () => {
|
|||||||
create: vi.fn().mockResolvedValue({ id: "api-config-123" }),
|
create: vi.fn().mockResolvedValue({ id: "api-config-123" }),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
updateById: vi.fn(),
|
||||||
deleteById: vi.fn(),
|
|
||||||
findProfilesForAutoRenewal: vi.fn(),
|
findProfilesForAutoRenewal: vi.fn(),
|
||||||
isConfigInUse: vi.fn(),
|
|
||||||
transaction: vi.fn(),
|
transaction: vi.fn(),
|
||||||
find: vi.fn(),
|
find: vi.fn(),
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
@@ -136,8 +134,6 @@ describe("CertificateProfileService", () => {
|
|||||||
create: vi.fn().mockResolvedValue({ id: "est-config-123" }),
|
create: vi.fn().mockResolvedValue({ id: "est-config-123" }),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
updateById: vi.fn(),
|
||||||
deleteById: vi.fn(),
|
|
||||||
isConfigInUse: vi.fn(),
|
|
||||||
transaction: vi.fn(),
|
transaction: vi.fn(),
|
||||||
find: vi.fn(),
|
find: vi.fn(),
|
||||||
findOne: vi.fn(),
|
findOne: vi.fn(),
|
||||||
@@ -158,6 +154,12 @@ describe("CertificateProfileService", () => {
|
|||||||
throwUnlessCan: vi.fn()
|
throwUnlessCan: vi.fn()
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
// Mock the transaction method to execute the callback and return the result
|
||||||
|
(mockCertificateProfileDAL.transaction as any).mockImplementation(async (fn: any) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/return-await
|
||||||
|
return await fn();
|
||||||
|
});
|
||||||
|
|
||||||
service = certificateProfileServiceFactory({
|
service = certificateProfileServiceFactory({
|
||||||
certificateProfileDAL: mockCertificateProfileDAL,
|
certificateProfileDAL: mockCertificateProfileDAL,
|
||||||
certificateTemplateV2DAL: mockCertificateTemplateV2DAL,
|
certificateTemplateV2DAL: mockCertificateTemplateV2DAL,
|
||||||
@@ -188,7 +190,10 @@ describe("CertificateProfileService", () => {
|
|||||||
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate);
|
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(sampleTemplate);
|
||||||
(mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null);
|
(mockCertificateProfileDAL.findByNameAndProjectId as any).mockResolvedValue(null);
|
||||||
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
||||||
(mockCertificateProfileDAL.create as any).mockResolvedValue(sampleProfile);
|
(mockCertificateProfileDAL.create as any).mockResolvedValue({
|
||||||
|
...sampleProfile,
|
||||||
|
enrollmentType: EnrollmentType.API // Ensure enrollmentType is explicitly included
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should create profile successfully", async () => {
|
it("should create profile successfully", async () => {
|
||||||
@@ -201,16 +206,19 @@ describe("CertificateProfileService", () => {
|
|||||||
expect(result).toEqual(sampleProfile);
|
expect(result).toEqual(sampleProfile);
|
||||||
expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123");
|
expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123");
|
||||||
expect(mockCertificateProfileDAL.findBySlugAndProjectId).toHaveBeenCalledWith("new-profile", "project-123");
|
expect(mockCertificateProfileDAL.findBySlugAndProjectId).toHaveBeenCalledWith("new-profile", "project-123");
|
||||||
expect(mockCertificateProfileDAL.create).toHaveBeenCalledWith({
|
expect(mockCertificateProfileDAL.create).toHaveBeenCalledWith(
|
||||||
slug: "new-profile",
|
{
|
||||||
description: "New test profile",
|
slug: "new-profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
description: "New test profile",
|
||||||
caId: "ca-123",
|
enrollmentType: EnrollmentType.API,
|
||||||
certificateTemplateId: "template-123",
|
caId: "ca-123",
|
||||||
apiConfigId: "api-config-123",
|
certificateTemplateId: "template-123",
|
||||||
estConfigId: null,
|
apiConfigId: "api-config-123",
|
||||||
projectId: "project-123"
|
estConfigId: null,
|
||||||
});
|
projectId: "project-123"
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw NotFoundError when certificate template not found", async () => {
|
it("should throw NotFoundError when certificate template not found", async () => {
|
||||||
@@ -318,7 +326,11 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
(mockCertificateProfileDAL.findById as any).mockResolvedValue(sampleProfile);
|
||||||
(mockCertificateProfileDAL.updateById as any).mockResolvedValue({ ...sampleProfile, ...updateData });
|
(mockCertificateProfileDAL.updateById as any).mockResolvedValue({
|
||||||
|
...sampleProfile,
|
||||||
|
...updateData,
|
||||||
|
enrollmentType: EnrollmentType.API // Ensure enrollmentType is explicitly included
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update profile successfully", async () => {
|
it("should update profile successfully", async () => {
|
||||||
@@ -330,7 +342,7 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
expect(result.slug).toBe("updated-profile");
|
expect(result.slug).toBe("updated-profile");
|
||||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||||
expect(mockCertificateProfileDAL.updateById).toHaveBeenCalledWith("profile-123", updateData);
|
expect(mockCertificateProfileDAL.updateById).toHaveBeenCalledWith("profile-123", updateData, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw NotFoundError when profile not found", async () => {
|
it("should throw NotFoundError when profile not found", async () => {
|
||||||
@@ -720,11 +732,14 @@ describe("CertificateProfileService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.enrollmentType).toBe(EnrollmentType.EST);
|
expect(result.enrollmentType).toBe(EnrollmentType.EST);
|
||||||
expect(mockEstEnrollmentConfigDAL.create).toHaveBeenCalledWith({
|
expect(mockEstEnrollmentConfigDAL.create).toHaveBeenCalledWith(
|
||||||
disableBootstrapCaValidation: estProfileData.estConfig.disableBootstrapCaValidation,
|
{
|
||||||
hashedPassphrase: "mocked-hash",
|
disableBootstrapCaValidation: estProfileData.estConfig.disableBootstrapCaValidation,
|
||||||
encryptedCaChain: Buffer.from(estProfileData.estConfig.encryptedCaChain, "base64")
|
hashedPassphrase: "mocked-hash",
|
||||||
});
|
encryptedCaChain: Buffer.from(estProfileData.estConfig.encryptedCaChain, "base64")
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle profile slug uniqueness validation", async () => {
|
it("should handle profile slug uniqueness validation", async () => {
|
||||||
@@ -772,7 +787,8 @@ describe("CertificateProfileService", () => {
|
|||||||
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
||||||
(mockCertificateProfileDAL.create as any).mockResolvedValue({
|
(mockCertificateProfileDAL.create as any).mockResolvedValue({
|
||||||
...sampleProfile,
|
...sampleProfile,
|
||||||
apiConfigId: "api-config-123"
|
apiConfigId: "api-config-123",
|
||||||
|
enrollmentType: EnrollmentType.API
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.createProfile({
|
const result = await service.createProfile({
|
||||||
@@ -781,10 +797,13 @@ describe("CertificateProfileService", () => {
|
|||||||
data: autoRenewData
|
data: autoRenewData
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockApiEnrollmentConfigDAL.create).toHaveBeenCalledWith({
|
expect(mockApiEnrollmentConfigDAL.create).toHaveBeenCalledWith(
|
||||||
autoRenew: true,
|
{
|
||||||
autoRenewDays: 7
|
autoRenew: true,
|
||||||
});
|
autoRenewDays: 7
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1076,7 +1095,8 @@ describe("CertificateProfileService", () => {
|
|||||||
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
(mockCertificateProfileDAL.findBySlugAndProjectId as any).mockResolvedValue(null);
|
||||||
(mockCertificateProfileDAL.create as any).mockResolvedValue({
|
(mockCertificateProfileDAL.create as any).mockResolvedValue({
|
||||||
...sampleProfile,
|
...sampleProfile,
|
||||||
slug: invalidSlugData.slug
|
slug: invalidSlugData.slug,
|
||||||
|
enrollmentType: EnrollmentType.API
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.createProfile({
|
const result = await service.createProfile({
|
||||||
|
|||||||
@@ -49,70 +49,6 @@ const convertDalToService = (dalResult: Record<string, unknown>): TCertificatePr
|
|||||||
} as TCertificateProfile;
|
} as TCertificateProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateEnrollmentConfig = async (data: {
|
|
||||||
enrollmentType: EnrollmentType;
|
|
||||||
estConfig?: TEstConfigData | null;
|
|
||||||
apiConfig?: TApiConfigData | null;
|
|
||||||
}): Promise<void> => {
|
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
|
||||||
if (!data.estConfig) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "EST enrollment type requires EST configuration"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.apiConfig) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "EST enrollment type cannot have API configuration"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfig) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "API enrollment type requires API configuration"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.estConfig) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "API enrollment type cannot have EST configuration"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateEnrollmentConfigForUpdate = async (data: {
|
|
||||||
enrollmentType: EnrollmentType;
|
|
||||||
estConfigId?: string | null;
|
|
||||||
apiConfigId?: string | null;
|
|
||||||
}): Promise<void> => {
|
|
||||||
if (data.enrollmentType === EnrollmentType.EST) {
|
|
||||||
if (!data.estConfigId) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "EST enrollment type requires EST configuration ID"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.apiConfigId) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "EST enrollment type cannot have API configuration ID"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (data.enrollmentType === EnrollmentType.API) {
|
|
||||||
if (!data.apiConfigId) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "API enrollment type requires API configuration ID"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.estConfigId) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "API enrollment type cannot have EST configuration ID"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEnrollmentConfigChanges = (data: TCertificateProfileUpdate): boolean => {
|
|
||||||
return !!(data.enrollmentType || data.estConfigId || data.apiConfigId);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const certificateProfileServiceFactory = ({
|
export const certificateProfileServiceFactory = ({
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2DAL,
|
certificateTemplateV2DAL,
|
||||||
@@ -169,43 +105,61 @@ export const certificateProfileServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate enrollment type configuration
|
// Validate enrollment configuration requirements
|
||||||
await validateEnrollmentConfig({
|
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
|
||||||
enrollmentType: data.enrollmentType,
|
throw new ForbiddenRequestError({
|
||||||
estConfig: data.estConfig,
|
message: "EST enrollment requires EST configuration"
|
||||||
apiConfig: data.apiConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create enrollment configs based on type
|
|
||||||
let estConfigId: string | null = null;
|
|
||||||
let apiConfigId: string | null = null;
|
|
||||||
|
|
||||||
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
|
||||||
const appCfg = getConfig();
|
|
||||||
// Hash the passphrase
|
|
||||||
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
|
|
||||||
|
|
||||||
const estConfig = await estEnrollmentConfigDAL.create({
|
|
||||||
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation,
|
|
||||||
hashedPassphrase,
|
|
||||||
encryptedCaChain: Buffer.from(data.estConfig.encryptedCaChain, "base64")
|
|
||||||
});
|
});
|
||||||
estConfigId = estConfig.id;
|
}
|
||||||
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
if (data.enrollmentType === EnrollmentType.API && !data.apiConfig) {
|
||||||
const apiConfig = await apiEnrollmentConfigDAL.create({
|
throw new ForbiddenRequestError({
|
||||||
autoRenew: data.apiConfig.autoRenew,
|
message: "API enrollment requires API configuration"
|
||||||
autoRenewDays: data.apiConfig.autoRenewDays
|
|
||||||
});
|
});
|
||||||
apiConfigId = apiConfig.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the profile with the created config IDs
|
// Create enrollment configs and profile
|
||||||
const { estConfig, apiConfig, ...profileData } = data;
|
const profile = await certificateProfileDAL.transaction(async (tx) => {
|
||||||
const profile = await certificateProfileDAL.create({
|
let estConfigId: string | null = null;
|
||||||
...profileData,
|
let apiConfigId: string | null = null;
|
||||||
projectId,
|
|
||||||
estConfigId,
|
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
|
||||||
apiConfigId
|
const appCfg = getConfig();
|
||||||
|
// Hash the passphrase
|
||||||
|
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
|
||||||
|
|
||||||
|
const estConfig = await estEnrollmentConfigDAL.create(
|
||||||
|
{
|
||||||
|
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation,
|
||||||
|
hashedPassphrase,
|
||||||
|
encryptedCaChain: Buffer.from(data.estConfig.encryptedCaChain, "base64")
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
estConfigId = estConfig.id;
|
||||||
|
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
|
||||||
|
const apiConfig = await apiEnrollmentConfigDAL.create(
|
||||||
|
{
|
||||||
|
autoRenew: data.apiConfig.autoRenew,
|
||||||
|
autoRenewDays: data.apiConfig.autoRenewDays
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
apiConfigId = apiConfig.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the profile with the created config IDs
|
||||||
|
const { estConfig, apiConfig, ...profileData } = data;
|
||||||
|
const profileResult = await certificateProfileDAL.create(
|
||||||
|
{
|
||||||
|
...profileData,
|
||||||
|
projectId,
|
||||||
|
estConfigId,
|
||||||
|
apiConfigId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return profileResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
return convertDalToService(profile);
|
return convertDalToService(profile);
|
||||||
@@ -268,37 +222,40 @@ export const certificateProfileServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasEnrollmentConfigChanges(data)) {
|
|
||||||
const mergedData = { ...existingProfile, ...data };
|
|
||||||
await validateEnrollmentConfigForUpdate({
|
|
||||||
enrollmentType: mergedData.enrollmentType as EnrollmentType,
|
|
||||||
estConfigId: mergedData.estConfigId,
|
|
||||||
apiConfigId: mergedData.apiConfigId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { estConfig, apiConfig, ...profileUpdateData } = data;
|
const { estConfig, apiConfig, ...profileUpdateData } = data;
|
||||||
|
|
||||||
if (estConfig && existingProfile.estConfigId) {
|
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||||
await estEnrollmentConfigDAL.updateById(existingProfile.estConfigId, {
|
if (estConfig && existingProfile.estConfigId) {
|
||||||
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation,
|
await estEnrollmentConfigDAL.updateById(
|
||||||
...(estConfig.passphrase && {
|
existingProfile.estConfigId,
|
||||||
hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS)
|
{
|
||||||
}),
|
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation,
|
||||||
...(estConfig.caChain && {
|
...(estConfig.passphrase && {
|
||||||
encryptedCaChain: Buffer.from(estConfig.caChain, "base64")
|
hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS)
|
||||||
})
|
}),
|
||||||
});
|
...(estConfig.caChain && {
|
||||||
}
|
encryptedCaChain: Buffer.from(estConfig.caChain, "base64")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (apiConfig && existingProfile.apiConfigId) {
|
if (apiConfig && existingProfile.apiConfigId) {
|
||||||
await apiEnrollmentConfigDAL.updateById(existingProfile.apiConfigId, {
|
await apiEnrollmentConfigDAL.updateById(
|
||||||
autoRenew: apiConfig.autoRenew,
|
existingProfile.apiConfigId,
|
||||||
autoRenewDays: apiConfig.autoRenewDays
|
{
|
||||||
});
|
autoRenew: apiConfig.autoRenew,
|
||||||
}
|
autoRenewDays: apiConfig.autoRenewDays
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileResult = await certificateProfileDAL.updateById(profileId, profileUpdateData, tx);
|
||||||
|
return profileResult;
|
||||||
|
});
|
||||||
|
|
||||||
const updatedProfile = await certificateProfileDAL.updateById(profileId, profileUpdateData);
|
|
||||||
return convertDalToService(updatedProfile);
|
return convertDalToService(updatedProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
|
|
||||||
export type TCertificateTemplateV2DALFactory = ReturnType<typeof certificateTemplateV2DALFactory>;
|
export type TCertificateTemplateV2DALFactory = ReturnType<typeof certificateTemplateV2DALFactory>;
|
||||||
|
|
||||||
|
interface CountResult {
|
||||||
|
count: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
||||||
const certificateTemplateV2Orm = ormify(db, TableName.CertificateTemplateV2);
|
const certificateTemplateV2Orm = ormify(db, TableName.CertificateTemplateV2);
|
||||||
|
|
||||||
@@ -199,12 +203,33 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
.count("*")
|
.count("*")
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
return parseInt(profileCount || "0", 10) > 0;
|
const profileUsage = parseInt((profileCount as unknown as CountResult).count || "0", 10) > 0;
|
||||||
|
|
||||||
|
const certCount = await (tx || db)(TableName.Certificate)
|
||||||
|
.where({ certificateTemplateId: templateId })
|
||||||
|
.count("*")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const certUsage = parseInt((certCount as unknown as CountResult).count || "0", 10) > 0;
|
||||||
|
|
||||||
|
return profileUsage || certUsage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Check if certificate template v2 is in use" });
|
throw new DatabaseError({ error, name: "Check if certificate template v2 is in use" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getProfilesUsingTemplate = async (templateId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const profiles = await (tx || db)(TableName.CertificateProfile)
|
||||||
|
.select("id", "slug", "description")
|
||||||
|
.where({ certificateTemplateId: templateId });
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Get profiles using certificate template v2" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...certificateTemplateV2Orm,
|
...certificateTemplateV2Orm,
|
||||||
create,
|
create,
|
||||||
@@ -214,6 +239,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
findByProjectId,
|
findByProjectId,
|
||||||
countByProjectId,
|
countByProjectId,
|
||||||
findBySlugAndProjectId,
|
findBySlugAndProjectId,
|
||||||
isTemplateInUse
|
isTemplateInUse,
|
||||||
|
getProfilesUsingTemplate
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import RE2 from "re2";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const attributeTypeSchema = z.enum(["common_name"]);
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import {
|
||||||
|
CertDurationUnit,
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertIncludeType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType,
|
||||||
|
CertSubjectAttributeType
|
||||||
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
|
||||||
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
|
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
|
||||||
|
|
||||||
const sanTypeSchema = z.enum(["dns_name", "ip_address", "email", "uri"]);
|
const includeTypeSchema = z.nativeEnum(CertIncludeType);
|
||||||
|
|
||||||
const durationUnitSchema = z.enum(["days", "months", "years"]);
|
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
|
||||||
|
|
||||||
|
const durationUnitSchema = z.nativeEnum(CertDurationUnit);
|
||||||
|
|
||||||
export const templateV2AttributeSchema = z
|
export const templateV2AttributeSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -17,32 +26,43 @@ export const templateV2AttributeSchema = z
|
|||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
if (data.type === "common_name" && data.value && data.value.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
|
if (data.include === "mandatory" && (!data.value || data.value.length > 1)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Mandatory attributes can only have one value or no value (empty)"
|
message: "Common name can only have one value. Mandatory attributes can only have one value or no value (empty)"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const templateV2KeyUsagesSchema = z.object({
|
export const templateV2KeyUsagesSchema = z.object({
|
||||||
requiredUsages: z.object({
|
requiredUsages: z
|
||||||
all: z.array(z.string())
|
.object({
|
||||||
}),
|
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||||
optionalUsages: z.object({
|
})
|
||||||
all: z.array(z.string())
|
.optional(),
|
||||||
})
|
optionalUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const templateV2ExtendedKeyUsagesSchema = z.object({
|
export const templateV2ExtendedKeyUsagesSchema = z.object({
|
||||||
requiredUsages: z.object({
|
requiredUsages: z
|
||||||
all: z.array(z.string())
|
.object({
|
||||||
}),
|
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||||
optionalUsages: z.object({
|
})
|
||||||
all: z.array(z.string())
|
.optional(),
|
||||||
})
|
optionalUsages: z
|
||||||
|
.object({
|
||||||
|
all: z.array(z.nativeEnum(CertExtendedKeyUsageType))
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const templateV2SanSchema = z
|
export const templateV2SanSchema = z
|
||||||
@@ -76,26 +96,30 @@ export const templateV2ValiditySchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const templateV2SignatureAlgorithmSchema = z.object({
|
export const templateV2SignatureAlgorithmSchema = z
|
||||||
allowedAlgorithms: z.array(z.string()).min(1),
|
.object({
|
||||||
defaultAlgorithm: z.string()
|
allowedAlgorithms: z.array(z.string()).min(1),
|
||||||
});
|
defaultAlgorithm: z.string()
|
||||||
|
})
|
||||||
|
.refine((data) => data.allowedAlgorithms.includes(data.defaultAlgorithm), {
|
||||||
|
message: "Default signature algorithm must be included in the allowed algorithms list"
|
||||||
|
});
|
||||||
|
|
||||||
export const templateV2KeyAlgorithmSchema = z.object({
|
export const templateV2KeyAlgorithmSchema = z
|
||||||
allowedKeyTypes: z.array(z.string()).min(1),
|
.object({
|
||||||
defaultKeyType: z.string()
|
allowedKeyTypes: z.array(z.string()).min(1),
|
||||||
});
|
defaultKeyType: z.string()
|
||||||
|
})
|
||||||
|
.refine((data) => data.allowedKeyTypes.includes(data.defaultKeyType), {
|
||||||
|
message: "Default key algorithm must be included in the allowed key types list"
|
||||||
|
});
|
||||||
|
|
||||||
export const createCertificateTemplateV2Schema = z.object({
|
export const createCertificateTemplateV2Schema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 255 }),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(255)
|
|
||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
attributes: z.array(templateV2AttributeSchema).optional(),
|
attributes: z.array(templateV2AttributeSchema).min(1),
|
||||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
keyUsages: templateV2KeyUsagesSchema,
|
||||||
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
|
||||||
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
subjectAlternativeNames: z.array(templateV2SanSchema).optional(),
|
||||||
validity: templateV2ValiditySchema.optional(),
|
validity: templateV2ValiditySchema.optional(),
|
||||||
@@ -104,12 +128,7 @@ export const createCertificateTemplateV2Schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const updateCertificateTemplateV2Schema = z.object({
|
export const updateCertificateTemplateV2Schema = z.object({
|
||||||
slug: z
|
slug: slugSchema({ min: 1, max: 255 }).optional(),
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.max(255)
|
|
||||||
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
|
||||||
.optional(),
|
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
attributes: z.array(templateV2AttributeSchema).optional(),
|
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
@@ -126,7 +145,7 @@ export const getCertificateTemplateV2ByIdSchema = z.object({
|
|||||||
|
|
||||||
export const getCertificateTemplateV2BySlugSchema = z.object({
|
export const getCertificateTemplateV2BySlugSchema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
slug: z.string().min(1)
|
slug: slugSchema()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listCertificateTemplatesV2Schema = z.object({
|
export const listCertificateTemplatesV2Schema = z.object({
|
||||||
@@ -142,8 +161,8 @@ export const deleteCertificateTemplateV2Schema = z.object({
|
|||||||
|
|
||||||
export const certificateRequestSchema = z.object({
|
export const certificateRequestSchema = z.object({
|
||||||
commonName: z.string().optional(),
|
commonName: z.string().optional(),
|
||||||
keyUsages: z.array(z.string()).optional(),
|
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||||
extendedKeyUsages: z.array(z.string()).optional(),
|
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||||
subjectAlternativeNames: z
|
subjectAlternativeNames: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
+509
-221
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,13 @@ import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
import { CertIncludeType, CertSubjectAttributeType } from "../certificate-common/certificate-constants";
|
||||||
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
|
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
|
||||||
import {
|
import {
|
||||||
TCertificateRequest,
|
TCertificateRequest,
|
||||||
TCertificateTemplateV2,
|
TCertificateTemplateV2,
|
||||||
TCertificateTemplateV2Insert,
|
TCertificateTemplateV2Insert,
|
||||||
TCertificateTemplateV2Update,
|
TCertificateTemplateV2Update,
|
||||||
TTemplateV2Policy,
|
|
||||||
TTemplateValidationResult
|
TTemplateValidationResult
|
||||||
} from "./certificate-template-v2-types";
|
} from "./certificate-template-v2-types";
|
||||||
|
|
||||||
@@ -70,8 +70,41 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRequestAttributeValue = (request: TCertificateRequest, attrType: string): string | undefined => {
|
const validateSubjectAttributePolicy = (attributes: Array<{ type: string; include: string; value?: string[] }>) => {
|
||||||
|
if (!attributes || attributes.length === 0) return;
|
||||||
|
|
||||||
|
const attributesByType = attributes.reduce(
|
||||||
|
(acc, attr) => {
|
||||||
|
if (!acc[attr.type]) acc[attr.type] = [];
|
||||||
|
acc[attr.type].push(attr);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, typeof attributes>
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [type, attrs] of Object.entries(attributesByType)) {
|
||||||
|
const mandatoryAttrs = attrs.filter((attr) => attr.include === CertIncludeType.MANDATORY);
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length > 1) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: `Multiple mandatory values found for subject attribute type '${type}'. Only one mandatory value is allowed per attribute type.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: `When a mandatory value exists for subject attribute type '${type}', no other values (optional or forbidden) are allowed for that attribute type.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequestAttributeValue = (
|
||||||
|
request: TCertificateRequest,
|
||||||
|
attrType: CertSubjectAttributeType | string
|
||||||
|
): string | undefined => {
|
||||||
switch (attrType) {
|
switch (attrType) {
|
||||||
|
case CertSubjectAttributeType.COMMON_NAME:
|
||||||
case "common_name":
|
case "common_name":
|
||||||
return request.commonName;
|
return request.commonName;
|
||||||
default:
|
default:
|
||||||
@@ -79,44 +112,6 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateTemplatePolicy = (policy: Partial<TTemplateV2Policy>): void => {
|
|
||||||
if (!policy) {
|
|
||||||
throw new Error("Template policy is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!policy.attributes || policy.attributes.length === 0) {
|
|
||||||
throw new Error("Template policy must include attributes array");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!policy.keyUsages || !policy.keyUsages.requiredUsages || !policy.keyUsages.optionalUsages) {
|
|
||||||
throw new Error("Template policy must include valid key usages configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.signatureAlgorithm) {
|
|
||||||
if (!policy.signatureAlgorithm.allowedAlgorithms.includes(policy.signatureAlgorithm.defaultAlgorithm)) {
|
|
||||||
throw new Error("Default signature algorithm must be in allowed algorithms list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.keyAlgorithm) {
|
|
||||||
if (!policy.keyAlgorithm.allowedKeyTypes.includes(policy.keyAlgorithm.defaultKeyType)) {
|
|
||||||
throw new Error("Default key algorithm must be in allowed key types list");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAnyPolicyField = (data: TCertificateTemplateV2Update): boolean => {
|
|
||||||
return !!(
|
|
||||||
data.attributes ||
|
|
||||||
data.keyUsages ||
|
|
||||||
data.extendedKeyUsages ||
|
|
||||||
data.subjectAlternativeNames ||
|
|
||||||
data.validity ||
|
|
||||||
data.signatureAlgorithm ||
|
|
||||||
data.keyAlgorithm
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateTemplateSlug = (baseSlug?: string): string => {
|
const generateTemplateSlug = (baseSlug?: string): string => {
|
||||||
if (baseSlug) {
|
if (baseSlug) {
|
||||||
return slugify(baseSlug);
|
return slugify(baseSlug);
|
||||||
@@ -139,177 +134,274 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
return randomSlug;
|
return randomSlug;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWildcardPattern = (value: string): boolean => {
|
||||||
|
return value.includes("*");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWildcardRegex = (pattern: string): RegExp => {
|
||||||
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const regexPattern = escaped.replace(/\*/g, ".*");
|
||||||
|
return new RE2(`^${regexPattern}$`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
"SHA256-RSA": "RSA-SHA256",
|
||||||
|
"SHA384-RSA": "RSA-SHA384",
|
||||||
|
"SHA512-RSA": "RSA-SHA512",
|
||||||
|
"SHA256-ECDSA": "ECDSA-SHA256",
|
||||||
|
"SHA384-ECDSA": "ECDSA-SHA384",
|
||||||
|
"SHA512-ECDSA": "ECDSA-SHA512"
|
||||||
|
};
|
||||||
|
return mapping[templateFormat] || templateFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
|
||||||
|
const mapping: Record<string, string> = {
|
||||||
|
"RSA-2048": "RSA_2048",
|
||||||
|
"RSA-4096": "RSA_4096",
|
||||||
|
"ECDSA-P256": "EC_prime256v1",
|
||||||
|
"ECDSA-P384": "EC_secp384r1"
|
||||||
|
};
|
||||||
|
return mapping[templateFormat] || templateFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateValueAgainstConstraints = (
|
||||||
|
value: string,
|
||||||
|
allowedValues: string[],
|
||||||
|
fieldName: string
|
||||||
|
): { isValid: boolean; error?: string } => {
|
||||||
|
if (!allowedValues || allowedValues.length === 0) {
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasWildcards = allowedValues.some(isWildcardPattern);
|
||||||
|
|
||||||
|
for (const allowedValue of allowedValues) {
|
||||||
|
if (isWildcardPattern(allowedValue)) {
|
||||||
|
try {
|
||||||
|
const regex = createWildcardRegex(allowedValue);
|
||||||
|
if (regex.test(value)) {
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (allowedValue === value) {
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (allowedValue === value) {
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasWildcards) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: `${fieldName} value '${value}' does not match allowed patterns: ${allowedValues.join(", ")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: `${fieldName} value '${value}' is not in allowed values list`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const validateRequestAgainstPolicy = (
|
const validateRequestAgainstPolicy = (
|
||||||
template: TCertificateTemplateV2,
|
template: TCertificateTemplateV2,
|
||||||
request: TCertificateRequest
|
request: TCertificateRequest
|
||||||
): TTemplateValidationResult => {
|
): TTemplateValidationResult => {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
const templateAttributeTypes = new Set(template.attributes?.map((attr) => attr.type) || []);
|
||||||
|
|
||||||
|
const attributePoliciesByType = new Map<string, typeof template.attributes>();
|
||||||
template.attributes?.forEach((attrPolicy) => {
|
template.attributes?.forEach((attrPolicy) => {
|
||||||
const requestValue = getRequestAttributeValue(request, attrPolicy.type);
|
const existing = attributePoliciesByType.get(attrPolicy.type) || [];
|
||||||
|
attributePoliciesByType.set(attrPolicy.type, [...existing, attrPolicy]);
|
||||||
if (attrPolicy.include === "mandatory") {
|
|
||||||
if (!requestValue) {
|
|
||||||
errors.push(`${attrPolicy.type} is mandatory but not provided in request`);
|
|
||||||
} else if (attrPolicy.value && attrPolicy.value.length > 0) {
|
|
||||||
// Check if the request value matches any allowed pattern
|
|
||||||
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
|
||||||
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
|
||||||
if (allowedValue.includes("*")) {
|
|
||||||
// Handle wildcard patterns
|
|
||||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
|
||||||
const regex = new RE2(`^${pattern}$`);
|
|
||||||
return regex.test(requestValue);
|
|
||||||
}
|
|
||||||
return allowedValue === requestValue;
|
|
||||||
});
|
|
||||||
if (!isValidValue) {
|
|
||||||
if (hasWildcards) {
|
|
||||||
errors.push(
|
|
||||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attrPolicy.include === "prohibit" && requestValue) {
|
|
||||||
errors.push(`${attrPolicy.type} is prohibited by template policy`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
|
|
||||||
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
|
||||||
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
|
||||||
if (allowedValue.includes("*")) {
|
|
||||||
// Handle wildcard patterns - escape dots and replace * with .*
|
|
||||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
|
||||||
const regex = new RE2(`^${pattern}$`);
|
|
||||||
return regex.test(requestValue);
|
|
||||||
}
|
|
||||||
return allowedValue === requestValue;
|
|
||||||
});
|
|
||||||
if (!isValidValue) {
|
|
||||||
if (hasWildcards) {
|
|
||||||
errors.push(
|
|
||||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (template.keyUsages) {
|
for (const [attrType, policies] of attributePoliciesByType) {
|
||||||
const missingRequired = template.keyUsages.requiredUsages.all.filter(
|
const requestValue = getRequestAttributeValue(request, attrType);
|
||||||
(usage) => !request.keyUsages?.includes(usage)
|
|
||||||
);
|
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
|
||||||
if (missingRequired.length > 0) {
|
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
|
||||||
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
|
|
||||||
|
if (hasProhibit && requestValue) {
|
||||||
|
errors.push(`${attrType} is prohibited by template policy`);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.keyUsages) {
|
if (hasMandatory && !requestValue) {
|
||||||
const allAllowedUsages = [...template.keyUsages.requiredUsages.all, ...template.keyUsages.optionalUsages.all];
|
errors.push(`${attrType} is mandatory but not provided in request`);
|
||||||
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
// eslint-disable-next-line no-continue
|
||||||
if (invalidUsages.length > 0) {
|
continue;
|
||||||
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
|
}
|
||||||
|
|
||||||
|
if (requestValue) {
|
||||||
|
const policiesWithValues = policies.filter(
|
||||||
|
(p) =>
|
||||||
|
p.value &&
|
||||||
|
p.value.length > 0 &&
|
||||||
|
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (policiesWithValues.length > 0) {
|
||||||
|
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
|
||||||
|
|
||||||
|
const validation = validateValueAgainstConstraints(requestValue, allAllowedValues, attrType);
|
||||||
|
if (!validation.isValid && validation.error) {
|
||||||
|
errors.push(validation.error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestAttributeTypes: CertSubjectAttributeType[] = [];
|
||||||
|
if (request.commonName) requestAttributeTypes.push(CertSubjectAttributeType.COMMON_NAME);
|
||||||
|
|
||||||
|
for (const requestAttrType of requestAttributeTypes) {
|
||||||
|
if (!templateAttributeTypes.has(requestAttrType)) {
|
||||||
|
errors.push(`${requestAttrType} is not allowed by template policy (not defined in template)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (template.keyUsages) {
|
||||||
|
if (template.keyUsages.requiredUsages && template.keyUsages.requiredUsages.all.length > 0) {
|
||||||
|
const missingRequired = template.keyUsages.requiredUsages.all.filter(
|
||||||
|
(usage) => !request.keyUsages?.includes(usage)
|
||||||
|
);
|
||||||
|
if (missingRequired.length > 0) {
|
||||||
|
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.keyUsages && (template.keyUsages.requiredUsages || template.keyUsages.optionalUsages)) {
|
||||||
|
const allAllowedUsages = [
|
||||||
|
...(template.keyUsages.requiredUsages?.all || []),
|
||||||
|
...(template.keyUsages.optionalUsages?.all || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allAllowedUsages.length > 0) {
|
||||||
|
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
||||||
|
if (invalidUsages.length > 0) {
|
||||||
|
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (request.keyUsages && request.keyUsages.length > 0) {
|
||||||
|
errors.push(`Key usages are not allowed by template policy (not defined in template)`);
|
||||||
|
}
|
||||||
|
|
||||||
if (template.extendedKeyUsages) {
|
if (template.extendedKeyUsages) {
|
||||||
const missingRequired = template.extendedKeyUsages.requiredUsages.all.filter(
|
if (template.extendedKeyUsages.requiredUsages && template.extendedKeyUsages.requiredUsages.all.length > 0) {
|
||||||
(usage) => !request.extendedKeyUsages?.includes(usage)
|
const missingRequired = template.extendedKeyUsages.requiredUsages.all.filter(
|
||||||
);
|
(usage) => !request.extendedKeyUsages?.includes(usage)
|
||||||
if (missingRequired.length > 0) {
|
);
|
||||||
errors.push(`Missing required extended key usages: ${missingRequired.join(", ")}`);
|
if (missingRequired.length > 0) {
|
||||||
}
|
errors.push(`Missing required extended key usages: ${missingRequired.join(", ")}`);
|
||||||
|
|
||||||
if (request.extendedKeyUsages) {
|
|
||||||
const allAllowedUsages = [
|
|
||||||
...template.extendedKeyUsages.requiredUsages.all,
|
|
||||||
...template.extendedKeyUsages.optionalUsages.all
|
|
||||||
];
|
|
||||||
const invalidUsages = request.extendedKeyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
|
||||||
if (invalidUsages.length > 0) {
|
|
||||||
errors.push(`Invalid extended key usages: ${invalidUsages.join(", ")}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.extendedKeyUsages &&
|
||||||
|
(template.extendedKeyUsages.requiredUsages || template.extendedKeyUsages.optionalUsages)
|
||||||
|
) {
|
||||||
|
const allAllowedUsages = [
|
||||||
|
...(template.extendedKeyUsages.requiredUsages?.all || []),
|
||||||
|
...(template.extendedKeyUsages.optionalUsages?.all || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allAllowedUsages.length > 0) {
|
||||||
|
const invalidUsages = request.extendedKeyUsages.filter((usage) => !allAllowedUsages.includes(usage));
|
||||||
|
if (invalidUsages.length > 0) {
|
||||||
|
errors.push(`Invalid extended key usages: ${invalidUsages.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (request.extendedKeyUsages && request.extendedKeyUsages.length > 0) {
|
||||||
|
errors.push(`Extended key usages are not allowed by template policy (not defined in template)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const templateSanTypes = new Set(template.subjectAlternativeNames?.map((san) => san.type) || []);
|
||||||
|
|
||||||
|
const sanPoliciesByType = new Map<string, typeof template.subjectAlternativeNames>();
|
||||||
template.subjectAlternativeNames?.forEach((sanPolicy) => {
|
template.subjectAlternativeNames?.forEach((sanPolicy) => {
|
||||||
const requestSans = request.subjectAlternativeNames?.filter((san) => san.type === sanPolicy.type) || [];
|
const existing = sanPoliciesByType.get(sanPolicy.type) || [];
|
||||||
|
sanPoliciesByType.set(sanPolicy.type, [...existing, sanPolicy]);
|
||||||
if (sanPolicy.include === "mandatory") {
|
|
||||||
if (requestSans.length === 0) {
|
|
||||||
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
|
|
||||||
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
|
|
||||||
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
|
||||||
requestSans.forEach((san) => {
|
|
||||||
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
|
||||||
if (allowedValue.includes("*")) {
|
|
||||||
// Handle wildcard patterns - escape dots and replace * with .*
|
|
||||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
|
||||||
const regex = new RE2(`^${pattern}$`);
|
|
||||||
return regex.test(san.value);
|
|
||||||
}
|
|
||||||
return allowedValue === san.value;
|
|
||||||
});
|
|
||||||
if (!isValidValue) {
|
|
||||||
if (hasWildcards) {
|
|
||||||
errors.push(
|
|
||||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sanPolicy.include === "prohibit" && requestSans.length > 0) {
|
|
||||||
errors.push(`${sanPolicy.type} SAN is prohibited by template policy`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
|
|
||||||
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
|
||||||
requestSans.forEach((san) => {
|
|
||||||
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
|
||||||
if (allowedValue.includes("*")) {
|
|
||||||
// Handle wildcard patterns - escape dots and replace * with .*
|
|
||||||
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
|
||||||
const regex = new RE2(`^${pattern}$`);
|
|
||||||
return regex.test(san.value);
|
|
||||||
}
|
|
||||||
return allowedValue === san.value;
|
|
||||||
});
|
|
||||||
if (!isValidValue) {
|
|
||||||
if (hasWildcards) {
|
|
||||||
errors.push(
|
|
||||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (request.signatureAlgorithm && template.signatureAlgorithm) {
|
for (const [sanType, policies] of sanPoliciesByType) {
|
||||||
if (!template.signatureAlgorithm?.allowedAlgorithms.includes(request.signatureAlgorithm)) {
|
const requestSans = request.subjectAlternativeNames?.filter((san) => san.type === sanType) || [];
|
||||||
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
|
|
||||||
|
const hasMandatory = policies.some((p) => p.include === CertIncludeType.MANDATORY);
|
||||||
|
const hasProhibit = policies.some((p) => p.include === CertIncludeType.PROHIBIT);
|
||||||
|
|
||||||
|
if (hasProhibit && requestSans.length > 0) {
|
||||||
|
errors.push(`${sanType} SAN is prohibited by template policy`);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMandatory && requestSans.length === 0) {
|
||||||
|
errors.push(`${sanType} SAN is mandatory but not provided in request`);
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestSans.length > 0) {
|
||||||
|
const policiesWithValues = policies.filter(
|
||||||
|
(p) =>
|
||||||
|
p.value &&
|
||||||
|
p.value.length > 0 &&
|
||||||
|
(p.include === CertIncludeType.MANDATORY || p.include === CertIncludeType.OPTIONAL)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (policiesWithValues.length > 0) {
|
||||||
|
const allAllowedValues = policiesWithValues.flatMap((p) => p.value || []);
|
||||||
|
|
||||||
|
requestSans.forEach((san) => {
|
||||||
|
const validation = validateValueAgainstConstraints(san.value, allAllowedValues, `${sanType} SAN`);
|
||||||
|
if (!validation.isValid && validation.error) {
|
||||||
|
errors.push(validation.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.keyAlgorithm && template.keyAlgorithm) {
|
const requestSanTypes = new Set(request.subjectAlternativeNames?.map((san) => san.type) || []);
|
||||||
if (!template.keyAlgorithm?.allowedKeyTypes.includes(request.keyAlgorithm)) {
|
for (const requestSanType of requestSanTypes) {
|
||||||
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
|
if (!templateSanTypes.has(requestSanType)) {
|
||||||
|
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.signatureAlgorithm) {
|
||||||
|
if (template.signatureAlgorithm && template.signatureAlgorithm.allowedAlgorithms) {
|
||||||
|
const mappedTemplateAlgorithms = template.signatureAlgorithm.allowedAlgorithms.map(
|
||||||
|
mapTemplateSignatureAlgorithmToApi
|
||||||
|
);
|
||||||
|
if (!mappedTemplateAlgorithms.includes(request.signatureAlgorithm)) {
|
||||||
|
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
|
||||||
|
}
|
||||||
|
} else if (!template.signatureAlgorithm) {
|
||||||
|
errors.push(
|
||||||
|
`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy (not defined in template)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.keyAlgorithm) {
|
||||||
|
if (template.keyAlgorithm && template.keyAlgorithm.allowedKeyTypes) {
|
||||||
|
const mappedTemplateKeyTypes = template.keyAlgorithm.allowedKeyTypes.map(mapTemplateKeyAlgorithmToApi);
|
||||||
|
if (!mappedTemplateKeyTypes.includes(request.keyAlgorithm)) {
|
||||||
|
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
|
||||||
|
}
|
||||||
|
} else if (!template.keyAlgorithm) {
|
||||||
|
errors.push(
|
||||||
|
`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy (not defined in template)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +427,44 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.validity?.ttl && (request.notBefore || request.notAfter)) {
|
||||||
|
errors.push(
|
||||||
|
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.notBefore && request.notAfter && request.notBefore >= request.notAfter) {
|
||||||
|
errors.push("notBefore must be earlier than notAfter");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((request.notBefore || request.notAfter) && template.validity) {
|
||||||
|
const notBefore = request.notBefore || new Date();
|
||||||
|
const { notAfter } = request;
|
||||||
|
|
||||||
|
if (notAfter && notBefore && notAfter instanceof Date && notBefore instanceof Date) {
|
||||||
|
const requestDuration = notAfter.getTime() - notBefore.getTime();
|
||||||
|
|
||||||
|
const maxDuration = convertToMilliseconds(
|
||||||
|
template.validity.maxDuration.value,
|
||||||
|
template.validity.maxDuration.unit
|
||||||
|
);
|
||||||
|
|
||||||
|
if (requestDuration > maxDuration) {
|
||||||
|
errors.push(`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.validity.minDuration) {
|
||||||
|
const minDuration = convertToMilliseconds(
|
||||||
|
template.validity.minDuration.value,
|
||||||
|
template.validity.minDuration.unit
|
||||||
|
);
|
||||||
|
if (requestDuration < minDuration) {
|
||||||
|
errors.push(`Requested validity period (notBefore to notAfter) is below minimum required duration`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
errors,
|
errors,
|
||||||
@@ -375,15 +505,9 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
throw new Error("Template data is required");
|
throw new Error("Template data is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
validateTemplatePolicy({
|
if (data.attributes) {
|
||||||
attributes: data.attributes,
|
validateSubjectAttributePolicy(data.attributes);
|
||||||
keyUsages: data.keyUsages,
|
}
|
||||||
extendedKeyUsages: data.extendedKeyUsages,
|
|
||||||
subjectAlternativeNames: data.subjectAlternativeNames,
|
|
||||||
validity: data.validity,
|
|
||||||
signatureAlgorithm: data.signatureAlgorithm,
|
|
||||||
keyAlgorithm: data.keyAlgorithm
|
|
||||||
});
|
|
||||||
|
|
||||||
const slug = data.slug || generateTemplateSlug();
|
const slug = data.slug || generateTemplateSlug();
|
||||||
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
||||||
@@ -431,18 +555,8 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateTemplates
|
ProjectPermissionSub.CertificateTemplates
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasAnyPolicyField(data)) {
|
if (data.attributes) {
|
||||||
const mergedPolicy = {
|
validateSubjectAttributePolicy(data.attributes);
|
||||||
attributes: data.attributes || existingTemplate.attributes,
|
|
||||||
keyUsages: data.keyUsages || existingTemplate.keyUsages,
|
|
||||||
extendedKeyUsages: data.extendedKeyUsages || existingTemplate.extendedKeyUsages,
|
|
||||||
subjectAlternativeNames: data.subjectAlternativeNames || existingTemplate.subjectAlternativeNames,
|
|
||||||
validity: data.validity || existingTemplate.validity,
|
|
||||||
signatureAlgorithm: data.signatureAlgorithm || existingTemplate.signatureAlgorithm,
|
|
||||||
keyAlgorithm: data.keyAlgorithm || existingTemplate.keyAlgorithm
|
|
||||||
};
|
|
||||||
|
|
||||||
validateTemplatePolicy(mergedPolicy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData = { ...data };
|
const updateData = { ...data };
|
||||||
@@ -614,8 +728,14 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
|
|
||||||
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
|
||||||
if (isInUse) {
|
if (isInUse) {
|
||||||
|
const profilesUsingTemplate = await certificateTemplateV2DAL.getProfilesUsingTemplate(templateId);
|
||||||
|
const profileNames = profilesUsingTemplate.map((profile) => profile.slug || profile.id).join(", ");
|
||||||
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: "Cannot delete template that is in use by certificate profiles"
|
message:
|
||||||
|
profilesUsingTemplate.length > 0
|
||||||
|
? `Cannot delete template '${template.slug}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
|
||||||
|
: `Cannot delete template '${template.slug}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
|
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2";
|
||||||
|
import {
|
||||||
|
CertDurationUnit,
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertIncludeType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType,
|
||||||
|
CertSubjectAttributeType
|
||||||
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
|
|
||||||
export interface TTemplateV2Policy {
|
export interface TTemplateV2Policy {
|
||||||
attributes: Array<{
|
attributes: Array<{
|
||||||
type: "common_name";
|
type: CertSubjectAttributeType;
|
||||||
include: "mandatory" | "optional" | "prohibit";
|
include: CertIncludeType;
|
||||||
value?: string[];
|
value?: string[];
|
||||||
}>;
|
}>;
|
||||||
keyUsages: {
|
keyUsages: {
|
||||||
requiredUsages: { all: string[] };
|
requiredUsages?: { all: CertKeyUsageType[] };
|
||||||
optionalUsages: { all: string[] };
|
optionalUsages?: { all: CertKeyUsageType[] };
|
||||||
};
|
};
|
||||||
extendedKeyUsages: {
|
extendedKeyUsages: {
|
||||||
requiredUsages: { all: string[] };
|
requiredUsages?: { all: CertExtendedKeyUsageType[] };
|
||||||
optionalUsages: { all: string[] };
|
optionalUsages?: { all: CertExtendedKeyUsageType[] };
|
||||||
};
|
};
|
||||||
subjectAlternativeNames: Array<{
|
subjectAlternativeNames: Array<{
|
||||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
type: CertSubjectAlternativeNameType;
|
||||||
include: "mandatory" | "optional" | "prohibit";
|
include: CertIncludeType;
|
||||||
value?: string[];
|
value?: string[];
|
||||||
}>;
|
}>;
|
||||||
validity: {
|
validity: {
|
||||||
maxDuration: { value: number; unit: "days" | "months" | "years" };
|
maxDuration: { value: number; unit: CertDurationUnit };
|
||||||
minDuration?: { value: number; unit: "days" | "months" | "years" };
|
minDuration?: { value: number; unit: CertDurationUnit };
|
||||||
};
|
};
|
||||||
signatureAlgorithm: {
|
signatureAlgorithm: {
|
||||||
allowedAlgorithms: string[];
|
allowedAlgorithms: string[];
|
||||||
@@ -88,15 +96,25 @@ export type TCertificateTemplateV2Update = Partial<
|
|||||||
|
|
||||||
export interface TCertificateRequest {
|
export interface TCertificateRequest {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
keyUsages?: string[];
|
organizationName?: string;
|
||||||
extendedKeyUsages?: string[];
|
organizationUnit?: string;
|
||||||
|
locality?: string;
|
||||||
|
state?: string;
|
||||||
|
country?: string;
|
||||||
|
email?: string;
|
||||||
|
streetAddress?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
keyUsages?: CertKeyUsageType[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
subjectAlternativeNames?: Array<{
|
subjectAlternativeNames?: Array<{
|
||||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
type: CertSubjectAlternativeNameType;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
validity?: {
|
validity?: {
|
||||||
ttl: string;
|
ttl: string;
|
||||||
};
|
};
|
||||||
|
notBefore?: Date;
|
||||||
|
notAfter?: Date;
|
||||||
signatureAlgorithm?: string;
|
signatureAlgorithm?: string;
|
||||||
keyAlgorithm?: string;
|
keyAlgorithm?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,40 @@ import z from "zod";
|
|||||||
|
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
|
||||||
export const validateTemplateRegexField = z
|
export const createTemplateFieldValidator = (options?: {
|
||||||
.string()
|
minLength?: number;
|
||||||
.min(1)
|
maxLength?: number;
|
||||||
.max(100)
|
allowedCharacters?: CharacterType[];
|
||||||
.refine(
|
customMessage?: string;
|
||||||
(val) =>
|
}) => {
|
||||||
characterValidator([
|
const {
|
||||||
CharacterType.AlphaNumeric,
|
minLength = 1,
|
||||||
CharacterType.Spaces, // (space)
|
maxLength = 100,
|
||||||
CharacterType.Asterisk, // *
|
allowedCharacters = [
|
||||||
CharacterType.At, // @
|
CharacterType.AlphaNumeric,
|
||||||
CharacterType.Hyphen, // -
|
CharacterType.Spaces, // (space)
|
||||||
CharacterType.Period, // .
|
CharacterType.Asterisk, // *
|
||||||
CharacterType.Backslash // \
|
CharacterType.At, // @
|
||||||
])(val),
|
CharacterType.Hyphen, // -
|
||||||
{
|
CharacterType.Period, // .
|
||||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
CharacterType.Backslash // \
|
||||||
}
|
],
|
||||||
)
|
customMessage = "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
} = options || {};
|
||||||
.refine((v) => safe(v), {
|
|
||||||
message: "Unsafe REGEX pattern"
|
return (
|
||||||
});
|
z
|
||||||
|
.string()
|
||||||
|
.min(minLength)
|
||||||
|
.max(maxLength)
|
||||||
|
.refine((val) => characterValidator(allowedCharacters)(val), {
|
||||||
|
message: customMessage
|
||||||
|
})
|
||||||
|
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||||
|
.refine((v) => safe(v), {
|
||||||
|
message: "Unsafe REGEX pattern"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateTemplateRegexField = createTemplateFieldValidator();
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
import { ACMESANType, CertificateOrderStatus } from "@app/services/certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertIncludeType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAttributeType
|
||||||
|
} from "@app/services/certificate-common/certificate-constants";
|
||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
|
|
||||||
import { ActorType, AuthMethod } from "../auth/auth-type";
|
import { ActorType, AuthMethod } from "../auth/auth-type";
|
||||||
@@ -76,8 +82,8 @@ describe("CertificateV3Service", () => {
|
|||||||
describe("issueCertificateFromProfile", () => {
|
describe("issueCertificateFromProfile", () => {
|
||||||
const mockCertificateRequest = {
|
const mockCertificateRequest = {
|
||||||
commonName: "test.example.com",
|
commonName: "test.example.com",
|
||||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
|
||||||
validity: { ttl: "30d" },
|
validity: { ttl: "30d" },
|
||||||
signatureAlgorithm: "RSA-SHA256",
|
signatureAlgorithm: "RSA-SHA256",
|
||||||
keyAlgorithm: "RSA_2048"
|
keyAlgorithm: "RSA_2048"
|
||||||
@@ -102,12 +108,19 @@ describe("CertificateV3Service", () => {
|
|||||||
id: "template-123",
|
id: "template-123",
|
||||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||||
attributes: []
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateResult = {
|
const mockCertificateResult = {
|
||||||
certificate: Buffer.from("cert"),
|
certificate: Buffer.from("cert"),
|
||||||
certificateChain: Buffer.from("chain"),
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||||
privateKey: Buffer.from("key"),
|
privateKey: Buffer.from("key"),
|
||||||
serialNumber: "123456"
|
serialNumber: "123456"
|
||||||
};
|
};
|
||||||
@@ -136,6 +149,8 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toHaveProperty("certificate");
|
expect(result).toHaveProperty("certificate");
|
||||||
|
expect(result).toHaveProperty("issuingCaCertificate");
|
||||||
|
expect(result).toHaveProperty("certificateChain");
|
||||||
expect(result).toHaveProperty("privateKey");
|
expect(result).toHaveProperty("privateKey");
|
||||||
expect(result).toHaveProperty("serialNumber", "123456");
|
expect(result).toHaveProperty("serialNumber", "123456");
|
||||||
expect(result).toHaveProperty("certificateId", "cert-123");
|
expect(result).toHaveProperty("certificateId", "cert-123");
|
||||||
@@ -160,12 +175,19 @@ describe("CertificateV3Service", () => {
|
|||||||
id: "template-123",
|
id: "template-123",
|
||||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||||
attributes: []
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateResult = {
|
const mockCertificateResult = {
|
||||||
certificate: Buffer.from("cert"),
|
certificate: Buffer.from("cert"),
|
||||||
certificateChain: Buffer.from("chain"),
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||||
privateKey: Buffer.from("key"),
|
privateKey: Buffer.from("key"),
|
||||||
serialNumber: "123456"
|
serialNumber: "123456"
|
||||||
};
|
};
|
||||||
@@ -178,17 +200,17 @@ describe("CertificateV3Service", () => {
|
|||||||
const camelCaseRequest = {
|
const camelCaseRequest = {
|
||||||
commonName: "test.example.com",
|
commonName: "test.example.com",
|
||||||
keyUsages: [
|
keyUsages: [
|
||||||
CertKeyUsage.DIGITAL_SIGNATURE,
|
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||||
CertKeyUsage.NON_REPUDIATION,
|
CertKeyUsageType.NON_REPUDIATION,
|
||||||
CertKeyUsage.KEY_AGREEMENT,
|
CertKeyUsageType.KEY_AGREEMENT,
|
||||||
CertKeyUsage.CRL_SIGN,
|
CertKeyUsageType.CRL_SIGN,
|
||||||
CertKeyUsage.DECIPHER_ONLY
|
CertKeyUsageType.DECIPHER_ONLY
|
||||||
],
|
],
|
||||||
extendedKeyUsages: [
|
extendedKeyUsages: [
|
||||||
CertExtendedKeyUsage.CLIENT_AUTH,
|
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||||
CertExtendedKeyUsage.CODE_SIGNING,
|
CertExtendedKeyUsageType.CODE_SIGNING,
|
||||||
CertExtendedKeyUsage.OCSP_SIGNING,
|
CertExtendedKeyUsageType.OCSP_SIGNING,
|
||||||
CertExtendedKeyUsage.SERVER_AUTH
|
CertExtendedKeyUsageType.SERVER_AUTH
|
||||||
],
|
],
|
||||||
validity: { ttl: "10d" }
|
validity: { ttl: "10d" }
|
||||||
};
|
};
|
||||||
@@ -215,8 +237,19 @@ describe("CertificateV3Service", () => {
|
|||||||
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
|
||||||
"template-123",
|
"template-123",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
keyUsages: ["digital_signature", "non_repudiation", "key_agreement", "crl_sign", "decipher_only"],
|
keyUsages: [
|
||||||
extendedKeyUsages: ["client_auth", "code_signing", "ocsp_signing", "server_auth"]
|
CertKeyUsageType.DIGITAL_SIGNATURE,
|
||||||
|
CertKeyUsageType.NON_REPUDIATION,
|
||||||
|
CertKeyUsageType.KEY_AGREEMENT,
|
||||||
|
CertKeyUsageType.CRL_SIGN,
|
||||||
|
CertKeyUsageType.DECIPHER_ONLY
|
||||||
|
],
|
||||||
|
extendedKeyUsages: [
|
||||||
|
CertExtendedKeyUsageType.CLIENT_AUTH,
|
||||||
|
CertExtendedKeyUsageType.CODE_SIGNING,
|
||||||
|
CertExtendedKeyUsageType.OCSP_SIGNING,
|
||||||
|
CertExtendedKeyUsageType.SERVER_AUTH
|
||||||
|
]
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -286,6 +319,7 @@ describe("CertificateV3Service", () => {
|
|||||||
const mockSignResult = {
|
const mockSignResult = {
|
||||||
certificate: Buffer.from("signed-cert"),
|
certificate: Buffer.from("signed-cert"),
|
||||||
certificateChain: Buffer.from("chain"),
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||||
serialNumber: "789012"
|
serialNumber: "789012"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -308,6 +342,8 @@ describe("CertificateV3Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toHaveProperty("certificate");
|
expect(result).toHaveProperty("certificate");
|
||||||
|
expect(result).toHaveProperty("issuingCaCertificate");
|
||||||
|
expect(result).toHaveProperty("certificateChain");
|
||||||
expect(result).toHaveProperty("serialNumber", "789012");
|
expect(result).toHaveProperty("serialNumber", "789012");
|
||||||
expect(result).toHaveProperty("certificateId", "cert-456");
|
expect(result).toHaveProperty("certificateId", "cert-456");
|
||||||
expect(result).not.toHaveProperty("privateKey");
|
expect(result).not.toHaveProperty("privateKey");
|
||||||
@@ -347,11 +383,11 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
describe("orderCertificateFromProfile", () => {
|
describe("orderCertificateFromProfile", () => {
|
||||||
const mockCertificateOrder = {
|
const mockCertificateOrder = {
|
||||||
subjectAlternativeNames: [{ type: "dns" as const, value: "example.com" }],
|
altNames: [{ type: ACMESANType.DNS, value: "example.com" }],
|
||||||
validity: { ttl: "30d" },
|
validity: { ttl: "30d" },
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
|
||||||
signatureAlgorithm: "RSA-SHA256",
|
signatureAlgorithm: "RSA-SHA256",
|
||||||
keyAlgorithm: "RSA_2048"
|
keyAlgorithm: "RSA_2048"
|
||||||
};
|
};
|
||||||
@@ -375,12 +411,19 @@ describe("CertificateV3Service", () => {
|
|||||||
id: "template-123",
|
id: "template-123",
|
||||||
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
signatureAlgorithm: { defaultAlgorithm: "RSA-SHA256" },
|
||||||
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
keyAlgorithm: { defaultKeyType: "RSA_2048" },
|
||||||
attributes: []
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockCertificateResult = {
|
const mockCertificateResult = {
|
||||||
certificate: Buffer.from("cert"),
|
certificate: Buffer.from("cert"),
|
||||||
certificateChain: Buffer.from("chain"),
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("issuing-ca"),
|
||||||
privateKey: Buffer.from("key"),
|
privateKey: Buffer.from("key"),
|
||||||
serialNumber: "123456"
|
serialNumber: "123456"
|
||||||
};
|
};
|
||||||
@@ -413,9 +456,9 @@ describe("CertificateV3Service", () => {
|
|||||||
expect(result).toHaveProperty("certificate");
|
expect(result).toHaveProperty("certificate");
|
||||||
expect(result.subjectAlternativeNames).toHaveLength(1);
|
expect(result.subjectAlternativeNames).toHaveLength(1);
|
||||||
expect(result.subjectAlternativeNames[0]).toEqual({
|
expect(result.subjectAlternativeNames[0]).toEqual({
|
||||||
type: "dns",
|
type: ACMESANType.DNS,
|
||||||
value: "example.com",
|
value: "example.com",
|
||||||
status: "valid"
|
status: CertificateOrderStatus.VALID
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -448,4 +491,230 @@ describe("CertificateV3Service", () => {
|
|||||||
).rejects.toThrow("Profile is not configured for api enrollment");
|
).rejects.toThrow("Profile is not configured for api enrollment");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("algorithm compatibility (integration tests)", () => {
|
||||||
|
const mockProfile = {
|
||||||
|
id: "profile-1",
|
||||||
|
slug: "test-profile",
|
||||||
|
projectId: "project-1",
|
||||||
|
caId: "ca-1",
|
||||||
|
certificateTemplateId: "template-1",
|
||||||
|
enrollmentType: EnrollmentType.API
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCertificateRequest = {
|
||||||
|
commonName: "test.example.com",
|
||||||
|
validity: { ttl: "30d" },
|
||||||
|
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
|
||||||
|
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully process RSA algorithms with RSA CAs", async () => {
|
||||||
|
const rsaCa = {
|
||||||
|
id: "ca-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
status: "active",
|
||||||
|
internalCa: {
|
||||||
|
keyAlgorithm: "RSA_2048"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rsaTemplate = {
|
||||||
|
id: "template-1",
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: ["SHA256-RSA", "SHA384-RSA"]
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||||
|
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsaCa);
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||||
|
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate);
|
||||||
|
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||||
|
certificate: Buffer.from("cert"),
|
||||||
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||||
|
privateKey: Buffer.from("key"),
|
||||||
|
serialNumber: "123456"
|
||||||
|
});
|
||||||
|
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||||
|
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Should not throw - RSA CA is compatible with RSA signature algorithms
|
||||||
|
await expect(
|
||||||
|
service.issueCertificateFromProfile({
|
||||||
|
profileId: mockProfile.id,
|
||||||
|
certificateRequest: {
|
||||||
|
...mockCertificateRequest,
|
||||||
|
signatureAlgorithm: "RSA-SHA256"
|
||||||
|
},
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully process ECDSA algorithms with EC CAs", async () => {
|
||||||
|
const ecCa = {
|
||||||
|
id: "ca-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
status: "active",
|
||||||
|
internalCa: {
|
||||||
|
keyAlgorithm: "EC_prime256v1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ecdsaTemplate = {
|
||||||
|
id: "template-1",
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: ["SHA256-ECDSA", "SHA384-ECDSA"]
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||||
|
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(ecCa);
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||||
|
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate);
|
||||||
|
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||||
|
certificate: Buffer.from("cert"),
|
||||||
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||||
|
privateKey: Buffer.from("key"),
|
||||||
|
serialNumber: "123456"
|
||||||
|
});
|
||||||
|
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||||
|
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Should not throw - EC CA is compatible with ECDSA signature algorithms
|
||||||
|
await expect(
|
||||||
|
service.issueCertificateFromProfile({
|
||||||
|
profileId: mockProfile.id,
|
||||||
|
certificateRequest: {
|
||||||
|
...mockCertificateRequest,
|
||||||
|
signatureAlgorithm: "ECDSA-SHA256"
|
||||||
|
},
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dynamically support new RSA key sizes", async () => {
|
||||||
|
const rsa8192Ca = {
|
||||||
|
id: "ca-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
status: "active",
|
||||||
|
internalCa: {
|
||||||
|
keyAlgorithm: "RSA_8192" // Future RSA key size
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rsaTemplate = {
|
||||||
|
id: "template-1",
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: ["SHA256-RSA"]
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||||
|
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(rsa8192Ca);
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||||
|
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(rsaTemplate);
|
||||||
|
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||||
|
certificate: Buffer.from("cert"),
|
||||||
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||||
|
privateKey: Buffer.from("key"),
|
||||||
|
serialNumber: "123456"
|
||||||
|
});
|
||||||
|
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||||
|
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Should not throw - dynamic check supports new RSA key sizes
|
||||||
|
await expect(
|
||||||
|
service.issueCertificateFromProfile({
|
||||||
|
profileId: mockProfile.id,
|
||||||
|
certificateRequest: {
|
||||||
|
...mockCertificateRequest,
|
||||||
|
signatureAlgorithm: "RSA-SHA256"
|
||||||
|
},
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dynamically support new EC curve types", async () => {
|
||||||
|
const newEcCa = {
|
||||||
|
id: "ca-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
status: "active",
|
||||||
|
internalCa: {
|
||||||
|
keyAlgorithm: "EC_secp521r1" // Future EC curve
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ecdsaTemplate = {
|
||||||
|
id: "template-1",
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: ["SHA384-ECDSA"]
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: CertSubjectAttributeType.COMMON_NAME,
|
||||||
|
include: CertIncludeType.OPTIONAL,
|
||||||
|
value: ["example.com"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
|
||||||
|
mockCertificateAuthorityDAL.findByIdWithAssociatedCa.mockResolvedValue(newEcCa);
|
||||||
|
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({ isValid: true, errors: [] });
|
||||||
|
mockCertificateTemplateV2Service.getTemplateV2ById.mockResolvedValue(ecdsaTemplate);
|
||||||
|
mockInternalCaService.issueCertFromCa.mockResolvedValue({
|
||||||
|
certificate: Buffer.from("cert"),
|
||||||
|
certificateChain: Buffer.from("chain"),
|
||||||
|
issuingCaCertificate: Buffer.from("ca-cert"),
|
||||||
|
privateKey: Buffer.from("key"),
|
||||||
|
serialNumber: "123456"
|
||||||
|
});
|
||||||
|
mockCertificateDAL.findOne.mockResolvedValue({ id: "cert-1" });
|
||||||
|
mockCertificateDAL.updateById.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// Should not throw - dynamic check supports new EC curves
|
||||||
|
await expect(
|
||||||
|
service.issueCertificateFromProfile({
|
||||||
|
profileId: mockProfile.id,
|
||||||
|
certificateRequest: {
|
||||||
|
...mockCertificateRequest,
|
||||||
|
signatureAlgorithm: "ECDSA-SHA384"
|
||||||
|
},
|
||||||
|
...mockActor
|
||||||
|
})
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
|
import { CertificateOrderStatus } from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
TCertificateAuthorityDALFactory,
|
TCertificateAuthorityDALFactory,
|
||||||
TCertificateAuthorityWithAssociatedCa
|
TCertificateAuthorityWithAssociatedCa
|
||||||
@@ -20,10 +21,13 @@ import { TCertificateProfileDALFactory } from "@app/services/certificate-profile
|
|||||||
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
|
||||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||||
|
|
||||||
|
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
|
||||||
import {
|
import {
|
||||||
bufferToString,
|
bufferToString,
|
||||||
buildCertificateSubjectFromTemplate,
|
buildCertificateSubjectFromTemplate,
|
||||||
buildSubjectAlternativeNamesFromTemplate,
|
buildSubjectAlternativeNamesFromTemplate,
|
||||||
|
convertExtendedKeyUsageArrayToLegacy,
|
||||||
|
convertKeyUsageArrayToLegacy,
|
||||||
mapEnumsForValidation,
|
mapEnumsForValidation,
|
||||||
normalizeDateForApi
|
normalizeDateForApi
|
||||||
} from "../certificate-common/certificate-utils";
|
} from "../certificate-common/certificate-utils";
|
||||||
@@ -95,6 +99,45 @@ const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation:
|
|||||||
return caType;
|
return caType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateAlgorithmCompatibility = (
|
||||||
|
ca: TCertificateAuthorityWithAssociatedCa,
|
||||||
|
template: {
|
||||||
|
signatureAlgorithm?: {
|
||||||
|
allowedAlgorithms?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (!template.signatureAlgorithm || !template.signatureAlgorithm.allowedAlgorithms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caKeyAlgorithm = ca.internalCa?.keyAlgorithm;
|
||||||
|
if (!caKeyAlgorithm) {
|
||||||
|
throw new BadRequestError({ message: "CA key algorithm not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleAlgorithms = template.signatureAlgorithm.allowedAlgorithms.filter((sigAlg: string) => {
|
||||||
|
const parts = sigAlg.split("-");
|
||||||
|
const keyType = parts[parts.length - 1];
|
||||||
|
|
||||||
|
if (caKeyAlgorithm.startsWith("RSA")) {
|
||||||
|
return keyType === "RSA";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caKeyAlgorithm.startsWith("EC")) {
|
||||||
|
return keyType === "ECDSA";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (compatibleAlgorithms.length === 0) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Template signature algorithms (${template.signatureAlgorithm.allowedAlgorithms.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | string): string => {
|
const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | string): string => {
|
||||||
if (typeof certData === "string") return certData;
|
if (typeof certData === "string") return certData;
|
||||||
if (Buffer.isBuffer(certData)) return bufferToString(certData);
|
if (Buffer.isBuffer(certData)) return bufferToString(certData);
|
||||||
@@ -131,6 +174,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
EnrollmentType.API
|
EnrollmentType.API
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (certificateRequest.commonName && Array.isArray(certificateRequest.commonName)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Common Name must be a single value, not an array"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
|
||||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||||
profile.certificateTemplateId,
|
profile.certificateTemplateId,
|
||||||
@@ -166,33 +215,48 @@ export const certificateV3ServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
const effectiveSignatureAlgorithm =
|
const effectiveSignatureAlgorithm =
|
||||||
certificateRequest.signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
certificateRequest.signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||||
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||||
|
|
||||||
|
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.attributes);
|
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.attributes);
|
||||||
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
|
||||||
certificateRequest,
|
{ subjectAlternativeNames: certificateRequest.altNames },
|
||||||
template.subjectAlternativeNames
|
template.subjectAlternativeNames
|
||||||
);
|
);
|
||||||
|
|
||||||
const { certificate, certificateChain, privateKey, serialNumber } = await internalCaService.issueCertFromCa({
|
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
|
||||||
caId: ca.id,
|
await internalCaService.issueCertFromCa({
|
||||||
friendlyName: certificateSubject.common_name || "Certificate",
|
caId: ca.id,
|
||||||
commonName: certificateSubject.common_name || "",
|
friendlyName: certificateSubject.common_name || "Certificate",
|
||||||
altNames: subjectAlternativeNames,
|
commonName: certificateSubject.common_name || "",
|
||||||
ttl: certificateRequest.validity.ttl,
|
altNames: subjectAlternativeNames,
|
||||||
keyUsages: certificateRequest.keyUsages,
|
ttl: certificateRequest.validity.ttl,
|
||||||
extendedKeyUsages: certificateRequest.extendedKeyUsages,
|
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
|
||||||
notBefore: normalizeDateForApi(certificateRequest.notBefore),
|
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
|
||||||
notAfter: normalizeDateForApi(certificateRequest.notAfter),
|
notBefore: normalizeDateForApi(certificateRequest.notBefore),
|
||||||
signatureAlgorithm: effectiveSignatureAlgorithm,
|
notAfter: normalizeDateForApi(certificateRequest.notAfter),
|
||||||
keyAlgorithm: effectiveKeyAlgorithm,
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
actor,
|
keyAlgorithm: effectiveKeyAlgorithm,
|
||||||
actorId,
|
actor,
|
||||||
actorAuthMethod,
|
actorId,
|
||||||
actorOrgId
|
actorAuthMethod,
|
||||||
});
|
actorOrgId
|
||||||
|
});
|
||||||
|
|
||||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||||
if (!cert) {
|
if (!cert) {
|
||||||
@@ -201,14 +265,15 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
await certificateDAL.updateById(cert.id, { profileId });
|
await certificateDAL.updateById(cert.id, { profileId });
|
||||||
|
|
||||||
const certificateChainString = bufferToString(certificateChain);
|
|
||||||
return {
|
return {
|
||||||
certificate: bufferToString(certificate),
|
certificate: bufferToString(certificate),
|
||||||
issuingCaCertificate: certificateChainString.split("\n").pop() || bufferToString(certificate),
|
issuingCaCertificate: bufferToString(issuingCaCertificate),
|
||||||
certificateChain: certificateChainString,
|
certificateChain: bufferToString(certificateChain),
|
||||||
privateKey: bufferToString(privateKey),
|
privateKey: bufferToString(privateKey),
|
||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id
|
certificateId: cert.id,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
profileName: profile.slug
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,6 +283,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
validity,
|
validity,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter,
|
notAfter,
|
||||||
|
signatureAlgorithm,
|
||||||
|
keyAlgorithm,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@@ -241,16 +308,52 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
validateCaSupport(ca, "CSR signing");
|
validateCaSupport(ca, "CSR signing");
|
||||||
|
|
||||||
const { certificate, certificateChain, serialNumber } = await internalCaService.signCertFromCa({
|
if (!actorAuthMethod) {
|
||||||
isInternal: true,
|
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
|
||||||
caId: ca.id,
|
}
|
||||||
csr,
|
|
||||||
ttl: validity.ttl,
|
const template = await certificateTemplateV2Service.getTemplateV2ById({
|
||||||
altNames: "",
|
actor,
|
||||||
notBefore: normalizeDateForApi(notBefore),
|
actorId,
|
||||||
notAfter: normalizeDateForApi(notAfter)
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
templateId: profile.certificateTemplateId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!template) {
|
||||||
|
throw new NotFoundError({ message: "Certificate template not found for this profile" });
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAlgorithmCompatibility(ca, template);
|
||||||
|
|
||||||
|
const effectiveSignatureAlgorithm = signatureAlgorithm || template.signatureAlgorithm?.defaultAlgorithm;
|
||||||
|
const effectiveKeyAlgorithm = keyAlgorithm || template.keyAlgorithm?.defaultKeyType;
|
||||||
|
|
||||||
|
if (template.keyAlgorithm?.allowedKeyTypes && !effectiveKeyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Key algorithm is required by template policy but not provided in request or template default"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.signatureAlgorithm?.allowedAlgorithms && !effectiveSignatureAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Signature algorithm is required by template policy but not provided in request or template default"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
|
||||||
|
await internalCaService.signCertFromCa({
|
||||||
|
isInternal: true,
|
||||||
|
caId: ca.id,
|
||||||
|
csr,
|
||||||
|
ttl: validity.ttl,
|
||||||
|
altNames: "",
|
||||||
|
notBefore: normalizeDateForApi(notBefore),
|
||||||
|
notAfter: normalizeDateForApi(notAfter),
|
||||||
|
signatureAlgorithm: effectiveSignatureAlgorithm,
|
||||||
|
keyAlgorithm: effectiveKeyAlgorithm
|
||||||
|
});
|
||||||
|
|
||||||
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
|
||||||
if (!cert) {
|
if (!cert) {
|
||||||
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
|
||||||
@@ -263,10 +366,12 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
certificate: certificateString,
|
certificate: certificateString,
|
||||||
issuingCaCertificate: certificateChainString.split("\n").pop() || certificateString,
|
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer),
|
||||||
certificateChain: certificateChainString,
|
certificateChain: certificateChainString,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
certificateId: cert.id
|
certificateId: cert.id,
|
||||||
|
projectId: profile.projectId,
|
||||||
|
profileName: profile.slug
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,8 +398,8 @@ export const certificateV3ServiceFactory = ({
|
|||||||
commonName: certificateOrder.commonName,
|
commonName: certificateOrder.commonName,
|
||||||
keyUsages: certificateOrder.keyUsages,
|
keyUsages: certificateOrder.keyUsages,
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
subjectAlternativeNames: certificateOrder.subjectAlternativeNames.map((san) => ({
|
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||||
type: san.type === "dns" ? ("dns_name" as const) : ("ip_address" as const),
|
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
|
||||||
value: san.value
|
value: san.value
|
||||||
})),
|
})),
|
||||||
validity: certificateOrder.validity,
|
validity: certificateOrder.validity,
|
||||||
@@ -334,43 +439,26 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const orderId = randomUUID();
|
const orderId = randomUUID();
|
||||||
const subjectAlternativeNames = certificateOrder.subjectAlternativeNames.map((san) => ({
|
|
||||||
type: san.type,
|
|
||||||
value: san.value,
|
|
||||||
status: "valid" as const
|
|
||||||
}));
|
|
||||||
|
|
||||||
const authorizations = certificateOrder.subjectAlternativeNames.map((san) => ({
|
|
||||||
identifier: {
|
|
||||||
type: san.type,
|
|
||||||
value: san.value
|
|
||||||
},
|
|
||||||
status: "valid" as const,
|
|
||||||
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
challenges: [
|
|
||||||
{
|
|
||||||
type: "internal-validation",
|
|
||||||
status: "valid" as const,
|
|
||||||
url: `/api/v3/certificates/orders/${orderId}/internal`,
|
|
||||||
token: "internal-ca-validation"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orderId,
|
orderId,
|
||||||
status: "valid",
|
status: CertificateOrderStatus.VALID,
|
||||||
subjectAlternativeNames,
|
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
|
||||||
authorizations,
|
type: san.type,
|
||||||
finalize: `/api/v3/certificates/orders/${orderId}/finalize`,
|
value: san.value,
|
||||||
certificate: certificateResult.certificate
|
status: CertificateOrderStatus.VALID
|
||||||
|
})),
|
||||||
|
authorizations: [],
|
||||||
|
finalize: `/api/v3/certificates/orders/${orderId}/completed`,
|
||||||
|
certificate: certificateResult.certificate,
|
||||||
|
projectId: certificateResult.projectId,
|
||||||
|
profileName: certificateResult.profileName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caType === CaType.ACME) {
|
if (caType === CaType.ACME) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message: "ACME certificate ordering via profiles is not yet implemented."
|
||||||
"ACME certificate ordering via profiles is not yet implemented. Use direct certificate issuance for ACME CAs."
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
|
||||||
|
import {
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType
|
||||||
|
} from "../certificate-common/certificate-constants";
|
||||||
|
|
||||||
export type TIssueCertificateFromProfileDTO = {
|
export type TIssueCertificateFromProfileDTO = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
certificateRequest: {
|
certificateRequest: {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsageType[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
subjectAlternativeNames?: Array<{
|
altNames?: Array<{
|
||||||
type: "dns_name" | "ip_address" | "email" | "uri";
|
type: CertSubjectAlternativeNameType;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
validity: {
|
validity: {
|
||||||
@@ -30,21 +35,23 @@ export type TSignCertificateFromProfileDTO = {
|
|||||||
};
|
};
|
||||||
notBefore?: Date;
|
notBefore?: Date;
|
||||||
notAfter?: Date;
|
notAfter?: Date;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TOrderCertificateFromProfileDTO = {
|
export type TOrderCertificateFromProfileDTO = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
certificateOrder: {
|
certificateOrder: {
|
||||||
subjectAlternativeNames: Array<{
|
altNames: Array<{
|
||||||
type: "dns" | "ip";
|
type: ACMESANType;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
validity: {
|
validity: {
|
||||||
ttl: string;
|
ttl: string;
|
||||||
};
|
};
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsageType[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsageType[];
|
||||||
notBefore?: Date;
|
notBefore?: Date;
|
||||||
notAfter?: Date;
|
notAfter?: Date;
|
||||||
signatureAlgorithm?: string;
|
signatureAlgorithm?: string;
|
||||||
@@ -59,30 +66,34 @@ export type TCertificateFromProfileResponse = {
|
|||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
|
projectId: string;
|
||||||
|
profileName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateOrderResponse = {
|
export type TCertificateOrderResponse = {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: CertificateOrderStatus;
|
||||||
subjectAlternativeNames: Array<{
|
subjectAlternativeNames: Array<{
|
||||||
type: "dns" | "ip";
|
type: ACMESANType;
|
||||||
value: string;
|
value: string;
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: CertificateOrderStatus;
|
||||||
}>;
|
}>;
|
||||||
authorizations: Array<{
|
authorizations: Array<{
|
||||||
identifier: {
|
identifier: {
|
||||||
type: "dns" | "ip";
|
type: ACMESANType;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: CertificateOrderStatus;
|
||||||
expires?: string;
|
expires?: string;
|
||||||
challenges: Array<{
|
challenges: Array<{
|
||||||
type: string;
|
type: string;
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: CertificateOrderStatus;
|
||||||
url: string;
|
url: string;
|
||||||
token: string;
|
token: string;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
finalize: string;
|
finalize: string;
|
||||||
certificate?: string;
|
certificate?: string;
|
||||||
|
projectId: string;
|
||||||
|
profileName: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ export enum CertKeyAlgorithm {
|
|||||||
ECDSA_P384 = "EC_secp384r1"
|
ECDSA_P384 = "EC_secp384r1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertSignatureAlgorithm {
|
||||||
|
RSA_SHA256 = "RSA-SHA256",
|
||||||
|
RSA_SHA384 = "RSA-SHA384",
|
||||||
|
RSA_SHA512 = "RSA-SHA512",
|
||||||
|
ECDSA_SHA256 = "ECDSA-SHA256",
|
||||||
|
ECDSA_SHA384 = "ECDSA-SHA384",
|
||||||
|
ECDSA_SHA512 = "ECDSA-SHA512"
|
||||||
|
}
|
||||||
|
|
||||||
export enum CertKeyUsage {
|
export enum CertKeyUsage {
|
||||||
DIGITAL_SIGNATURE = "digitalSignature",
|
DIGITAL_SIGNATURE = "digitalSignature",
|
||||||
KEY_ENCIPHERMENT = "keyEncipherment",
|
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||||
@@ -111,7 +120,26 @@ export enum TAltNameType {
|
|||||||
IP = "ip",
|
IP = "ip",
|
||||||
URL = "url"
|
URL = "url"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertSubjectAlternativeNameType {
|
||||||
|
DNS_NAME = "dns_name",
|
||||||
|
IP_ADDRESS = "ip_address",
|
||||||
|
EMAIL = "email",
|
||||||
|
URI = "uri"
|
||||||
|
}
|
||||||
export type TAltNameMapping = {
|
export type TAltNameMapping = {
|
||||||
type: TAltNameType;
|
type: TAltNameType;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum ACMESANType {
|
||||||
|
DNS = "dns",
|
||||||
|
IP = "ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertificateOrderStatus {
|
||||||
|
PENDING = "pending",
|
||||||
|
PROCESSING = "processing",
|
||||||
|
VALID = "valid",
|
||||||
|
INVALID = "invalid"
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteById = async (id: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).del().returning("*");
|
|
||||||
|
|
||||||
return estConfig;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Delete EST enrollment config" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first();
|
const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first();
|
||||||
@@ -52,25 +42,10 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isConfigInUse = async (configId: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const profileCount = await (tx || db)(TableName.CertificateProfile)
|
|
||||||
.where({ estConfigId: configId })
|
|
||||||
.count("* as count")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return parseInt(profileCount || "0", 10) > 0;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Check if EST enrollment config is in use" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...estEnrollmentConfigOrm,
|
...estEnrollmentConfigOrm,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
deleteById,
|
findById
|
||||||
findById,
|
|
||||||
isConfigInUse
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/pki/certificate-profiles"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/pki/certificate-profiles/{id}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/pki/certificate-profiles/{id}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Slug"
|
||||||
|
openapi: "GET /api/v1/pki/certificate-profiles/slug/{slug}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List Certificates"
|
||||||
|
openapi: "GET /api/v1/pki/certificate-profiles/{id}/certificates"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/pki/certificate-profiles"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/pki/certificate-profiles/{id}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v2/certificate-templates"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v2/certificate-templates/{id}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v2/certificate-templates/{id}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v2/certificate-templates"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v2/certificate-templates/{id}"
|
||||||
|
---
|
||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Create"
|
title: "Create"
|
||||||
openapi: "POST /api/v1/pki/certificate-templates"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Delete"
|
title: "Delete"
|
||||||
openapi: "DELETE /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Get by ID"
|
title: "Get by ID"
|
||||||
openapi: "GET /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Update"
|
title: "Update"
|
||||||
openapi: "PATCH /api/v1/pki/certificate-templates/{certificateTemplateId}"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Create"
|
title: "Create"
|
||||||
openapi: "POST /api/v1/pki/subscribers"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Delete"
|
title: "Delete"
|
||||||
openapi: "DELETE /api/v1/pki/subscribers/{subscriberName}"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Retrieve latest certificate bundle"
|
title: "Retrieve latest certificate bundle"
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Issue Certificate"
|
title: "Issue Certificate"
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "List Certificates"
|
title: "List Certificates"
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}/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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Order Certificate"
|
title: "Order Certificate"
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Retrieve"
|
title: "Retrieve"
|
||||||
openapi: "GET /api/v1/pki/subscribers/{subscriberName}"
|
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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Sign Certificate"
|
title: "Sign Certificate"
|
||||||
openapi: "POST /api/v1/pki/subscribers/{subscriberName}/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>
|
||||||
|
|||||||
@@ -2,3 +2,9 @@
|
|||||||
title: "Update"
|
title: "Update"
|
||||||
openapi: "PATCH /api/v1/pki/subscribers/{subscriberName}"
|
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>
|
||||||
|
|||||||
+24
-4
@@ -2493,10 +2493,21 @@
|
|||||||
{
|
{
|
||||||
"group": "Certificate Templates",
|
"group": "Certificate Templates",
|
||||||
"pages": [
|
"pages": [
|
||||||
"api-reference/endpoints/certificate-templates/create",
|
"api-reference/endpoints/certificate-templates-v2/list",
|
||||||
"api-reference/endpoints/certificate-templates/update",
|
"api-reference/endpoints/certificate-templates-v2/create",
|
||||||
"api-reference/endpoints/certificate-templates/get-by-id",
|
"api-reference/endpoints/certificate-templates-v2/update",
|
||||||
"api-reference/endpoints/certificate-templates/delete"
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2520,6 +2531,15 @@
|
|||||||
"api-reference/endpoints/pki-alerts/delete"
|
"api-reference/endpoints/pki-alerts/delete"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Certificate Profiles",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/certificate-profiles/create",
|
||||||
|
"api-reference/endpoints/certificate-profiles/update",
|
||||||
|
"api-reference/endpoints/certificate-profiles/get-by-id",
|
||||||
|
"api-reference/endpoints/certificate-profiles/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Certificate Syncs",
|
"group": "Certificate Syncs",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionAuditLogsActions,
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ export enum ProjectPermissionPkiTemplateActions {
|
|||||||
ListCerts = "list-certs"
|
ListCerts = "list-certs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCertificateProfileActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueCert = "issue-cert"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretRotationActions {
|
export enum ProjectPermissionSecretRotationActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
ReadGeneratedCredentials = "read-generated-credentials",
|
ReadGeneratedCredentials = "read-generated-credentials",
|
||||||
@@ -217,6 +225,7 @@ 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
|
||||||
@@ -293,6 +302,7 @@ export enum ProjectPermissionSub {
|
|||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
PkiSubscribers = "pki-subscribers",
|
PkiSubscribers = "pki-subscribers",
|
||||||
|
CertificateProfiles = "certificate-profiles",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
Cmek = "cmek",
|
Cmek = "cmek",
|
||||||
SecretSyncs = "secret-syncs",
|
SecretSyncs = "secret-syncs",
|
||||||
@@ -470,6 +480,7 @@ export type ProjectPermissionSet =
|
|||||||
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionAuditLogsActions,
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
|
|||||||
case ProjectType.SecretManager:
|
case ProjectType.SecretManager:
|
||||||
return "/projects/secret-management/$projectId/overview" as const;
|
return "/projects/secret-management/$projectId/overview" as const;
|
||||||
case ProjectType.CertificateManager:
|
case ProjectType.CertificateManager:
|
||||||
return "/projects/cert-management/$projectId/subscribers" as const;
|
return "/projects/cert-management/$projectId/policies" as const;
|
||||||
case ProjectType.SecretScanning:
|
case ProjectType.SecretScanning:
|
||||||
return `/projects/${type}/$projectId/data-sources` as const;
|
return `/projects/${type}/$projectId/data-sources` as const;
|
||||||
case ProjectType.PAM:
|
case ProjectType.PAM:
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||||
|
export type { TOrderCertificateDTO, TOrderCertificateResponse } from "./types";
|
||||||
export {
|
export {
|
||||||
useCreateCa,
|
useCreateCa,
|
||||||
useCreateCertificate,
|
useCreateCertificate,
|
||||||
useCreateCertificateV3,
|
useCreateCertificateV3,
|
||||||
useDeleteCa,
|
useDeleteCa,
|
||||||
useImportCaCertificate,
|
useImportCaCertificate,
|
||||||
|
useOrderCertificateWithProfile,
|
||||||
useRenewCa,
|
useRenewCa,
|
||||||
useSignIntermediate,
|
useSignIntermediate,
|
||||||
useUpdateCa
|
useUpdateCa
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
TDeleteCertificateAuthorityDTO,
|
TDeleteCertificateAuthorityDTO,
|
||||||
TImportCaCertificateDTO,
|
TImportCaCertificateDTO,
|
||||||
TImportCaCertificateResponse,
|
TImportCaCertificateResponse,
|
||||||
|
TOrderCertificateDTO,
|
||||||
|
TOrderCertificateResponse,
|
||||||
TRenewCaDTO,
|
TRenewCaDTO,
|
||||||
TRenewCaResponse,
|
TRenewCaResponse,
|
||||||
TSignIntermediateDTO,
|
TSignIntermediateDTO,
|
||||||
@@ -168,6 +170,24 @@ export const useCreateCertificateV3 = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useOrderCertificateWithProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TOrderCertificateResponse, object, TOrderCertificateDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { data } = await apiRequest.post<TOrderCertificateResponse>(
|
||||||
|
"/api/v3/certificates/order-certificate",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useRenewCa = () => {
|
export const useRenewCa = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export type TCreateCertificateDTO = {
|
|||||||
pkiCollectionId?: string;
|
pkiCollectionId?: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
altNames: string; // sans
|
subjectAltNames: string; // sans
|
||||||
ttl: string; // string compatible with ms
|
ttl: string; // string compatible with ms
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
@@ -185,7 +185,7 @@ export type TCreateCertificateV3DTO = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
streetAddress?: string;
|
streetAddress?: string;
|
||||||
postalCode?: string;
|
postalCode?: string;
|
||||||
altNames: string;
|
subjectAltNames: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
@@ -197,6 +197,54 @@ export type TCreateCertificateV3DTO = {
|
|||||||
|
|
||||||
export type TCreateCertificateV3Response = TCreateCertificateResponse;
|
export type TCreateCertificateV3Response = TCreateCertificateResponse;
|
||||||
|
|
||||||
|
export type TOrderCertificateDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
profileId: string;
|
||||||
|
subjectAlternativeNames: Array<{
|
||||||
|
type: "dns" | "ip";
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
ttl: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
commonName?: string;
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrderCertificateResponse = {
|
||||||
|
orderId: string;
|
||||||
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
|
subjectAlternativeNames: Array<{
|
||||||
|
type: "dns" | "ip";
|
||||||
|
value: string;
|
||||||
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
|
}>;
|
||||||
|
authorizations: Array<{
|
||||||
|
identifier: {
|
||||||
|
type: "dns" | "ip";
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
|
expires?: string;
|
||||||
|
challenges: Array<{
|
||||||
|
type: string;
|
||||||
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
validated?: string;
|
||||||
|
error?: any;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
certificate?: string;
|
||||||
|
privateKey?: string;
|
||||||
|
expires: string;
|
||||||
|
notBefore: string;
|
||||||
|
notAfter: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TRenewCaDTO = {
|
export type TRenewCaDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
|
|||||||
@@ -124,16 +124,7 @@ export type TListCertificateTemplatesDTO = {
|
|||||||
|
|
||||||
export type TCertificateTemplateV2Policy = {
|
export type TCertificateTemplateV2Policy = {
|
||||||
attributes: Array<{
|
attributes: Array<{
|
||||||
type:
|
type: "common_name";
|
||||||
| "common_name"
|
|
||||||
| "organization_name"
|
|
||||||
| "organization_unit"
|
|
||||||
| "locality"
|
|
||||||
| "state"
|
|
||||||
| "country"
|
|
||||||
| "email"
|
|
||||||
| "street_address"
|
|
||||||
| "postal_code";
|
|
||||||
include: "mandatory" | "optional" | "prohibit";
|
include: "mandatory" | "optional" | "prohibit";
|
||||||
value?: string[];
|
value?: string[];
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -25,22 +25,22 @@ export enum CrlReason {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum CertKeyUsage {
|
export enum CertKeyUsage {
|
||||||
DIGITAL_SIGNATURE = "digitalSignature",
|
DIGITAL_SIGNATURE = "digital_signature",
|
||||||
KEY_ENCIPHERMENT = "keyEncipherment",
|
KEY_ENCIPHERMENT = "key_encipherment",
|
||||||
NON_REPUDIATION = "nonRepudiation",
|
NON_REPUDIATION = "non_repudiation",
|
||||||
DATA_ENCIPHERMENT = "dataEncipherment",
|
DATA_ENCIPHERMENT = "data_encipherment",
|
||||||
KEY_AGREEMENT = "keyAgreement",
|
KEY_AGREEMENT = "key_agreement",
|
||||||
KEY_CERT_SIGN = "keyCertSign",
|
KEY_CERT_SIGN = "key_cert_sign",
|
||||||
CRL_SIGN = "cRLSign",
|
CRL_SIGN = "crl_sign",
|
||||||
ENCIPHER_ONLY = "encipherOnly",
|
ENCIPHER_ONLY = "encipher_only",
|
||||||
DECIPHER_ONLY = "decipherOnly"
|
DECIPHER_ONLY = "decipher_only"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CertExtendedKeyUsage {
|
export enum CertExtendedKeyUsage {
|
||||||
CLIENT_AUTH = "clientAuth",
|
CLIENT_AUTH = "client_auth",
|
||||||
SERVER_AUTH = "serverAuth",
|
SERVER_AUTH = "server_auth",
|
||||||
CODE_SIGNING = "codeSigning",
|
CODE_SIGNING = "code_signing",
|
||||||
EMAIL_PROTECTION = "emailProtection",
|
EMAIL_PROTECTION = "email_protection",
|
||||||
TIMESTAMPING = "timeStamping",
|
TIMESTAMPING = "time_stamping",
|
||||||
OCSP_SIGNING = "ocspSigning"
|
OCSP_SIGNING = "ocsp_signing"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type TCertificate = {
|
|||||||
status: CertStatus;
|
status: CertStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
commonName: string;
|
commonName: string;
|
||||||
altNames: string;
|
subjectAltNames: string;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
notBefore: string;
|
notBefore: string;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Link, Outlet } from "@tanstack/react-router";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||||
import { useProject, useProjectPermission } from "@app/context";
|
import { useProject, useProjectPermission, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useListWorkspaceCertificateTemplates,
|
useListWorkspaceCertificateTemplates,
|
||||||
useListWorkspacePkiSubscribers
|
useListWorkspacePkiSubscribers
|
||||||
@@ -29,6 +29,7 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
|
|||||||
export const PkiManagerLayout = () => {
|
export const PkiManagerLayout = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||||
@@ -39,7 +40,8 @@ export const PkiManagerLayout = () => {
|
|||||||
|
|
||||||
const hasExistingSubscribers = subscribers.length > 0;
|
const hasExistingSubscribers = subscribers.length > 0;
|
||||||
const hasExistingTemplates = templates.length > 0;
|
const hasExistingTemplates = templates.length > 0;
|
||||||
const showLegacySection = hasExistingSubscribers || hasExistingTemplates;
|
const showLegacySection =
|
||||||
|
subscription.pkiLegacyTemplates || hasExistingSubscribers || hasExistingTemplates;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -166,7 +168,7 @@ export const PkiManagerLayout = () => {
|
|||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
{showLegacySection && (
|
{showLegacySection && (
|
||||||
<MenuGroup title="Legacy">
|
<MenuGroup title="Legacy">
|
||||||
{hasExistingSubscribers && (
|
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/subscribers"
|
to="/projects/cert-management/$projectId/subscribers"
|
||||||
params={{
|
params={{
|
||||||
@@ -185,7 +187,7 @@ export const PkiManagerLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{hasExistingTemplates && (
|
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/certificate-templates"
|
to="/projects/cert-management/$projectId/certificate-templates"
|
||||||
params={{
|
params={{
|
||||||
|
|||||||
-20
@@ -7,7 +7,6 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
@@ -24,8 +23,6 @@ const schema = z.object({
|
|||||||
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
certificatePem: z.string().trim().min(1, "Certificate PEM is required"),
|
||||||
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
privateKeyPem: z.string().trim().min(1, "Private Key PEM is required"),
|
||||||
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
chainPem: z.string().trim().min(1, "Certificate Chain PEM is required"),
|
||||||
|
|
||||||
friendlyName: z.string(),
|
|
||||||
collectionId: z.string().optional()
|
collectionId: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,7 +69,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
certificatePem,
|
certificatePem,
|
||||||
privateKeyPem,
|
privateKeyPem,
|
||||||
chainPem,
|
chainPem,
|
||||||
friendlyName,
|
|
||||||
collectionId
|
collectionId
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
@@ -84,8 +80,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
certificatePem,
|
certificatePem,
|
||||||
privateKeyPem,
|
privateKeyPem,
|
||||||
chainPem,
|
chainPem,
|
||||||
|
|
||||||
friendlyName,
|
|
||||||
pkiCollectionId: collectionId
|
pkiCollectionId: collectionId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,20 +144,6 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="friendlyName"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Friendly Name"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
|
|||||||
+423
-431
File diff suppressed because it is too large
Load Diff
@@ -46,9 +46,8 @@ const schema = z.object({
|
|||||||
certificateTemplateId: z.string().optional(),
|
certificateTemplateId: z.string().optional(),
|
||||||
caId: z.string(),
|
caId: z.string(),
|
||||||
collectionId: z.string().optional(),
|
collectionId: z.string().optional(),
|
||||||
friendlyName: z.string(),
|
|
||||||
commonName: z.string().trim().min(1),
|
commonName: z.string().trim().min(1),
|
||||||
altNames: z.string(),
|
subjectAltNames: z.string(),
|
||||||
ttl: z.string().trim(),
|
ttl: z.string().trim(),
|
||||||
keyUsages: z.object({
|
keyUsages: z.object({
|
||||||
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||||
@@ -139,9 +138,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
if (cert) {
|
if (cert) {
|
||||||
reset({
|
reset({
|
||||||
caId: cert.caId,
|
caId: cert.caId,
|
||||||
friendlyName: cert.friendlyName,
|
|
||||||
commonName: cert.commonName,
|
commonName: cert.commonName,
|
||||||
altNames: cert.altNames,
|
subjectAltNames: cert.subjectAltNames,
|
||||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||||
ttl: "",
|
ttl: "",
|
||||||
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||||
@@ -152,9 +150,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
caId: "",
|
caId: "",
|
||||||
friendlyName: "",
|
|
||||||
commonName: "",
|
commonName: "",
|
||||||
altNames: "",
|
subjectAltNames: "",
|
||||||
ttl: "",
|
ttl: "",
|
||||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
||||||
keyUsages: {
|
keyUsages: {
|
||||||
@@ -182,10 +179,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
caId,
|
caId,
|
||||||
friendlyName,
|
|
||||||
collectionId,
|
collectionId,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
subjectAltNames,
|
||||||
ttl,
|
ttl,
|
||||||
keyUsages,
|
keyUsages,
|
||||||
extendedKeyUsages
|
extendedKeyUsages
|
||||||
@@ -198,9 +194,8 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
|
||||||
projectSlug: currentProject.slug,
|
projectSlug: currentProject.slug,
|
||||||
pkiCollectionId: collectionId,
|
pkiCollectionId: collectionId,
|
||||||
friendlyName,
|
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
subjectAltNames,
|
||||||
ttl,
|
ttl,
|
||||||
keyUsages: Object.entries(keyUsages)
|
keyUsages: Object.entries(keyUsages)
|
||||||
.filter(([, value]) => value)
|
.filter(([, value]) => value)
|
||||||
@@ -359,20 +354,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="friendlyName"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Friendly Name"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="My Certificate" isDisabled={Boolean(cert)} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -391,7 +372,7 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
name="altNames"
|
name="subjectAltNames"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Alternative Names (SANs)"
|
label="Alternative Names (SANs)"
|
||||||
|
|||||||
+7
-5
@@ -7,7 +7,8 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionPkiSubscriberActions,
|
ProjectPermissionPkiSubscriberActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProject
|
useProject,
|
||||||
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
|
||||||
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
|
||||||
@@ -18,9 +19,10 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
|||||||
|
|
||||||
export const PkiSubscriberSection = () => {
|
export const PkiSubscriberSection = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const projectId = currentProject.id;
|
const projectId = currentProject.id;
|
||||||
|
|
||||||
const allowNewSubscriberCreation = false;
|
const allowNewSubscriberCreation = subscription.pkiLegacyTemplates;
|
||||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||||
|
|
||||||
@@ -85,16 +87,16 @@ export const PkiSubscriberSection = () => {
|
|||||||
const subscriberName = subscriberStatusData?.subscriberName || "";
|
const subscriberName = subscriberStatusData?.subscriberName || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="border-mineshaft-600 bg-mineshaft-900 mb-6 rounded-lg border p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Subscribers</p>
|
<p className="text-mineshaft-100 text-xl font-medium">Subscribers</p>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
|
href="https://infisical.com/docs/documentation/platform/pki/subscribers"
|
||||||
>
|
>
|
||||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
<span className="border-mineshaft-500 bg-mineshaft-600 text-mineshaft-200 hover:border-primary/40 hover:bg-primary/10 flex w-max cursor-pointer items-center rounded-md border px-4 py-2 duration-200 hover:text-white">
|
||||||
Documentation{" "}
|
Documentation{" "}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ export const PkiTemplateListPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="container mx-auto mb-6 max-w-7xl rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex justify-between">
|
{
|
||||||
|
subscription?.pkiLegacyTemplates && (
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
<p className="text-xl font-medium text-mineshaft-100">Templates</p>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
@@ -134,6 +136,8 @@ export const PkiTemplateListPage = () => {
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
|
|||||||
+5
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { useProjectPermission } from "@app/context";
|
import { useProjectPermission } from "@app/context";
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +55,10 @@ export const CertificateProfilesTab = () => {
|
|||||||
});
|
});
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setSelectedProfile(null);
|
setSelectedProfile(null);
|
||||||
|
createNotification({
|
||||||
|
text: `Certificate profile "${selectedProfile.slug}" deleted successfully`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete profile:", error);
|
console.error("Failed to delete profile:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-8
@@ -122,7 +122,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
const certificateAuthorities = caData || [];
|
const certificateAuthorities = caData || [];
|
||||||
const certificateTemplates = templateData?.certificateTemplates || [];
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||||
|
|
||||||
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||||
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
||||||
defaultValues: isEdit
|
defaultValues: isEdit
|
||||||
? {
|
? {
|
||||||
@@ -260,7 +260,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
name="certificateAuthorityId"
|
name="certificateAuthorityId"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Certificate Authority"
|
label="Issuing CA"
|
||||||
isRequired
|
isRequired
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
@@ -296,7 +296,6 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
onChange(value);
|
|
||||||
if (watchedEnrollmentType === "est") {
|
if (watchedEnrollmentType === "est") {
|
||||||
setValue("estConfig", {
|
setValue("estConfig", {
|
||||||
disableBootstrapCaValidation: false,
|
disableBootstrapCaValidation: false,
|
||||||
@@ -311,6 +310,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
});
|
});
|
||||||
setValue("estConfig", undefined);
|
setValue("estConfig", undefined);
|
||||||
}
|
}
|
||||||
|
onChange(value);
|
||||||
}}
|
}}
|
||||||
placeholder="Select a certificate template"
|
placeholder="Select a certificate template"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -339,7 +339,23 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={onChange}
|
onValueChange={(value) => {
|
||||||
|
if (value === "est") {
|
||||||
|
setValue("apiConfig", undefined);
|
||||||
|
setValue("estConfig", {
|
||||||
|
disableBootstrapCaValidation: false,
|
||||||
|
passphrase: "",
|
||||||
|
caChain: ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setValue("estConfig", undefined);
|
||||||
|
setValue("apiConfig", {
|
||||||
|
autoRenew: false,
|
||||||
|
autoRenewDays: 30
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onChange(value);
|
||||||
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
position="popper"
|
position="popper"
|
||||||
isDisabled={Boolean(isEdit)}
|
isDisabled={Boolean(isEdit)}
|
||||||
@@ -360,17 +376,17 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
name="estConfig.disableBootstrapCaValidation"
|
name="estConfig.disableBootstrapCaValidation"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="border-mineshaft-600 bg-mineshaft-900 flex items-center gap-3 rounded-md border p-4">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="disableBootstrapCaValidation"
|
id="disableBootstrapCaValidation"
|
||||||
isChecked={value}
|
isChecked={value}
|
||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="text-sm font-medium text-mineshaft-100">
|
<span className="text-mineshaft-100 text-sm font-medium">
|
||||||
Disable Bootstrap CA Validation
|
Disable Bootstrap CA Validation
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs text-bunker-300">
|
<p className="text-bunker-300 text-xs">
|
||||||
Skip CA certificate validation during EST bootstrap phase
|
Skip CA certificate validation during EST bootstrap phase
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,7 +433,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
rows={6}
|
rows={6}
|
||||||
className="w-full font-mono text-xs"
|
className="w-full font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-bunker-400">
|
<p className="text-bunker-400 text-xs">
|
||||||
Paste the complete CA certificate chain in PEM format
|
Paste the complete CA certificate chain in PEM format
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -478,6 +494,9 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
|
|||||||
type="submit"
|
type="submit"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
isLoading={isEdit ? updateProfile.isPending : createProfile.isPending}
|
isLoading={isEdit ? updateProfile.isPending : createProfile.isPending}
|
||||||
|
isDisabled={
|
||||||
|
!formState.isValid || (isEdit ? updateProfile.isPending : createProfile.isPending)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isEdit ? "Save Changes" : "Create"}
|
{isEdit ? "Save Changes" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+22
-18
@@ -4,6 +4,7 @@ import {
|
|||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
@@ -33,14 +34,6 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
|
|
||||||
const profiles = data?.certificateProfiles || [];
|
const profiles = data?.certificateProfiles || [];
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <TableSkeleton columns={6} innerKey="certificate-profiles" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!profiles || profiles.length === 0) {
|
|
||||||
return <EmptyState title="No Certificate Profiles" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -48,21 +41,32 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Enrollment Type</Th>
|
<Th>Enrollment Type</Th>
|
||||||
<Th>Certificate Authority</Th>
|
<Th>Issuing CA</Th>
|
||||||
<Th>Template</Th>
|
<Th>Certificate Template</Th>
|
||||||
<Th>Certificates</Th>
|
<Th>Certificates</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{profiles.map((profile) => (
|
{isLoading && <TableSkeleton columns={6} innerKey="certificate-profiles" />}
|
||||||
<ProfileRow
|
{!isLoading && (!profiles || profiles.length === 0) && (
|
||||||
key={profile.id}
|
<Tr>
|
||||||
profile={profile}
|
<Td colSpan={6}>
|
||||||
onEditProfile={onEditProfile}
|
<EmptyState title="No Certificate Profiles" />
|
||||||
onDeleteProfile={onDeleteProfile}
|
</Td>
|
||||||
/>
|
</Tr>
|
||||||
))}
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
profiles &&
|
||||||
|
profiles.length > 0 &&
|
||||||
|
profiles.map((profile) => (
|
||||||
|
<ProfileRow
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
onDeleteProfile={onDeleteProfile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
+52
-1
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCircleInfo, faCopy, faEdit, faEllipsis, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,11 +15,16 @@ import {
|
|||||||
import { useProjectPermission } from "@app/context";
|
import { useProjectPermission } from "@app/context";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
||||||
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||||
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { CertificateIssuanceModal } from "@app/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: TCertificateProfile;
|
profile: TCertificateProfile;
|
||||||
@@ -32,6 +37,27 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
|
|
||||||
const { data: caData } = useGetCaById(profile.caId);
|
const { data: caData } = useGetCaById(profile.caId);
|
||||||
|
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp([
|
||||||
|
"certificateIssuance"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const handleCopyId = useCallback(() => {
|
||||||
|
setIsIdCopied.on();
|
||||||
|
navigator.clipboard.writeText(profile.id);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Profile ID copied to clipboard",
|
||||||
|
type: "info"
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => setIsIdCopied.off(), 2000);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isIdCopied]);
|
||||||
|
|
||||||
const { data: templateData } = useGetCertificateTemplateV2ById({
|
const { data: templateData } = useGetCertificateTemplateV2ById({
|
||||||
templateId: profile.certificateTemplateId
|
templateId: profile.certificateTemplateId
|
||||||
});
|
});
|
||||||
@@ -41,6 +67,11 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canIssueCertificate = permission.can(
|
||||||
|
ProjectPermissionCertificateProfileActions.IssueCert,
|
||||||
|
ProjectPermissionSub.CertificateProfiles
|
||||||
|
);
|
||||||
|
|
||||||
const canDeleteProfile = permission.can(
|
const canDeleteProfile = permission.can(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
ProjectPermissionSub.CertificateAuthorities
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
@@ -128,6 +159,12 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
|
||||||
|
onClick={() => handleCopyId()}
|
||||||
|
>
|
||||||
|
Copy Profile ID
|
||||||
|
</DropdownMenuItem>
|
||||||
{canEditProfile && (
|
{canEditProfile && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -139,6 +176,19 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
Edit Profile
|
Edit Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{
|
||||||
|
canIssueCertificate && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpToggle("certificateIssuance");
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Issue Certificate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
{canDeleteProfile && (
|
{canDeleteProfile && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -152,6 +202,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} profileId={profile.id}/>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-1
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { useProjectPermission } from "@app/context";
|
import { useProjectPermission } from "@app/context";
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +53,11 @@ export const CertificateTemplatesV2Tab = () => {
|
|||||||
});
|
});
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
} catch (error) {
|
createNotification({
|
||||||
|
text: `Certificate template "${selectedTemplate.slug}" deleted successfully`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
console.error("Failed to delete template:", error);
|
console.error("Failed to delete template:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+325
-216
@@ -1,5 +1,6 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faExclamationTriangle, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
TextArea
|
TextArea,
|
||||||
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import {
|
import {
|
||||||
@@ -27,7 +29,7 @@ import {
|
|||||||
} from "@app/hooks/api/certificateTemplates/mutations";
|
} from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
import { INCLUDE_OPTIONS, SAN_TYPES, SUBJECT_ATTRIBUTE_TYPES } from "./shared/utils";
|
import { INCLUDE_TYPE_OPTIONS, SAN_TYPE_OPTIONS, SUBJECT_ATTRIBUTE_TYPE_OPTIONS } from "./shared/certificate-constants";
|
||||||
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
|
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
|
||||||
|
|
||||||
export type FormData = TemplateFormData;
|
export type FormData = TemplateFormData;
|
||||||
@@ -39,26 +41,18 @@ interface Props {
|
|||||||
mode?: "create" | "edit";
|
mode?: "create" | "edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
const ATTRIBUTE_TYPE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_TYPES)[number], string> = {
|
const ATTRIBUTE_TYPE_LABELS: Record<(typeof SUBJECT_ATTRIBUTE_TYPE_OPTIONS)[number], string> = {
|
||||||
common_name: "Common Name (CN)",
|
common_name: "Common Name (CN)"
|
||||||
organization_name: "Organization (O)",
|
|
||||||
organization_unit: "Organizational Unit (OU)",
|
|
||||||
locality: "Locality (L)",
|
|
||||||
state: "State/Province (ST)",
|
|
||||||
country: "Country (C)",
|
|
||||||
email: "Email Address",
|
|
||||||
street_address: "Street Address",
|
|
||||||
postal_code: "Postal Code"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAN_TYPE_LABELS: Record<(typeof SAN_TYPES)[number], string> = {
|
const SAN_TYPE_LABELS: Record<(typeof SAN_TYPE_OPTIONS)[number], string> = {
|
||||||
dns_name: "DNS Name",
|
dns_name: "DNS Name",
|
||||||
ip_address: "IP Address",
|
ip_address: "IP Address",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
uri: "URI"
|
uri: "URI"
|
||||||
};
|
};
|
||||||
|
|
||||||
const INCLUDE_TYPE_LABELS: Record<(typeof INCLUDE_OPTIONS)[number], string> = {
|
const INCLUDE_TYPE_LABELS: Record<(typeof INCLUDE_TYPE_OPTIONS)[number], string> = {
|
||||||
mandatory: "Mandatory",
|
mandatory: "Mandatory",
|
||||||
optional: "Optional",
|
optional: "Optional",
|
||||||
prohibit: "Prohibited"
|
prohibit: "Prohibited"
|
||||||
@@ -89,7 +83,36 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
|
|
||||||
const isEdit = mode === "edit" && template;
|
const isEdit = mode === "edit" && template;
|
||||||
|
|
||||||
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
const validateAttributeRules = (attributes: FormData["attributes"]) => {
|
||||||
|
if (!attributes) return { isValid: true, warnings: [], invalidIndices: [] };
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const invalidIndices: number[] = [];
|
||||||
|
|
||||||
|
const attributesByType = attributes.reduce((acc, attr, index) => {
|
||||||
|
if (!acc[attr.type]) acc[attr.type] = [];
|
||||||
|
acc[attr.type].push({ ...attr, index });
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Array<(typeof attributes[0] & { index: number })>>);
|
||||||
|
|
||||||
|
Object.entries(attributesByType).forEach(([type, attrs]) => {
|
||||||
|
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length > 1) {
|
||||||
|
mandatoryAttrs.forEach(attr => invalidIndices.push(attr.index));
|
||||||
|
warnings.push(`Multiple mandatory values found for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}. Only one mandatory value is allowed per attribute type.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||||
|
attrs.forEach(attr => invalidIndices.push(attr.index));
|
||||||
|
warnings.push(`When a mandatory value exists for ${ATTRIBUTE_TYPE_LABELS[type as keyof typeof ATTRIBUTE_TYPE_LABELS]}, no other values (optional or forbidden) are allowed for that attribute type.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isValid: warnings.length === 0, warnings, invalidIndices };
|
||||||
|
};
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
|
||||||
resolver: zodResolver(templateSchema),
|
resolver: zodResolver(templateSchema),
|
||||||
defaultValues: isEdit
|
defaultValues: isEdit
|
||||||
? {
|
? {
|
||||||
@@ -138,13 +161,42 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
|
|
||||||
const watchedAttributes = watch("attributes") || [];
|
const watchedAttributes = watch("attributes") || [];
|
||||||
const watchedSans = watch("subjectAlternativeNames") || [];
|
const watchedSans = watch("subjectAlternativeNames") || [];
|
||||||
const watchedKeyUsages = watch("keyUsages");
|
const watchedKeyUsages = watch("keyUsages") || { requiredUsages: [], optionalUsages: [] };
|
||||||
const watchedExtendedKeyUsages = watch("extendedKeyUsages");
|
const watchedExtendedKeyUsages = watch("extendedKeyUsages") || { requiredUsages: [], optionalUsages: [] };
|
||||||
|
|
||||||
|
const attributeValidation = useMemo(() =>
|
||||||
|
validateAttributeRules(watchedAttributes),
|
||||||
|
[watchedAttributes]
|
||||||
|
);
|
||||||
|
|
||||||
const onFormSubmit = async (data: FormData) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentProject?.id && !isEdit) return;
|
if (!currentProject?.id && !isEdit) return;
|
||||||
|
|
||||||
|
if (!attributeValidation.isValid) {
|
||||||
|
createNotification({
|
||||||
|
text: "Please fix validation errors before submitting",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasEmptyAttributeValues = data.attributes?.some(attr =>
|
||||||
|
!attr.value || attr.value.length === 0 || attr.value.some(v => !v.trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasEmptySanValues = data.subjectAlternativeNames?.some(san =>
|
||||||
|
!san.value || san.value.length === 0 || san.value.some(v => !v.trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasEmptyAttributeValues || hasEmptySanValues) {
|
||||||
|
createNotification({
|
||||||
|
text: "All values must be non-empty. Use wildcards (*) if needed.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
const updateData = {
|
const updateData = {
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
@@ -221,9 +273,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
|
|
||||||
const addAttribute = () => {
|
const addAttribute = () => {
|
||||||
const newAttribute = {
|
const newAttribute = {
|
||||||
type: SUBJECT_ATTRIBUTE_TYPES[0],
|
type: SUBJECT_ATTRIBUTE_TYPE_OPTIONS[0],
|
||||||
include: INCLUDE_OPTIONS[1],
|
include: INCLUDE_TYPE_OPTIONS[1],
|
||||||
value: []
|
value: ["*"]
|
||||||
};
|
};
|
||||||
setValue("attributes", [...watchedAttributes, newAttribute]);
|
setValue("attributes", [...watchedAttributes, newAttribute]);
|
||||||
};
|
};
|
||||||
@@ -235,9 +287,9 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
|
|
||||||
const addSan = () => {
|
const addSan = () => {
|
||||||
const newSan = {
|
const newSan = {
|
||||||
type: SAN_TYPES[0],
|
type: SAN_TYPE_OPTIONS[0],
|
||||||
include: INCLUDE_OPTIONS[1],
|
include: INCLUDE_TYPE_OPTIONS[1],
|
||||||
value: []
|
value: ["*"]
|
||||||
};
|
};
|
||||||
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
||||||
};
|
};
|
||||||
@@ -247,42 +299,18 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
setValue("subjectAlternativeNames", newSans);
|
setValue("subjectAlternativeNames", newSans);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleKeyUsage = (usage: string, type: "required" | "optional") => {
|
const handleKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
|
||||||
const current = watchedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
|
||||||
const otherType = type === "required" ? "optional" : "required";
|
|
||||||
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
|
||||||
const otherList = Array.isArray(current[`${otherType}Usages`])
|
|
||||||
? current[`${otherType}Usages`]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
|
||||||
const newCurrentList = currentList?.includes(usage)
|
|
||||||
? currentList.filter((u) => u !== usage)
|
|
||||||
: [...(currentList || []), usage];
|
|
||||||
|
|
||||||
setValue("keyUsages", {
|
setValue("keyUsages", {
|
||||||
[`${type}Usages`]: newCurrentList,
|
requiredUsages: usages.requiredUsages as any,
|
||||||
[`${otherType}Usages`]: newOtherList
|
optionalUsages: usages.optionalUsages as any
|
||||||
} as any);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleExtendedKeyUsage = (usage: string, type: "required" | "optional") => {
|
const handleExtendedKeyUsagesChange = (usages: { requiredUsages: string[]; optionalUsages: string[] }) => {
|
||||||
const current = watchedExtendedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
|
||||||
const otherType = type === "required" ? "optional" : "required";
|
|
||||||
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
|
||||||
const otherList = Array.isArray(current[`${otherType}Usages`])
|
|
||||||
? current[`${otherType}Usages`]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
|
||||||
const newCurrentList = currentList?.includes(usage)
|
|
||||||
? currentList.filter((u) => u !== usage)
|
|
||||||
: [...(currentList || []), usage];
|
|
||||||
|
|
||||||
setValue("extendedKeyUsages", {
|
setValue("extendedKeyUsages", {
|
||||||
[`${type}Usages`]: newCurrentList,
|
requiredUsages: usages.requiredUsages as any,
|
||||||
[`${otherType}Usages`]: newOtherList
|
optionalUsages: usages.optionalUsages as any
|
||||||
} as any);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -297,7 +325,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-4xl"
|
className="max-w-4xl"
|
||||||
title={isEdit ? "Edit Certificate Template V2" : "Create Certificate Template V2"}
|
title={isEdit ? "Edit Certificate Template" : "Create Certificate Template"}
|
||||||
subTitle={
|
subTitle={
|
||||||
isEdit
|
isEdit
|
||||||
? `Update configuration for ${template?.slug}`
|
? `Update configuration for ${template?.slug}`
|
||||||
@@ -306,42 +334,36 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6">
|
||||||
<Accordion type="multiple" defaultValue={["basic"]} className="w-full">
|
<Accordion type="multiple" defaultValue={["basic"]} className="w-full">
|
||||||
<AccordionItem value="basic">
|
<div className="space-y-4">
|
||||||
<AccordionTrigger>Basic Information</AccordionTrigger>
|
<Controller
|
||||||
<AccordionContent>
|
control={control}
|
||||||
<div className="space-y-4">
|
name="slug"
|
||||||
<Controller
|
render={({ field, fieldState: { error } }) => (
|
||||||
control={control}
|
<FormControl
|
||||||
name="slug"
|
label="Template Name"
|
||||||
render={({ field, fieldState: { error } }) => (
|
isRequired
|
||||||
<FormControl
|
isError={Boolean(error)}
|
||||||
label="Template Name"
|
errorText={error?.message}
|
||||||
isRequired
|
>
|
||||||
isError={Boolean(error)}
|
<Input {...field} placeholder="Enter template name" className="w-full" />
|
||||||
errorText={error?.message}
|
</FormControl>
|
||||||
>
|
)}
|
||||||
<Input {...field} placeholder="Enter template name" className="w-full" />
|
/>
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="description"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Description"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<AccordionItem value="attributes">
|
<AccordionItem value="attributes">
|
||||||
<AccordionTrigger>Subject Attributes</AccordionTrigger>
|
<AccordionTrigger>Subject Attributes</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
@@ -351,92 +373,173 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={addAttribute}
|
onClick={addAttribute}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
>
|
>
|
||||||
Add Attribute
|
Add Attribute
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation warnings */}
|
||||||
|
{!attributeValidation.isValid && attributeValidation.warnings.length > 0 && (
|
||||||
|
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-md p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<FontAwesomeIcon icon={faExclamationTriangle} className="text-yellow-500 mt-0.5" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-yellow-500 font-medium text-sm">Validation Warnings</h4>
|
||||||
|
<ul className="text-yellow-400 text-sm mt-1 space-y-1">
|
||||||
|
{attributeValidation.warnings.map((warning, index) => (
|
||||||
|
<li key={index}>• {warning}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{watchedAttributes.length === 0 ? (
|
{watchedAttributes.length === 0 ? (
|
||||||
<div className="py-8 text-center text-bunker-300">
|
<div className="text-bunker-300 py-8 text-center">
|
||||||
No subject attributes configured yet. Click "Add Attribute" to get
|
No subject attributes configured yet. Click "Add Attribute" to get
|
||||||
started.
|
started.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
watchedAttributes.map((attr, index) => (
|
watchedAttributes.map((attr, index) => {
|
||||||
<div
|
const isInvalid = attributeValidation.invalidIndices.includes(index);
|
||||||
key={`attr-${attr.type}`}
|
const errorClass = isInvalid ? "border-red-500 focus:border-red-500" : "";
|
||||||
className="flex flex-col space-y-2 rounded-md border border-mineshaft-600 p-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium text-mineshaft-200">
|
|
||||||
{ATTRIBUTE_TYPE_LABELS[attr.type] || attr.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
return (
|
||||||
<Select
|
<div key={`attr-${attr.type}-${attr.include}-${index}`} className="flex items-start gap-2">
|
||||||
value={attr.type}
|
{isInvalid ? (
|
||||||
onValueChange={(value) => {
|
<Tooltip content="This attribute has validation errors. Check the warnings above for details.">
|
||||||
const newAttributes = [...watchedAttributes];
|
<div className="flex items-start gap-2 flex-1">
|
||||||
newAttributes[index] = { ...attr, type: value as any };
|
<Select
|
||||||
setValue("attributes", newAttributes);
|
value={attr.type}
|
||||||
}}
|
onValueChange={(value) => {
|
||||||
position="popper"
|
const newAttributes = [...watchedAttributes];
|
||||||
>
|
newAttributes[index] = { ...attr, type: value as any };
|
||||||
{SUBJECT_ATTRIBUTE_TYPES.map((type) => (
|
setValue("attributes", newAttributes);
|
||||||
<SelectItem key={type} value={type}>
|
}}
|
||||||
{ATTRIBUTE_TYPE_LABELS[type]}
|
className={`w-48 ${errorClass}`}
|
||||||
</SelectItem>
|
>
|
||||||
))}
|
{SUBJECT_ATTRIBUTE_TYPE_OPTIONS.map((type) => (
|
||||||
</Select>
|
<SelectItem key={type} value={type}>
|
||||||
|
{ATTRIBUTE_TYPE_LABELS[type]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={attr.include}
|
value={attr.include}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const newAttributes = [...watchedAttributes];
|
const newAttributes = [...watchedAttributes];
|
||||||
newAttributes[index] = { ...attr, include: value as any };
|
newAttributes[index] = { ...attr, include: value as any };
|
||||||
setValue("attributes", newAttributes);
|
setValue("attributes", newAttributes);
|
||||||
}}
|
}}
|
||||||
position="popper"
|
className={`w-32 ${errorClass}`}
|
||||||
>
|
>
|
||||||
{INCLUDE_OPTIONS.map((type) => (
|
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||||
<SelectItem key={type} value={type}>
|
<SelectItem key={type} value={type}>
|
||||||
{INCLUDE_TYPE_LABELS[type]}
|
{INCLUDE_TYPE_LABELS[type]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Pattern/Value (optional)"
|
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||||
value={attr.value?.[0] || ""}
|
value={attr.value?.[0] || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newAttributes = [...watchedAttributes];
|
const newAttributes = [...watchedAttributes];
|
||||||
newAttributes[index] = {
|
newAttributes[index] = {
|
||||||
...attr,
|
...attr,
|
||||||
value: e.target.value ? [e.target.value] : []
|
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||||
};
|
};
|
||||||
setValue("attributes", newAttributes);
|
setValue("attributes", newAttributes);
|
||||||
}}
|
}}
|
||||||
/>
|
className={`flex-1 ${errorClass} ${
|
||||||
|
attr.value && attr.value.length > 0 && attr.value[0] === ""
|
||||||
|
? "border-red-500 focus:border-red-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
value={attr.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, type: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-48"
|
||||||
|
>
|
||||||
|
{SUBJECT_ATTRIBUTE_TYPE_OPTIONS.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{ATTRIBUTE_TYPE_LABELS[type]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={attr.include}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, include: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-32"
|
||||||
|
>
|
||||||
|
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{INCLUDE_TYPE_LABELS[type]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||||
|
value={attr.value?.[0] || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = {
|
||||||
|
...attr,
|
||||||
|
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||||
|
};
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className={`flex-1 ${
|
||||||
|
attr.value && attr.value.length > 0 && attr.value[0] === ""
|
||||||
|
? "border-red-500 focus:border-red-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{watchedAttributes.length > 1 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="delete attribute"
|
ariaLabel="Remove Attribute"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
onClick={() => removeAttribute(index)}
|
onClick={() => removeAttribute(index)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faTrash} className="text-red-500" />
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="san">
|
<AccordionItem value="san" className="mt-4">
|
||||||
<AccordionTrigger>Subject Alternative Names</AccordionTrigger>
|
<AccordionTrigger>Subject Alternative Names</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -445,6 +548,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={addSan}
|
onClick={addSan}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
>
|
>
|
||||||
Add SAN
|
Add SAN
|
||||||
@@ -453,72 +557,72 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{watchedSans.length === 0 ? (
|
{watchedSans.length === 0 ? (
|
||||||
<div className="py-8 text-center text-bunker-300">
|
<div className="text-bunker-300 py-8 text-center">
|
||||||
No subject alternative names configured yet. Click "Add SAN" to
|
No subject alternative names configured yet. Click "Add SAN" to
|
||||||
get started.
|
get started.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
watchedSans.map((san, index) => (
|
watchedSans.map((san, index) => (
|
||||||
<div
|
<div key={`san-${san.type}-${san.include}-${index}`} className="flex items-start gap-2">
|
||||||
key={`san-${san.type}`}
|
<Select
|
||||||
className="flex flex-col space-y-4 rounded-md border border-mineshaft-600 p-4"
|
value={san.type}
|
||||||
>
|
onValueChange={(value) => {
|
||||||
<div className="flex gap-3">
|
const newSans = [...watchedSans];
|
||||||
<Select
|
newSans[index] = { ...san, type: value as any };
|
||||||
value={san.type}
|
setValue("subjectAlternativeNames", newSans);
|
||||||
onValueChange={(value) => {
|
}}
|
||||||
const newSans = [...watchedSans];
|
className="w-24"
|
||||||
newSans[index] = { ...san, type: value as any };
|
>
|
||||||
setValue("subjectAlternativeNames", newSans);
|
{SAN_TYPE_OPTIONS.map((type) => (
|
||||||
}}
|
<SelectItem key={type} value={type}>
|
||||||
position="popper"
|
{SAN_TYPE_LABELS[type]}
|
||||||
>
|
</SelectItem>
|
||||||
{SAN_TYPES.map((type) => (
|
))}
|
||||||
<SelectItem key={type} value={type}>
|
</Select>
|
||||||
{SAN_TYPE_LABELS[type]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={san.include}
|
value={san.include}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const newSans = [...watchedSans];
|
const newSans = [...watchedSans];
|
||||||
newSans[index] = { ...san, include: value as any };
|
newSans[index] = { ...san, include: value as any };
|
||||||
setValue("subjectAlternativeNames", newSans);
|
setValue("subjectAlternativeNames", newSans);
|
||||||
}}
|
}}
|
||||||
position="popper"
|
className="w-32"
|
||||||
>
|
>
|
||||||
{INCLUDE_OPTIONS.map((type) => (
|
{INCLUDE_TYPE_OPTIONS.map((type) => (
|
||||||
<SelectItem key={type} value={type}>
|
<SelectItem key={type} value={type}>
|
||||||
{INCLUDE_TYPE_LABELS[type]}
|
{INCLUDE_TYPE_LABELS[type]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Pattern/Value (optional)"
|
placeholder="Pattern/Value (required - use * for wildcards)"
|
||||||
value={san.value?.[0] || ""}
|
value={san.value?.[0] || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newSans = [...watchedSans];
|
const newSans = [...watchedSans];
|
||||||
newSans[index] = {
|
newSans[index] = {
|
||||||
...san,
|
...san,
|
||||||
value: e.target.value ? [e.target.value] : []
|
value: e.target.value.trim() ? [e.target.value.trim()] : []
|
||||||
};
|
};
|
||||||
setValue("subjectAlternativeNames", newSans);
|
setValue("subjectAlternativeNames", newSans);
|
||||||
}}
|
}}
|
||||||
/>
|
className={`flex-1 ${
|
||||||
<div className="flex items-center justify-between">
|
san.value && san.value.length > 0 && san.value[0] === ""
|
||||||
<IconButton
|
? "border-red-500 focus:border-red-500"
|
||||||
onClick={() => removeSan(index)}
|
: ""
|
||||||
size="sm"
|
}`}
|
||||||
variant="plain"
|
required
|
||||||
ariaLabel="Remove SAN"
|
/>
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} className="text-red-500" />
|
<IconButton
|
||||||
</IconButton>
|
ariaLabel="Remove SAN"
|
||||||
</div>
|
variant="plain"
|
||||||
</div>
|
size="sm"
|
||||||
|
onClick={() => removeSan(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -527,19 +631,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="usages">
|
<AccordionItem value="usages" className="mt-4">
|
||||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<KeyUsagesSection
|
<KeyUsagesSection
|
||||||
watchedKeyUsages={watchedKeyUsages}
|
watchedKeyUsages={watchedKeyUsages}
|
||||||
watchedExtendedKeyUsages={watchedExtendedKeyUsages}
|
watchedExtendedKeyUsages={watchedExtendedKeyUsages}
|
||||||
toggleKeyUsage={toggleKeyUsage}
|
onKeyUsagesChange={handleKeyUsagesChange}
|
||||||
toggleExtendedKeyUsage={toggleExtendedKeyUsage}
|
onExtendedKeyUsagesChange={handleExtendedKeyUsagesChange}
|
||||||
/>
|
/>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
<AccordionItem value="constraints">
|
<AccordionItem value="constraints" className="mt-4">
|
||||||
<AccordionTrigger>Constraints</AccordionTrigger>
|
<AccordionTrigger>Constraints</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -637,7 +741,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`sig-alg-${alg}`}
|
htmlFor={`sig-alg-${alg}`}
|
||||||
className="cursor-pointer text-sm font-medium text-mineshaft-200"
|
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||||
>
|
>
|
||||||
{alg}
|
{alg}
|
||||||
</label>
|
</label>
|
||||||
@@ -717,7 +821,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor={`key-alg-${alg}`}
|
htmlFor={`key-alg-${alg}`}
|
||||||
className="cursor-pointer text-sm font-medium text-mineshaft-200"
|
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||||
>
|
>
|
||||||
{alg}
|
{alg}
|
||||||
</label>
|
</label>
|
||||||
@@ -765,6 +869,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
|
|||||||
type="submit"
|
type="submit"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
isLoading={isEdit ? updateTemplate.isPending : createTemplate.isPending}
|
isLoading={isEdit ? updateTemplate.isPending : createTemplate.isPending}
|
||||||
|
isDisabled={
|
||||||
|
!formState.isValid ||
|
||||||
|
!attributeValidation.isValid ||
|
||||||
|
(isEdit ? updateTemplate.isPending : createTemplate.isPending)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isEdit ? "Save Changes" : "Create"}
|
{isEdit ? "Save Changes" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+9
-9
@@ -56,14 +56,6 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
|||||||
return new Date(dateString).toLocaleDateString();
|
return new Date(dateString).toLocaleDateString();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <TableSkeleton columns={4} innerKey="certificate-templates" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!templates || templates.length === 0) {
|
|
||||||
return <EmptyState title="No Certificate Templates" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -75,7 +67,15 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{templates.map((template) => (
|
{isLoading && <TableSkeleton columns={3} innerKey="certificate-templates" />}
|
||||||
|
{!isLoading && (!templates || templates.length === 0) && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={3}>
|
||||||
|
<EmptyState title="No Certificate Templates" />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isLoading && templates && templates.length > 0 && templates.map((template) => (
|
||||||
<Tr
|
<Tr
|
||||||
key={template.id}
|
key={template.id}
|
||||||
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
|
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
|||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
export enum CertSubjectAlternativeNameType {
|
||||||
|
DNS_NAME = "dns_name",
|
||||||
|
IP_ADDRESS = "ip_address",
|
||||||
|
EMAIL = "email",
|
||||||
|
URI = "uri"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertKeyUsageType {
|
||||||
|
DIGITAL_SIGNATURE = "digital_signature",
|
||||||
|
KEY_ENCIPHERMENT = "key_encipherment",
|
||||||
|
NON_REPUDIATION = "non_repudiation",
|
||||||
|
DATA_ENCIPHERMENT = "data_encipherment",
|
||||||
|
KEY_AGREEMENT = "key_agreement",
|
||||||
|
KEY_CERT_SIGN = "key_cert_sign",
|
||||||
|
CRL_SIGN = "crl_sign",
|
||||||
|
ENCIPHER_ONLY = "encipher_only",
|
||||||
|
DECIPHER_ONLY = "decipher_only"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertExtendedKeyUsageType {
|
||||||
|
CLIENT_AUTH = "client_auth",
|
||||||
|
SERVER_AUTH = "server_auth",
|
||||||
|
CODE_SIGNING = "code_signing",
|
||||||
|
EMAIL_PROTECTION = "email_protection",
|
||||||
|
OCSP_SIGNING = "ocsp_signing",
|
||||||
|
TIME_STAMPING = "time_stamping"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertIncludeType {
|
||||||
|
MANDATORY = "mandatory",
|
||||||
|
OPTIONAL = "optional",
|
||||||
|
PROHIBIT = "prohibit"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertDurationUnit {
|
||||||
|
DAYS = "days",
|
||||||
|
MONTHS = "months",
|
||||||
|
YEARS = "years"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertSubjectAttributeType {
|
||||||
|
COMMON_NAME = "common_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatSANType = (type: CertSubjectAlternativeNameType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return "DNS Name";
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return "IP Address";
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return "Email";
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return "URI";
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatKeyUsage = (usage: CertKeyUsageType): string => {
|
||||||
|
switch (usage) {
|
||||||
|
case CertKeyUsageType.DIGITAL_SIGNATURE:
|
||||||
|
return "Digital Signature";
|
||||||
|
case CertKeyUsageType.KEY_ENCIPHERMENT:
|
||||||
|
return "Key Encipherment";
|
||||||
|
case CertKeyUsageType.NON_REPUDIATION:
|
||||||
|
return "Non Repudiation";
|
||||||
|
case CertKeyUsageType.DATA_ENCIPHERMENT:
|
||||||
|
return "Data Encipherment";
|
||||||
|
case CertKeyUsageType.KEY_AGREEMENT:
|
||||||
|
return "Key Agreement";
|
||||||
|
case CertKeyUsageType.KEY_CERT_SIGN:
|
||||||
|
return "Key Cert Sign";
|
||||||
|
case CertKeyUsageType.CRL_SIGN:
|
||||||
|
return "CRL Sign";
|
||||||
|
case CertKeyUsageType.ENCIPHER_ONLY:
|
||||||
|
return "Encipher Only";
|
||||||
|
case CertKeyUsageType.DECIPHER_ONLY:
|
||||||
|
return "Decipher Only";
|
||||||
|
default:
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatExtendedKeyUsage = (usage: CertExtendedKeyUsageType): string => {
|
||||||
|
switch (usage) {
|
||||||
|
case CertExtendedKeyUsageType.CLIENT_AUTH:
|
||||||
|
return "Client Auth";
|
||||||
|
case CertExtendedKeyUsageType.SERVER_AUTH:
|
||||||
|
return "Server Auth";
|
||||||
|
case CertExtendedKeyUsageType.CODE_SIGNING:
|
||||||
|
return "Code Signing";
|
||||||
|
case CertExtendedKeyUsageType.EMAIL_PROTECTION:
|
||||||
|
return "Email Protection";
|
||||||
|
case CertExtendedKeyUsageType.OCSP_SIGNING:
|
||||||
|
return "OCSP Signing";
|
||||||
|
case CertExtendedKeyUsageType.TIME_STAMPING:
|
||||||
|
return "Time Stamping";
|
||||||
|
default:
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatSubjectAttributeType = (type: CertSubjectAttributeType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case CertSubjectAttributeType.COMMON_NAME:
|
||||||
|
return "Common Name";
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatIncludeType = (include: CertIncludeType): string => {
|
||||||
|
switch (include) {
|
||||||
|
case CertIncludeType.MANDATORY:
|
||||||
|
return "Mandatory";
|
||||||
|
case CertIncludeType.OPTIONAL:
|
||||||
|
return "Optional";
|
||||||
|
case CertIncludeType.PROHIBIT:
|
||||||
|
return "Prohibit";
|
||||||
|
default:
|
||||||
|
return include;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapLegacySANTypeToStandard = (type: string): CertSubjectAlternativeNameType => {
|
||||||
|
switch (type) {
|
||||||
|
case "dns":
|
||||||
|
case "dns_name":
|
||||||
|
return CertSubjectAlternativeNameType.DNS_NAME;
|
||||||
|
case "ip":
|
||||||
|
case "ip_address":
|
||||||
|
return CertSubjectAlternativeNameType.IP_ADDRESS;
|
||||||
|
case "email":
|
||||||
|
return CertSubjectAlternativeNameType.EMAIL;
|
||||||
|
case "uri":
|
||||||
|
case "url":
|
||||||
|
return CertSubjectAlternativeNameType.URI;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown SAN type: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapSANTypeToLegacy = (type: CertSubjectAlternativeNameType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case CertSubjectAlternativeNameType.DNS_NAME:
|
||||||
|
return "dns";
|
||||||
|
case CertSubjectAlternativeNameType.IP_ADDRESS:
|
||||||
|
return "ip";
|
||||||
|
case CertSubjectAlternativeNameType.EMAIL:
|
||||||
|
return "email";
|
||||||
|
case CertSubjectAlternativeNameType.URI:
|
||||||
|
return "uri";
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
|
||||||
|
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
|
||||||
|
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
|
||||||
|
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
|
||||||
|
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
|
||||||
|
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
|
||||||
+124
-113
@@ -1,141 +1,152 @@
|
|||||||
import { Button } from "@app/components/v2";
|
import { Checkbox } from "@app/components/v2";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EXTENDED_KEY_USAGES,
|
CertExtendedKeyUsageType,
|
||||||
formatUsageName,
|
CertKeyUsageType,
|
||||||
getUsageState,
|
formatExtendedKeyUsage,
|
||||||
KEY_USAGES,
|
formatKeyUsage,
|
||||||
toggleUsageState
|
EXTENDED_KEY_USAGE_OPTIONS,
|
||||||
} from "./utils";
|
KEY_USAGE_OPTIONS
|
||||||
|
} from "./certificate-constants";
|
||||||
|
|
||||||
type UsageToggleProps = {
|
type UsageState = "mandatory" | "optional" | undefined;
|
||||||
value: "required" | "optional" | undefined;
|
|
||||||
onChange: (value: "required" | "optional" | undefined) => void;
|
type ThreeStateCheckboxProps = {
|
||||||
|
value: UsageState;
|
||||||
|
onChange: (newValue: UsageState) => void;
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UsageToggle = ({ value, onChange }: UsageToggleProps) => {
|
const ThreeStateCheckbox = ({ value, onChange, label, id }: ThreeStateCheckboxProps) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
if (value === undefined) {
|
||||||
|
onChange("optional");
|
||||||
|
} else if (value === "optional") {
|
||||||
|
onChange("mandatory");
|
||||||
|
} else {
|
||||||
|
onChange(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCheckboxState = () => {
|
||||||
|
if (value) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIndeterminateState = () => {
|
||||||
|
return value === "optional";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStateLabel = () => {
|
||||||
|
if (value === "mandatory") return " (Mandatory)";
|
||||||
|
if (value === "optional") return " (Optional)";
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-mineshaft-600 bg-mineshaft-800 flex gap-x-0.5 rounded-md border p-1">
|
<div className="flex items-center space-x-3">
|
||||||
<Button
|
<Checkbox
|
||||||
variant="outline_bg"
|
id={id}
|
||||||
onClick={() => {
|
isChecked={getCheckboxState()}
|
||||||
onChange(value === "required" ? undefined : "required");
|
isIndeterminate={getIndeterminateState()}
|
||||||
}}
|
onCheckedChange={handleClick}
|
||||||
size="xs"
|
/>
|
||||||
className={`${
|
<label
|
||||||
value === "required" ? "bg-mineshaft-500" : "bg-transparent"
|
htmlFor={id}
|
||||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
className="text-mineshaft-200 cursor-pointer text-sm font-medium"
|
||||||
>
|
>
|
||||||
Required
|
{label}
|
||||||
</Button>
|
{value && (
|
||||||
<Button
|
<span className="text-mineshaft-400 text-xs ml-1">
|
||||||
variant="outline_bg"
|
{getStateLabel()}
|
||||||
onClick={() => {
|
</span>
|
||||||
onChange(value === "optional" ? undefined : "optional");
|
)}
|
||||||
}}
|
</label>
|
||||||
size="xs"
|
|
||||||
className={`${
|
|
||||||
value === "optional" ? "bg-mineshaft-500" : "bg-transparent"
|
|
||||||
} hover:bg-mineshaft-600 min-w-[2.4rem] rounded border-none`}
|
|
||||||
>
|
|
||||||
Optional
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type KeyUsagesSectionProps = {
|
type KeyUsagesSectionProps = {
|
||||||
watchedKeyUsages?: {
|
watchedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
|
||||||
requiredUsages?: string[];
|
watchedExtendedKeyUsages?: { requiredUsages?: string[]; optionalUsages?: string[] };
|
||||||
optionalUsages?: string[];
|
onKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
|
||||||
};
|
onExtendedKeyUsagesChange: (usages: { requiredUsages: string[]; optionalUsages: string[] }) => void;
|
||||||
watchedExtendedKeyUsages?: {
|
|
||||||
requiredUsages?: string[];
|
|
||||||
optionalUsages?: string[];
|
|
||||||
};
|
|
||||||
toggleKeyUsage: (usage: string, type: "required" | "optional") => void;
|
|
||||||
toggleExtendedKeyUsage: (usage: string, type: "required" | "optional") => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeyUsagesSection = ({
|
export const KeyUsagesSection = ({
|
||||||
watchedKeyUsages,
|
watchedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||||
watchedExtendedKeyUsages,
|
watchedExtendedKeyUsages = { requiredUsages: [], optionalUsages: [] },
|
||||||
toggleKeyUsage,
|
onKeyUsagesChange,
|
||||||
toggleExtendedKeyUsage
|
onExtendedKeyUsagesChange
|
||||||
}: KeyUsagesSectionProps) => {
|
}: KeyUsagesSectionProps) => {
|
||||||
|
const getUsageState = (usage: string, data: { requiredUsages?: string[]; optionalUsages?: string[] }): UsageState => {
|
||||||
|
if (data.requiredUsages?.includes(usage)) return "mandatory";
|
||||||
|
if (data.optionalUsages?.includes(usage)) return "optional";
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUsageChange = (usage: CertKeyUsageType, newState: UsageState) => {
|
||||||
|
const currentRequired = watchedKeyUsages.requiredUsages || [];
|
||||||
|
const currentOptional = watchedKeyUsages.optionalUsages || [];
|
||||||
|
|
||||||
|
let newRequired = currentRequired.filter(u => u !== usage);
|
||||||
|
let newOptional = currentOptional.filter(u => u !== usage);
|
||||||
|
|
||||||
|
if (newState === "mandatory") {
|
||||||
|
newRequired = [...newRequired, usage];
|
||||||
|
} else if (newState === "optional") {
|
||||||
|
newOptional = [...newOptional, usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExtendedKeyUsageChange = (usage: CertExtendedKeyUsageType, newState: UsageState) => {
|
||||||
|
const currentRequired = watchedExtendedKeyUsages.requiredUsages || [];
|
||||||
|
const currentOptional = watchedExtendedKeyUsages.optionalUsages || [];
|
||||||
|
|
||||||
|
let newRequired = currentRequired.filter(u => u !== usage);
|
||||||
|
let newOptional = currentOptional.filter(u => u !== usage);
|
||||||
|
|
||||||
|
if (newState === "mandatory") {
|
||||||
|
newRequired = [...newRequired, usage];
|
||||||
|
} else if (newState === "optional") {
|
||||||
|
newOptional = [...newOptional, usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
onExtendedKeyUsagesChange({ requiredUsages: newRequired, optionalUsages: newOptional });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
|
<h3 className="text-mineshaft-200 text-sm font-medium">Key Usages</h3>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||||
{KEY_USAGES.map((usage) => {
|
{KEY_USAGE_OPTIONS.map((usage) => (
|
||||||
const requiredUsages = Array.isArray(watchedKeyUsages?.requiredUsages)
|
<ThreeStateCheckbox
|
||||||
? watchedKeyUsages.requiredUsages
|
key={usage}
|
||||||
: [];
|
id={`key-usage-${usage}`}
|
||||||
const optionalUsages = Array.isArray(watchedKeyUsages?.optionalUsages)
|
label={formatKeyUsage(usage)}
|
||||||
? watchedKeyUsages.optionalUsages
|
value={getUsageState(usage, watchedKeyUsages)}
|
||||||
: [];
|
onChange={(newState) => handleKeyUsageChange(usage, newState)}
|
||||||
|
/>
|
||||||
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
))}
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={usage} className="flex items-center justify-between p-2">
|
|
||||||
<span className="text-mineshaft-300 text-sm capitalize">
|
|
||||||
{formatUsageName(usage)}
|
|
||||||
</span>
|
|
||||||
<UsageToggle
|
|
||||||
value={currentState}
|
|
||||||
onChange={(newValue) => {
|
|
||||||
toggleUsageState(
|
|
||||||
usage,
|
|
||||||
newValue,
|
|
||||||
requiredUsages,
|
|
||||||
optionalUsages,
|
|
||||||
(u) => toggleKeyUsage(u, "required"),
|
|
||||||
(u) => toggleKeyUsage(u, "optional")
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
|
<h3 className="text-mineshaft-200 text-sm font-medium">Extended Key Usages</h3>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||||
{EXTENDED_KEY_USAGES.map((usage) => {
|
{EXTENDED_KEY_USAGE_OPTIONS.map((usage) => (
|
||||||
const requiredUsages = Array.isArray(watchedExtendedKeyUsages?.requiredUsages)
|
<ThreeStateCheckbox
|
||||||
? watchedExtendedKeyUsages.requiredUsages
|
key={usage}
|
||||||
: [];
|
id={`ext-key-usage-${usage}`}
|
||||||
const optionalUsages = Array.isArray(watchedExtendedKeyUsages?.optionalUsages)
|
label={formatExtendedKeyUsage(usage)}
|
||||||
? watchedExtendedKeyUsages.optionalUsages
|
value={getUsageState(usage, watchedExtendedKeyUsages)}
|
||||||
: [];
|
onChange={(newState) => handleExtendedKeyUsageChange(usage, newState)}
|
||||||
|
/>
|
||||||
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
))}
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={usage} className="flex items-center justify-between p-2">
|
|
||||||
<span className="text-mineshaft-300 text-sm capitalize">
|
|
||||||
{formatUsageName(usage)}
|
|
||||||
</span>
|
|
||||||
<UsageToggle
|
|
||||||
value={currentState}
|
|
||||||
onChange={(newValue) => {
|
|
||||||
toggleUsageState(
|
|
||||||
usage,
|
|
||||||
newValue,
|
|
||||||
requiredUsages,
|
|
||||||
optionalUsages,
|
|
||||||
(u) => toggleExtendedKeyUsage(u, "required"),
|
|
||||||
(u) => toggleExtendedKeyUsage(u, "optional")
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+45
-14
@@ -1,33 +1,64 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { INCLUDE_OPTIONS, SAN_TYPES, SUBJECT_ATTRIBUTE_TYPES } from "./utils";
|
import {
|
||||||
|
CertDurationUnit,
|
||||||
|
CertExtendedKeyUsageType,
|
||||||
|
CertIncludeType,
|
||||||
|
CertKeyUsageType,
|
||||||
|
CertSubjectAlternativeNameType,
|
||||||
|
CertSubjectAttributeType
|
||||||
|
} from "./certificate-constants";
|
||||||
|
|
||||||
export const attributeSchema = z.object({
|
export const attributeSchema = z.object({
|
||||||
type: z.enum(SUBJECT_ATTRIBUTE_TYPES),
|
type: z.nativeEnum(CertSubjectAttributeType),
|
||||||
include: z.enum(INCLUDE_OPTIONS),
|
include: z.nativeEnum(CertIncludeType),
|
||||||
value: z.array(z.string()).optional()
|
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sanSchema = z.object({
|
export const sanSchema = z.object({
|
||||||
type: z.enum(SAN_TYPES),
|
type: z.nativeEnum(CertSubjectAlternativeNameType),
|
||||||
include: z.enum(INCLUDE_OPTIONS),
|
include: z.nativeEnum(CertIncludeType),
|
||||||
value: z.array(z.string()).optional()
|
value: z.array(z.string().min(1, "Value cannot be empty")).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const templateSchema = z.object({
|
export const templateSchema = z.object({
|
||||||
slug: z.string().trim().min(1, "Template name is required"),
|
slug: z.string().trim().min(1, "Template name is required"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
attributes: z.array(attributeSchema).optional(),
|
attributes: z.array(attributeSchema).optional().refine((attributes) => {
|
||||||
|
if (!attributes) return true;
|
||||||
|
|
||||||
|
const attributesByType = attributes.reduce((acc, attr) => {
|
||||||
|
if (!acc[attr.type]) acc[attr.type] = [];
|
||||||
|
acc[attr.type].push(attr);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, typeof attributes>);
|
||||||
|
|
||||||
|
for (const [, attrs] of Object.entries(attributesByType)) {
|
||||||
|
const mandatoryAttrs = attrs.filter(attr => attr.include === 'mandatory');
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mandatoryAttrs.length === 1 && attrs.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: "Attribute validation failed: when a mandatory value exists, no other values are allowed for that attribute type"
|
||||||
|
}),
|
||||||
keyUsages: z
|
keyUsages: z
|
||||||
.object({
|
.object({
|
||||||
requiredUsages: z.array(z.string()).optional(),
|
requiredUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
|
||||||
optionalUsages: z.array(z.string()).optional()
|
optionalUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
extendedKeyUsages: z
|
extendedKeyUsages: z
|
||||||
.object({
|
.object({
|
||||||
requiredUsages: z.array(z.string()).optional(),
|
requiredUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
|
||||||
optionalUsages: z.array(z.string()).optional()
|
optionalUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
subjectAlternativeNames: z.array(sanSchema).optional(),
|
subjectAlternativeNames: z.array(sanSchema).optional(),
|
||||||
@@ -36,13 +67,13 @@ export const templateSchema = z.object({
|
|||||||
maxDuration: z
|
maxDuration: z
|
||||||
.object({
|
.object({
|
||||||
value: z.number().min(1, "Duration must be at least 1"),
|
value: z.number().min(1, "Duration must be at least 1"),
|
||||||
unit: z.enum(["days", "months", "years"])
|
unit: z.nativeEnum(CertDurationUnit)
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
minDuration: z
|
minDuration: z
|
||||||
.object({
|
.object({
|
||||||
value: z.number().min(1, "Duration must be at least 1"),
|
value: z.number().min(1, "Duration must be at least 1"),
|
||||||
unit: z.enum(["days", "months", "years"])
|
unit: z.nativeEnum(CertDurationUnit)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
+24
-44
@@ -1,47 +1,27 @@
|
|||||||
export const KEY_USAGES = [
|
import {
|
||||||
"digital_signature",
|
CertExtendedKeyUsageType,
|
||||||
"key_encipherment",
|
CertKeyUsageType,
|
||||||
"non_repudiation",
|
formatExtendedKeyUsage,
|
||||||
"data_encipherment",
|
formatKeyUsage
|
||||||
"key_agreement",
|
} from "./certificate-constants";
|
||||||
"key_cert_sign",
|
|
||||||
"crl_sign",
|
|
||||||
"encipher_only",
|
|
||||||
"decipher_only"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const EXTENDED_KEY_USAGES = [
|
|
||||||
"client_auth",
|
|
||||||
"server_auth",
|
|
||||||
"code_signing",
|
|
||||||
"email_protection",
|
|
||||||
"ocsp_signing",
|
|
||||||
"time_stamping"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const SUBJECT_ATTRIBUTE_TYPES = [
|
|
||||||
"common_name",
|
|
||||||
"organization_name",
|
|
||||||
"organization_unit",
|
|
||||||
"locality",
|
|
||||||
"state",
|
|
||||||
"country",
|
|
||||||
"email",
|
|
||||||
"street_address",
|
|
||||||
"postal_code"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const SAN_TYPES = ["dns_name", "ip_address", "email", "uri"] as const;
|
|
||||||
|
|
||||||
export const INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
|
|
||||||
|
|
||||||
export const formatUsageName = (usage: string): string => {
|
export const formatUsageName = (usage: string): string => {
|
||||||
|
try {
|
||||||
|
if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) {
|
||||||
|
return formatKeyUsage(usage as CertKeyUsageType);
|
||||||
|
}
|
||||||
|
if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) {
|
||||||
|
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
return usage.replace(/_/g, " ");
|
return usage.replace(/_/g, " ");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUsageState = (
|
export const getUsageState = (
|
||||||
usage: string,
|
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||||
requiredUsages: string[],
|
requiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||||
optionalUsages: string[]
|
optionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[]
|
||||||
): "required" | "optional" | undefined => {
|
): "required" | "optional" | undefined => {
|
||||||
if (requiredUsages.includes(usage)) return "required";
|
if (requiredUsages.includes(usage)) return "required";
|
||||||
if (optionalUsages.includes(usage)) return "optional";
|
if (optionalUsages.includes(usage)) return "optional";
|
||||||
@@ -49,12 +29,12 @@ export const getUsageState = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const toggleUsageState = (
|
export const toggleUsageState = (
|
||||||
usage: string,
|
usage: CertKeyUsageType | CertExtendedKeyUsageType,
|
||||||
newState: "required" | "optional" | undefined,
|
newState: "required" | "optional" | undefined,
|
||||||
currentRequiredUsages: string[],
|
currentRequiredUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||||
currentOptionalUsages: string[],
|
currentOptionalUsages: (CertKeyUsageType | CertExtendedKeyUsageType)[],
|
||||||
toggleRequired: (usage: string) => void,
|
toggleRequired: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void,
|
||||||
toggleOptional: (usage: string) => void
|
toggleOptional: (usage: CertKeyUsageType | CertExtendedKeyUsageType) => void
|
||||||
) => {
|
) => {
|
||||||
const isRequired = currentRequiredUsages.includes(usage);
|
const isRequired = currentRequiredUsages.includes(usage);
|
||||||
const isOptional = currentOptionalUsages.includes(usage);
|
const isOptional = currentOptionalUsages.includes(usage);
|
||||||
|
|||||||
+56
-1
@@ -7,6 +7,7 @@ import { Tooltip } from "@app/components/v2";
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
|
ProjectPermissionCertificateProfileActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
@@ -215,6 +216,13 @@ const PkiTemplatePolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
|
[ProjectPermissionPkiTemplateActions.IssueCert]: z.boolean().optional(),
|
||||||
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
|
[ProjectPermissionPkiTemplateActions.ListCerts]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
const CertificateProfilePolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionCertificateProfileActions.Read]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Create]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Edit]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Delete]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCertificateProfileActions.IssueCert]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const SecretEventsPolicyActionSchema = z.object({
|
const SecretEventsPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionSecretEventActions.SubscribeCreated]: z.boolean().optional(),
|
[ProjectPermissionSecretEventActions.SubscribeCreated]: z.boolean().optional(),
|
||||||
@@ -385,6 +393,12 @@ export const projectRoleFormSchema = z.object({
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
|
[ProjectPermissionSub.CertificateProfiles]: CertificateProfilePolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
|
conditions: ConditionSchema
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
@@ -465,6 +479,7 @@ 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 ||
|
||||||
@@ -808,7 +823,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||||
|
|
||||||
if (!formVal[subject]) formVal[subject] = [{}];
|
if (!formVal[subject]) {
|
||||||
|
formVal[subject] = [{ conditions: [] }];
|
||||||
|
}
|
||||||
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
||||||
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
@@ -1138,6 +1155,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||||
inverted
|
inverted
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (subject === ProjectPermissionSub.CertificateProfiles) {
|
||||||
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
|
formVal[subject]!.push({
|
||||||
|
[ProjectPermissionCertificateProfileActions.Edit]: action.includes(
|
||||||
|
ProjectPermissionCertificateProfileActions.Edit
|
||||||
|
),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Delete]: action.includes(
|
||||||
|
ProjectPermissionCertificateProfileActions.Delete
|
||||||
|
),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Create]: action.includes(
|
||||||
|
ProjectPermissionCertificateProfileActions.Create
|
||||||
|
),
|
||||||
|
[ProjectPermissionCertificateProfileActions.Read]: action.includes(
|
||||||
|
ProjectPermissionCertificateProfileActions.Read
|
||||||
|
),
|
||||||
|
[ProjectPermissionCertificateProfileActions.IssueCert]: action.includes(
|
||||||
|
ProjectPermissionCertificateProfileActions.IssueCert
|
||||||
|
),
|
||||||
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||||
|
inverted
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subject === ProjectPermissionSub.PamAccounts) {
|
if (subject === ProjectPermissionSub.PamAccounts) {
|
||||||
@@ -1522,6 +1566,16 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
|
{ label: "List Certificates", value: ProjectPermissionPkiTemplateActions.ListCerts }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
[ProjectPermissionSub.CertificateProfiles]: {
|
||||||
|
title: "Certificate Profiles",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: ProjectPermissionCertificateProfileActions.Read },
|
||||||
|
{ label: "Create", value: ProjectPermissionCertificateProfileActions.Create },
|
||||||
|
{ label: "Modify", value: ProjectPermissionCertificateProfileActions.Edit },
|
||||||
|
{ label: "Remove", value: ProjectPermissionCertificateProfileActions.Delete },
|
||||||
|
{ label: "Issue Certificates", value: ProjectPermissionCertificateProfileActions.IssueCert }
|
||||||
|
]
|
||||||
|
},
|
||||||
[ProjectPermissionSub.SshCertificateAuthorities]: {
|
[ProjectPermissionSub.SshCertificateAuthorities]: {
|
||||||
title: "SSH Certificate Authorities",
|
title: "SSH Certificate Authorities",
|
||||||
actions: [
|
actions: [
|
||||||
@@ -1869,6 +1923,7 @@ const CertificateManagerPermissionSubjects = (enabled = false) => ({
|
|||||||
[ProjectPermissionSub.PkiSyncs]: enabled,
|
[ProjectPermissionSub.PkiSyncs]: enabled,
|
||||||
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
[ProjectPermissionSub.CertificateAuthorities]: enabled,
|
||||||
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
[ProjectPermissionSub.CertificateTemplates]: enabled,
|
||||||
|
[ProjectPermissionSub.CertificateProfiles]: enabled,
|
||||||
[ProjectPermissionSub.Certificates]: enabled
|
[ProjectPermissionSub.Certificates]: enabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user