Address greptile comments

This commit is contained in:
Carlos Monastyrski
2025-10-17 02:36:44 -03:00
parent 87a91ce423
commit 534ce7158b
49 changed files with 1844 additions and 617 deletions
+14 -14
View File
@@ -56,9 +56,6 @@ import {
TCertificateBodies, TCertificateBodies,
TCertificateBodiesInsert, TCertificateBodiesInsert,
TCertificateBodiesUpdate, TCertificateBodiesUpdate,
TCertificateProfiles,
TCertificateProfilesInsert,
TCertificateProfilesUpdate,
TCertificates, TCertificates,
TCertificateSecrets, TCertificateSecrets,
TCertificateSecretsInsert, TCertificateSecretsInsert,
@@ -71,9 +68,6 @@ import {
TCertificateTemplates, TCertificateTemplates,
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate, TCertificateTemplatesUpdate,
TCertificateTemplatesV2,
TCertificateTemplatesV2Insert,
TCertificateTemplatesV2Update,
TDynamicSecretLeases, TDynamicSecretLeases,
TDynamicSecretLeasesInsert, TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate, TDynamicSecretLeasesUpdate,
@@ -275,6 +269,12 @@ import {
TPkiApiEnrollmentConfigs, TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert, TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate, TPkiApiEnrollmentConfigsUpdate,
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate,
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert,
TPkiCertificateTemplatesV2Update,
TPkiCollectionItems, TPkiCollectionItems,
TPkiCollectionItemsInsert, TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate, TPkiCollectionItemsUpdate,
@@ -683,15 +683,15 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate TCertificateTemplatesUpdate
>; >;
[TableName.CertificateTemplateV2]: KnexOriginal.CompositeTableType< [TableName.PkiCertificateTemplateV2]: KnexOriginal.CompositeTableType<
TCertificateTemplatesV2, TPkiCertificateTemplatesV2,
TCertificateTemplatesV2Insert, TPkiCertificateTemplatesV2Insert,
TCertificateTemplatesV2Update TPkiCertificateTemplatesV2Update
>; >;
[TableName.CertificateProfile]: KnexOriginal.CompositeTableType< [TableName.PkiCertificateProfile]: KnexOriginal.CompositeTableType<
TCertificateProfiles, TPkiCertificateProfiles,
TCertificateProfilesInsert, TPkiCertificateProfilesInsert,
TCertificateProfilesUpdate TPkiCertificateProfilesUpdate
>; >;
[TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType< [TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType<
TPkiEstEnrollmentConfigs, TPkiEstEnrollmentConfigs,
@@ -4,8 +4,8 @@ import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.CertificateTemplateV2))) { if (!(await knex.schema.hasTable(TableName.PkiCertificateTemplateV2))) {
await knex.schema.createTable(TableName.CertificateTemplateV2, (t) => { await knex.schema.createTable(TableName.PkiCertificateTemplateV2, (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); t.foreign("projectId").references("id").inTable(TableName.Project);
@@ -25,7 +25,7 @@ export async function up(knex: Knex): Promise<void> {
t.unique(["name", "projectId"]); t.unique(["name", "projectId"]);
}); });
await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2); await createOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2);
} }
if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) { if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) {
@@ -55,17 +55,17 @@ export async function up(knex: Knex): Promise<void> {
await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
} }
if (!(await knex.schema.hasTable(TableName.CertificateProfile))) { if (!(await knex.schema.hasTable(TableName.PkiCertificateProfile))) {
await knex.schema.createTable(TableName.CertificateProfile, (t) => { await knex.schema.createTable(TableName.PkiCertificateProfile, (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); t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("caId").notNullable(); t.uuid("caId").notNullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority); t.foreign("caId").references("id").inTable(TableName.CertificateAuthority);
t.uuid("certificateTemplateId").notNullable(); t.uuid("certificateTemplateId").notNullable();
t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2); t.foreign("certificateTemplateId").references("id").inTable(TableName.PkiCertificateTemplateV2);
t.string("slug").notNullable(); t.string("slug").notNullable();
t.string("description"); t.string("description");
@@ -82,13 +82,13 @@ export async function up(knex: Knex): Promise<void> {
t.unique(["slug", "projectId"]); t.unique(["slug", "projectId"]);
}); });
await createOnUpdateTrigger(knex, TableName.CertificateProfile); await createOnUpdateTrigger(knex, TableName.PkiCertificateProfile);
} }
if (!(await knex.schema.hasColumn(TableName.Certificate, "profileId"))) { if (!(await knex.schema.hasColumn(TableName.Certificate, "profileId"))) {
await knex.schema.alterTable(TableName.Certificate, (t) => { await knex.schema.alterTable(TableName.Certificate, (t) => {
t.uuid("profileId"); t.uuid("profileId");
t.foreign("profileId").references("id").inTable(TableName.CertificateProfile).onDelete("SET NULL"); t.foreign("profileId").references("id").inTable(TableName.PkiCertificateProfile).onDelete("SET NULL");
t.index("profileId"); t.index("profileId");
}); });
} }
@@ -103,8 +103,8 @@ export async function down(knex: Knex): Promise<void> {
}); });
} }
await knex.schema.dropTableIfExists(TableName.CertificateProfile); await knex.schema.dropTableIfExists(TableName.PkiCertificateProfile);
await dropOnUpdateTrigger(knex, TableName.CertificateProfile); await dropOnUpdateTrigger(knex, TableName.PkiCertificateProfile);
await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig); await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig);
await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig); await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
@@ -112,6 +112,6 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig); await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig);
await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig); await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig);
await knex.schema.dropTableIfExists(TableName.CertificateTemplateV2); await knex.schema.dropTableIfExists(TableName.PkiCertificateTemplateV2);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateV2); await dropOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2);
} }
+2 -2
View File
@@ -16,11 +16,9 @@ export * from "./certificate-authority-certs";
export * from "./certificate-authority-crl"; export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret"; export * from "./certificate-authority-secret";
export * from "./certificate-bodies"; export * from "./certificate-bodies";
export * from "./certificate-profiles";
export * from "./certificate-secrets"; export * from "./certificate-secrets";
export * from "./certificate-template-est-configs"; export * from "./certificate-template-est-configs";
export * from "./certificate-templates"; export * from "./certificate-templates";
export * from "./certificate-templates-v2";
export * from "./certificates"; export * from "./certificates";
export * from "./dynamic-secret-leases"; export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets"; export * from "./dynamic-secrets";
@@ -95,6 +93,8 @@ export * from "./pam-resources";
export * from "./pam-sessions"; export * from "./pam-sessions";
export * from "./pki-alerts"; export * from "./pki-alerts";
export * from "./pki-api-enrollment-configs"; export * from "./pki-api-enrollment-configs";
export * from "./pki-certificate-profiles";
export * from "./pki-certificate-templates-v2";
export * from "./pki-collection-items"; export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./pki-est-enrollment-configs"; export * from "./pki-est-enrollment-configs";
+2 -2
View File
@@ -23,8 +23,8 @@ export enum TableName {
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates", CertificateTemplate = "certificate_templates",
CertificateTemplateV2 = "certificate_templates_v2", PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
CertificateProfile = "certificate_profiles", PkiCertificateProfile = "pki_certificate_profiles",
PkiEstEnrollmentConfig = "pki_est_enrollment_configs", PkiEstEnrollmentConfig = "pki_est_enrollment_configs",
PkiApiEnrollmentConfig = "pki_api_enrollment_configs", PkiApiEnrollmentConfig = "pki_api_enrollment_configs",
PkiSubscriber = "pki_subscribers", PkiSubscriber = "pki_subscribers",
@@ -7,7 +7,7 @@ import { z } from "zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const CertificateProfilesSchema = z.object({ export const PkiCertificateProfilesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string(),
caId: z.string().uuid(), caId: z.string().uuid(),
@@ -21,6 +21,8 @@ export const CertificateProfilesSchema = z.object({
updatedAt: z.date() updatedAt: z.date()
}); });
export type TCertificateProfiles = z.infer<typeof CertificateProfilesSchema>; export type TPkiCertificateProfiles = z.infer<typeof PkiCertificateProfilesSchema>;
export type TCertificateProfilesInsert = Omit<z.input<typeof CertificateProfilesSchema>, TImmutableDBKeys>; export type TPkiCertificateProfilesInsert = Omit<z.input<typeof PkiCertificateProfilesSchema>, TImmutableDBKeys>;
export type TCertificateProfilesUpdate = Partial<Omit<z.input<typeof CertificateProfilesSchema>, TImmutableDBKeys>>; export type TPkiCertificateProfilesUpdate = Partial<
Omit<z.input<typeof PkiCertificateProfilesSchema>, TImmutableDBKeys>
>;
@@ -7,7 +7,7 @@ import { z } from "zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const CertificateTemplatesV2Schema = z.object({ export const PkiCertificateTemplatesV2Schema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string(),
name: z.string(), name: z.string(),
@@ -22,8 +22,8 @@ export const CertificateTemplatesV2Schema = z.object({
updatedAt: z.date() updatedAt: z.date()
}); });
export type TCertificateTemplatesV2 = z.infer<typeof CertificateTemplatesV2Schema>; export type TPkiCertificateTemplatesV2 = z.infer<typeof PkiCertificateTemplatesV2Schema>;
export type TCertificateTemplatesV2Insert = Omit<z.input<typeof CertificateTemplatesV2Schema>, TImmutableDBKeys>; export type TPkiCertificateTemplatesV2Insert = Omit<z.input<typeof PkiCertificateTemplatesV2Schema>, TImmutableDBKeys>;
export type TCertificateTemplatesV2Update = Partial< export type TPkiCertificateTemplatesV2Update = Partial<
Omit<z.input<typeof CertificateTemplatesV2Schema>, TImmutableDBKeys> Omit<z.input<typeof PkiCertificateTemplatesV2Schema>, TImmutableDBKeys>
>; >;
@@ -4,24 +4,53 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => { export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => { const getIdentifierType = async ({
identifier,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: {
identifier: string;
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}): Promise<"template" | "profile" | null> => {
try { try {
await server.services.certificateProfile.getEstConfigurationByProfile({ await server.services.certificateProfile.getEstConfigurationByProfile({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId: identifier profileId: identifier
}); });
return "profile"; return "profile";
} catch { } catch (profileError) {
try { try {
await server.services.certificateTemplate.getEstConfiguration({ await server.services.certificateTemplate.getEstConfiguration({
isInternal: true, isInternal: false,
actor,
actorId,
actorAuthMethod,
actorOrgId,
certificateTemplateId: identifier certificateTemplateId: identifier
}); });
return "template"; return "template";
} catch { } catch (templateError) {
server.log.debug(
{
identifier,
profileError: profileError instanceof Error ? profileError.message : "Unknown error",
templateError: templateError instanceof Error ? templateError.message : "Unknown error"
},
"EST identifier not found as profile or template"
);
return null; return null;
} }
} }
@@ -80,7 +109,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
const identifier = urlFragments.slice(-2)[0]; const identifier = urlFragments.slice(-2)[0];
const identifierType = await getIdentifierType(identifier); const identifierType = await getIdentifierType({
identifier,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
if (!identifierType) { if (!identifierType) {
res.raw.statusCode = 404; res.raw.statusCode = 404;
res.raw.setHeader("Content-Type", "text/plain"); res.raw.setHeader("Content-Type", "text/plain");
@@ -92,6 +127,10 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
let estConfig; let estConfig;
if (identifierType === "profile") { if (identifierType === "profile") {
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({ estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: identifier profileId: identifier
}); });
} else { } else {
@@ -149,7 +188,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params; const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier); const identifierType = await getIdentifierType({
identifier,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
if (!identifierType) { if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" }); throw new BadRequestError({ message: "Certificate template or profile not found" });
@@ -190,7 +235,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params; const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier); const identifierType = await getIdentifierType({
identifier,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
if (!identifierType) { if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" }); throw new BadRequestError({ message: "Certificate template or profile not found" });
@@ -230,7 +281,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params; const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier); const identifierType = await getIdentifierType({
identifier,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
if (!identifierType) { if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" }); throw new BadRequestError({ message: "Certificate template or profile not found" });
@@ -1,7 +1,7 @@
import RE2 from "re2"; import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { CertificateProfilesSchema } from "@app/db/schemas"; import { PkiCertificateProfilesSchema } 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";
@@ -72,7 +72,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
), ),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: CertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema
}) })
} }
}, },
@@ -126,7 +126,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfiles: CertificateProfilesSchema.extend({ certificateProfiles: PkiCertificateProfilesSchema.extend({
metrics: z metrics: z
.object({ .object({
profileId: z.string(), profileId: z.string(),
@@ -177,7 +177,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles], tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({ params: z.object({
id: z.string().min(1) id: z.string().uuid()
}), }),
querystring: z.object({ querystring: z.object({
includeMetrics: z.coerce.boolean().optional().default(false), includeMetrics: z.coerce.boolean().optional().default(false),
@@ -185,7 +185,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: CertificateProfilesSchema.extend({ certificateProfile: PkiCertificateProfilesSchema.extend({
certificateAuthority: z certificateAuthority: z
.object({ .object({
id: z.string(), id: z.string(),
@@ -207,7 +207,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
id: z.string(), id: z.string(),
disableBootstrapCaValidation: z.boolean(), disableBootstrapCaValidation: z.boolean(),
hashedPassphrase: z.string(), hashedPassphrase: z.string(),
encryptedCaChain: z.any() encryptedCaChain: z.string()
}) })
.optional(), .optional(),
apiConfig: z apiConfig: z
@@ -288,7 +288,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: CertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema
}) })
} }
}, },
@@ -317,7 +317,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles], tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({ params: z.object({
id: z.string().min(1) id: z.string().uuid()
}), }),
body: z body: z
.object({ .object({
@@ -363,7 +363,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
), ),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: CertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema
}) })
} }
}, },
@@ -408,7 +408,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
}), }),
response: { response: {
200: z.object({ 200: z.object({
certificateProfile: CertificateProfilesSchema certificateProfile: PkiCertificateProfilesSchema
}) })
} }
}, },
@@ -448,11 +448,11 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles], tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({ params: z.object({
id: z.string().min(1) id: z.string().uuid()
}), }),
querystring: z.object({ querystring: z.object({
offset: z.number().min(0).default(0), offset: z.coerce.number().min(0).default(0),
limit: z.number().min(1).max(100).default(20), limit: z.coerce.number().min(1).max(100).default(20),
status: z.enum(["active", "expired", "revoked"]).optional(), status: z.enum(["active", "expired", "revoked"]).optional(),
search: z.string().optional() search: z.string().optional()
}), }),
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
@@ -93,9 +94,19 @@ const templateV2SanSchema = z
const templateV2ValiditySchema = z.object({ const templateV2ValiditySchema = z.object({
max: z max: z
.string() .string()
.regex(/^\d+[dhmy]$/, { .refine(
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'" (val) => {
}) if (!val) return true;
if (val.length < 2) return false;
const unit = val.slice(-1);
const number = val.slice(0, -1);
const digitRegex = new RE2("^\\d+$");
return ["d", "h", "m", "y"].includes(unit) && digitRegex.test(number);
},
{
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
}
)
.optional() .optional()
}); });
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
@@ -90,24 +91,28 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
keyUsages: req.body.keyUsages, keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages, extendedKeyUsages: req.body.extendedKeyUsages,
altNames: req.body.subjectAltNames altNames: req.body.subjectAltNames
? req.body.subjectAltNames ? (() => {
.split(", ") const splitRegex = new RE2("[,;]+");
.map((name) => name.trim()) return req.body.subjectAltNames
.map((name) => { .split(splitRegex)
const mappedType = validateAndMapAltNameType(name); .map((name) => name.trim())
if (!mappedType) return null; .filter((name) => name.length > 0)
const typeMapping = { .map((name) => {
dns: "dns_name", const mappedType = validateAndMapAltNameType(name);
ip: "ip_address", if (!mappedType) return null;
email: "email", const typeMapping = {
url: "uri" dns: "dns_name",
} as const; ip: "ip_address",
return { email: "email",
type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType, url: "uri"
value: mappedType.value } as const;
}; return {
}) type: typeMapping[mappedType.type] as CertSubjectAlternativeNameType,
.filter((item): item is NonNullable<typeof item> => item !== null) value: mappedType.value
};
})
.filter((item): item is NonNullable<typeof item> => item !== null);
})()
: undefined, : undefined,
validity: { validity: {
ttl: req.body.ttl ttl: req.body.ttl
@@ -223,7 +228,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
certificateProfileId: req.body.profileId, certificateProfileId: req.body.profileId,
certificateId: data.certificateId, certificateId: data.certificateId,
profileName: data.profileName, profileName: data.profileName,
commonName: req.body.csr || "" commonName: ""
} }
} }
}); });
@@ -136,7 +136,7 @@ describe("signatureAlgorithmToAlgCfg", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_secp521r1"); const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_secp521r1");
expect(result.name).toBe("ECDSA"); expect(result.name).toBe("ECDSA");
expect(result.namedCurve).toBe("P-256"); expect(result.namedCurve).toBe("P-521");
expect(result.hash).toBe("SHA-256"); expect(result.hash).toBe("SHA-256");
}); });
@@ -101,8 +101,16 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgorithm: CertKeyAlgorithm | string) => { 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.
if (!signatureAlgorithm || typeof signatureAlgorithm !== "string" || !signatureAlgorithm.includes("-")) {
throw new Error(`Invalid signature algorithm format: ${signatureAlgorithm}`);
}
const [keyType, hashType] = signatureAlgorithm.split("-"); const [keyType, hashType] = signatureAlgorithm.split("-");
if (!keyType || !hashType) {
throw new Error(`Malformed signature algorithm: ${signatureAlgorithm}`);
}
const normalizeHashType = (hash: string) => { const normalizeHashType = (hash: string) => {
const upperHash = hash.toUpperCase(); const upperHash = hash.toUpperCase();
@@ -118,7 +126,7 @@ export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgori
if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384"; if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384";
if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512"; if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512";
return hash; throw new Error(`Unsupported hash algorithm: ${hash}`);
}; };
const normalizedHash = hashType ? normalizeHashType(hashType) : undefined; const normalizedHash = hashType ? normalizeHashType(hashType) : undefined;
@@ -136,7 +144,16 @@ export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgori
const is384Curve = const is384Curve =
keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384"; keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384";
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const namedCurve = is384Curve ? "P-384" : "P-256"; const is521Curve = keyAlgorithm === "EC_secp521r1" || keyAlgorithm === "EC_P521";
// eslint-disable-next-line no-case-declarations
let namedCurve: string;
if (is521Curve) {
namedCurve = "P-521";
} else if (is384Curve) {
namedCurve = "P-384";
} else {
namedCurve = "P-256";
}
return { return {
name: "ECDSA", name: "ECDSA",
namedCurve, namedCurve,
@@ -18,7 +18,7 @@ export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick(
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
}); });
@@ -26,7 +26,7 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
}); });
@@ -191,6 +191,23 @@ export const mapLegacyExtendedKeyUsageToStandard = (usage: string): CertExtended
} }
}; };
export enum CertKeyAlgorithm {
RSA_2048 = "RSA_2048",
RSA_3072 = "RSA_3072",
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
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 const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType); export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType); export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType); export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
@@ -199,3 +216,5 @@ export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType); export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule); export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect); export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const KEY_ALGORITHM_OPTIONS = Object.values(CertKeyAlgorithm);
export const SIGNATURE_ALGORITHM_OPTIONS = Object.values(CertSignatureAlgorithm);
@@ -1,3 +1,5 @@
import RE2 from "re2";
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
@@ -78,6 +80,43 @@ export const buildCertificateSubjectFromTemplate = (
return subject; return subject;
}; };
const isWildcardPattern = (value: string): boolean => {
return value.includes("*");
};
const createWildcardRegex = (pattern: string): RE2 => {
const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g);
const escaped = pattern.replace(escapeRegex, "\\$&");
const wildcardRegex = new RE2(/\*/g);
const regexPattern = escaped.replace(wildcardRegex, ".*");
return new RE2(`^${regexPattern}$`);
};
const validateValueAgainstPatterns = (value: string, patterns: string[]): boolean => {
if (!patterns || patterns.length === 0) {
return false;
}
for (const pattern of patterns) {
if (isWildcardPattern(pattern)) {
try {
const regex = createWildcardRegex(pattern);
if (regex.test(value)) {
return true;
}
} catch {
if (pattern === value) {
return true;
}
}
} else if (pattern === value) {
return true;
}
}
return false;
};
export const buildSubjectAlternativeNamesFromTemplate = ( export const buildSubjectAlternativeNamesFromTemplate = (
request: { subjectAlternativeNames?: Array<{ type: string; value: string }> }, request: { subjectAlternativeNames?: Array<{ type: string; value: string }> },
templateSans?: Array<{ templateSans?: Array<{
@@ -98,7 +137,25 @@ export const buildSubjectAlternativeNamesFromTemplate = (
const allowedSans: string[] = []; const allowedSans: string[] = [];
request.subjectAlternativeNames.forEach((san) => { request.subjectAlternativeNames.forEach((san) => {
allowedSans.push(san.value); const templateSan = templateSans.find((template) => template.type === san.type);
if (!templateSan) {
allowedSans.push(san.value);
return;
}
if (templateSan.denied && validateValueAgainstPatterns(san.value, templateSan.denied)) {
throw new Error(`SAN value '${san.value}' is explicitly denied for type '${san.type}'`);
}
const isRequired = templateSan.required && validateValueAgainstPatterns(san.value, templateSan.required);
const isAllowed = templateSan.allowed && validateValueAgainstPatterns(san.value, templateSan.allowed);
if (isRequired || isAllowed || (!templateSan.allowed && !templateSan.required)) {
allowedSans.push(san.value);
} else {
throw new Error(`SAN value '${san.value}' is not allowed for type '${san.type}'`);
}
}); });
return allowedSans.join(","); return allowedSans.join(",");
@@ -173,6 +173,11 @@ export const certificateEstV3ServiceFactory = ({
} }
const certTemplate = await certificateTemplateDAL.findById(profile.certificateTemplateId); const certTemplate = await certificateTemplateDAL.findById(profile.certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: `Certificate template with ID '${profile.certificateTemplateId}' not found`
});
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0]; const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
@@ -7,118 +7,188 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
import { import {
EnrollmentType, EnrollmentType,
TCertificateProfile,
TCertificateProfileCertificate, TCertificateProfileCertificate,
TCertificateProfileInsert, TCertificateProfileInsert,
TCertificateProfileMetrics, TCertificateProfileMetrics,
TCertificateProfileUpdate TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
} from "./certificate-profile-types"; } from "./certificate-profile-types";
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>; export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
export const certificateProfileDALFactory = (db: TDbClient) => { export const certificateProfileDALFactory = (db: TDbClient) => {
const certificateProfileOrm = ormify(db, TableName.CertificateProfile); const certificateProfileOrm = ormify(db, TableName.PkiCertificateProfile);
const create = async (data: TCertificateProfileInsert, tx?: Knex) => { const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise<TCertificateProfile> => {
try { try {
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).insert(data).returning("*"); const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile).insert(data).returning("*")) as [
TCertificateProfile
];
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Create certificate profile" }); throw new DatabaseError({ error, name: "Create certificate profile" });
} }
}; };
const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex) => { const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise<TCertificateProfile> => {
try { try {
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile) const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ id }) .where({ id })
.update(data) .update(data)
.returning("*"); .returning("*")) as [TCertificateProfile];
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Update certificate profile" }); throw new DatabaseError({ error, name: "Update certificate profile" });
} }
}; };
const deleteById = async (id: string, tx?: Knex) => { const deleteById = async (id: string, tx?: Knex): Promise<TCertificateProfile> => {
try { try {
const [certificateProfile] = await (tx || db)(TableName.CertificateProfile).where({ id }).del().returning("*"); const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ id })
.del()
.returning("*")) as [TCertificateProfile];
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Delete certificate profile" }); throw new DatabaseError({ error, name: "Delete certificate profile" });
} }
}; };
const findById = async (id: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex): Promise<TCertificateProfile | undefined> => {
try { try {
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ id }).first(); const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first()) as
| TCertificateProfile
| undefined;
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by id" }); throw new DatabaseError({ error, name: "Find certificate profile by id" });
} }
}; };
const findByIdWithConfigs = async (id: string, tx?: Knex) => { const findByIdWithConfigs = async (id: string, tx?: Knex): Promise<TCertificateProfileWithConfigs | undefined> => {
try { try {
const result = await (tx || db)(TableName.CertificateProfile) const query = (tx || db)(TableName.PkiCertificateProfile)
.select(
selectAllTableCols(TableName.CertificateProfile),
(tx || db).ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
(tx || db).ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"),
(tx || db).ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
(tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"),
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
(tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
(tx || db)
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estConfigDisableBootstrapCaValidation"),
(tx || db)
.ref("hashedPassphrase")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estConfigHashedPassphrase"),
(tx || db)
.ref("encryptedCaChain")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estConfigEncryptedCaChain"),
(tx || db).ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
(tx || db).ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
(tx || db).ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
)
.leftJoin( .leftJoin(
TableName.CertificateAuthority, TableName.CertificateAuthority,
`${TableName.CertificateProfile}.caId`, `${TableName.PkiCertificateProfile}.caId`,
`${TableName.CertificateAuthority}.id` `${TableName.CertificateAuthority}.id`
) )
.leftJoin( .leftJoin(
TableName.CertificateTemplateV2, TableName.PkiCertificateTemplateV2,
`${TableName.CertificateProfile}.certificateTemplateId`, `${TableName.PkiCertificateProfile}.certificateTemplateId`,
`${TableName.CertificateTemplateV2}.id` `${TableName.PkiCertificateTemplateV2}.id`
) )
.leftJoin( .leftJoin(
TableName.PkiEstEnrollmentConfig, TableName.PkiEstEnrollmentConfig,
`${TableName.CertificateProfile}.estConfigId`, `${TableName.PkiCertificateProfile}.estConfigId`,
`${TableName.PkiEstEnrollmentConfig}.id` `${TableName.PkiEstEnrollmentConfig}.id`
) )
.leftJoin( .leftJoin(
TableName.PkiApiEnrollmentConfig, TableName.PkiApiEnrollmentConfig,
`${TableName.CertificateProfile}.apiConfigId`, `${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id` `${TableName.PkiApiEnrollmentConfig}.id`
) )
.where(`${TableName.CertificateProfile}.id`, id) .select(selectAllTableCols(TableName.PkiCertificateProfile))
.select(
db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"),
db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
db.ref("id").withSchema(TableName.PkiCertificateTemplateV2).as("templateId"),
db.ref("projectId").withSchema(TableName.PkiCertificateTemplateV2).as("templateProjectId"),
db.ref("name").withSchema(TableName.PkiCertificateTemplateV2).as("templateName"),
db.ref("description").withSchema(TableName.PkiCertificateTemplateV2).as("templateDescription"),
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estConfigDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
)
.where(`${TableName.PkiCertificateProfile}.id`, id)
.first(); .first();
return result; const result = await query;
if (!result) return undefined;
const estConfig =
result.estConfigEncryptedCaChain && result.estConfigId && result.estConfigHashedPassphrase
? ({
id: result.estConfigId,
disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation,
hashedPassphrase: result.estConfigHashedPassphrase,
encryptedCaChain: result.estConfigEncryptedCaChain.toString("base64")
} as TCertificateProfileWithConfigs["estConfig"])
: undefined;
const apiConfig = result.apiConfigId
? ({
id: result.apiConfigId,
autoRenew: !!result.apiConfigAutoRenew,
autoRenewDays: result.apiConfigAutoRenewDays || undefined
} as TCertificateProfileWithConfigs["apiConfig"])
: undefined;
const certificateAuthority =
result.caId && result.caProjectId && result.caStatus && result.caName
? ({
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"])
: undefined;
const certificateTemplate =
result.templateId && result.templateProjectId && result.templateName
? ({
id: result.templateId,
projectId: result.templateProjectId,
name: result.templateName,
description: result.templateDescription || undefined
} as TCertificateProfileWithConfigs["certificateTemplate"])
: undefined;
const transformedResult: TCertificateProfileWithConfigs = {
id: result.id,
projectId: result.projectId,
caId: result.caId,
certificateTemplateId: result.certificateTemplateId,
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
estConfig,
apiConfig,
certificateAuthority,
certificateTemplate
};
return transformedResult;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by id with configs" }); throw new DatabaseError({ error, name: "Find certificate profile by id with configs" });
} }
}; };
const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => { const findBySlugAndProjectId = async (
slug: string,
projectId: string,
tx?: Knex
): Promise<TCertificateProfile | undefined> => {
try { try {
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ slug, projectId }).first(); const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ slug, projectId })
.first()) as TCertificateProfile | undefined;
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by slug and project id" }); throw new DatabaseError({ error, name: "Find certificate profile by slug and project id" });
@@ -137,7 +207,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
expiringDays?: number; expiringDays?: number;
} = {}, } = {},
tx?: Knex tx?: Knex
) => { ): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[]> => {
try { try {
const { const {
offset = 0, offset = 0,
@@ -149,25 +219,27 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
expiringDays = 7 expiringDays = 7
} = options; } = options;
let query = (tx || db)(TableName.CertificateProfile).where( let query = (tx || db)(TableName.PkiCertificateProfile).where(
`${TableName.CertificateProfile}.projectId`, `${TableName.PkiCertificateProfile}.projectId`,
projectId projectId
); );
if (search) { if (search) {
query = query.where((builder) => { query = query.where((builder) => {
void builder void builder.where((qb) => {
.whereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`) void qb
.orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`); .whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`)
.orWhereILike(`${TableName.PkiCertificateProfile}.description`, `%${search}%`);
});
}); });
} }
if (enrollmentType) { if (enrollmentType) {
query = query.where(`${TableName.CertificateProfile}.enrollmentType`, enrollmentType); query = query.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType);
} }
if (caId) { if (caId) {
query = query.where(`${TableName.CertificateProfile}.caId`, caId); query = query.where(`${TableName.PkiCertificateProfile}.caId`, caId);
} }
if (includeMetrics) { if (includeMetrics) {
@@ -176,9 +248,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
expiringDate.setDate(now.getDate() + expiringDays); expiringDate.setDate(now.getDate() + expiringDays);
const certificateProfiles = await query const certificateProfiles = await query
.leftJoin(TableName.Certificate, `${TableName.CertificateProfile}.id`, `${TableName.Certificate}.profileId`) .leftJoin(
TableName.Certificate,
`${TableName.PkiCertificateProfile}.id`,
`${TableName.Certificate}.profileId`
)
.select( .select(
selectAllTableCols(TableName.CertificateProfile), selectAllTableCols(TableName.PkiCertificateProfile),
db.raw("COUNT(certificates.id) as total_certificates"), db.raw("COUNT(certificates.id) as total_certificates"),
db.raw( db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates', 'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
@@ -194,21 +270,21 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
), ),
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates') db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
) )
.groupBy(`${TableName.CertificateProfile}.id`) .groupBy(`${TableName.PkiCertificateProfile}.id`)
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc") .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset) .offset(offset)
.limit(limit); .limit(limit);
return certificateProfiles; return certificateProfiles as TCertificateProfileWithRawMetrics[];
} }
const certificateProfiles = await query const certificateProfiles = await query
.select(selectAllTableCols(TableName.CertificateProfile)) .select(selectAllTableCols(TableName.PkiCertificateProfile))
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc") .orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset) .offset(offset)
.limit(limit); .limit(limit);
return certificateProfiles; return certificateProfiles as TCertificateProfile[];
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate profiles by project id" }); throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
} }
@@ -222,15 +298,17 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
caId?: string; caId?: string;
} = {}, } = {},
tx?: Knex tx?: Knex
) => { ): Promise<number> => {
try { try {
const { search, enrollmentType, caId } = options; const { search, enrollmentType, caId } = options;
let query = (tx || db)(TableName.CertificateProfile).where({ projectId }); let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
if (search) { if (search) {
query = query.where((builder) => { query = query.where((builder) => {
void builder.orWhereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`); void builder.where((qb) => {
void qb.whereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`);
});
}); });
} }
@@ -249,11 +327,15 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
} }
}; };
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { const findByNameAndProjectId = async (
name: string,
projectId: string,
tx?: Knex
): Promise<TCertificateProfile | undefined> => {
try { try {
const certificateProfile = await (tx || db)(TableName.CertificateProfile) const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ slug: name, projectId }) .where({ slug: name, projectId })
.first(); .first()) as TCertificateProfile | undefined;
return certificateProfile; return certificateProfile;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" }); throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
@@ -278,7 +360,9 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
if (search) { if (search) {
query = query.where((builder) => { query = query.where((builder) => {
void builder.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`); void builder.where((qb) => {
void qb.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`);
});
}); });
} }
@@ -311,7 +395,10 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
.offset(offset) .offset(offset)
.limit(limit); .limit(limit);
return certificates; return certificates.map((cert) => ({
...cert,
revokedAt: cert.revokedAt ?? null
}));
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Get certificates by profile" }); throw new DatabaseError({ error, name: "Get certificates by profile" });
} }
@@ -5,7 +5,7 @@ import { EnrollmentType } from "./certificate-profile-types";
export const createCertificateProfileSchema = z export const createCertificateProfileSchema = z
.object({ .object({
projectId: z.string().min(1), projectId: z.string().uuid("Project ID must be valid"),
caId: z.string().uuid(), caId: z.string().uuid(),
certificateTemplateId: z.string().uuid(), certificateTemplateId: z.string().uuid(),
slug: z slug: z
@@ -103,12 +103,12 @@ export const getCertificateProfileByIdSchema = z.object({
}); });
export const getCertificateProfileBySlugSchema = z.object({ export const getCertificateProfileBySlugSchema = z.object({
projectId: z.string().min(1), projectId: z.string().uuid("Project ID must be valid"),
slug: z.string().min(1) slug: z.string().min(1)
}); });
export const listCertificateProfilesSchema = z.object({ export const listCertificateProfilesSchema = z.object({
projectId: z.string().min(1), projectId: z.string().uuid("Project ID must be valid"),
offset: z.coerce.number().min(0).default(0), offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20), limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(), search: z.string().optional(),
@@ -712,7 +712,7 @@ describe("CertificateProfileService", () => {
estConfig: { estConfig: {
disableBootstrapCaValidation: false, disableBootstrapCaValidation: false,
passphrase: "secret-passphrase", passphrase: "secret-passphrase",
encryptedCaChain: "encrypted-ca-chain-data" encryptedCaChain: Buffer.from("test-ca-chain-data").toString("base64")
} }
}; };
@@ -1117,14 +1117,17 @@ describe("CertificateProfileService", () => {
...sampleProfileWithConfigs, ...sampleProfileWithConfigs,
id: profileId, id: profileId,
enrollmentType: EnrollmentType.EST, enrollmentType: EnrollmentType.EST,
estConfigEncryptedCaChain: Buffer.from("mock-ca-chain"), estConfig: {
estConfigDisableBootstrapCaValidation: false, id: "est-config-123",
estConfigHashedPassphrase: "hashed-passphrase" disableBootstrapCaValidation: false,
hashedPassphrase: "hashed-passphrase",
encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64")
}
} as TCertificateProfileWithConfigs; } as TCertificateProfileWithConfigs;
(mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile);
const result = await service.getEstConfigurationByProfile({ profileId }); const result = await service.getEstConfigurationByProfile({ ...mockActor, profileId });
expect(result).toEqual({ expect(result).toEqual({
orgId: "project-123", orgId: "project-123",
@@ -1139,7 +1142,7 @@ describe("CertificateProfileService", () => {
const profileId = "non-existent-profile"; const profileId = "non-existent-profile";
(mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(null); (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(null);
await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(NotFoundError); await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(NotFoundError);
}); });
it("should throw ForbiddenRequestError when profile is not configured for EST enrollment", async () => { it("should throw ForbiddenRequestError when profile is not configured for EST enrollment", async () => {
@@ -1148,15 +1151,20 @@ describe("CertificateProfileService", () => {
...sampleProfileWithConfigs, ...sampleProfileWithConfigs,
id: profileId, id: profileId,
enrollmentType: EnrollmentType.API, // Wrong enrollment type enrollmentType: EnrollmentType.API, // Wrong enrollment type
estConfigEncryptedCaChain: Buffer.from("mock-ca-chain"), estConfig: {
estConfigDisableBootstrapCaValidation: false, id: "est-config-123",
estConfigHashedPassphrase: "hashed-passphrase" disableBootstrapCaValidation: false,
hashedPassphrase: "hashed-passphrase",
encryptedCaChain: Buffer.from("mock-ca-chain").toString("base64")
}
} as TCertificateProfileWithConfigs; } as TCertificateProfileWithConfigs;
(mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile);
await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(ForbiddenRequestError); await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(
await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow( ForbiddenRequestError
);
await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(
"Profile is not configured for EST enrollment" "Profile is not configured for EST enrollment"
); );
}); });
@@ -1167,15 +1175,13 @@ describe("CertificateProfileService", () => {
...sampleProfileWithConfigs, ...sampleProfileWithConfigs,
id: profileId, id: profileId,
enrollmentType: EnrollmentType.EST, enrollmentType: EnrollmentType.EST,
estConfigEncryptedCaChain: null, // Missing EST config estConfig: undefined // Missing EST config
estConfigDisableBootstrapCaValidation: false,
estConfigHashedPassphrase: "hashed-passphrase"
} as TCertificateProfileWithConfigs; } as TCertificateProfileWithConfigs;
(mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile); (mockCertificateProfileDAL.findByIdWithConfigs as any).mockResolvedValue(mockProfile);
await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow(NotFoundError); await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(NotFoundError);
await expect(service.getEstConfigurationByProfile({ profileId })).rejects.toThrow( await expect(service.getEstConfigurationByProfile({ ...mockActor, profileId })).rejects.toThrow(
"EST configuration not found for this profile" "EST configuration not found for this profile"
); );
}); });
@@ -8,7 +8,7 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal"; import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
@@ -127,11 +127,24 @@ export const certificateProfileServiceFactory = ({
// Hash the passphrase // Hash the passphrase
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS); const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
let encryptedCaChainBuffer: Buffer;
try {
if (!data.estConfig.encryptedCaChain || typeof data.estConfig.encryptedCaChain !== "string") {
throw new BadRequestError({ message: "Invalid or missing CA chain data" });
}
encryptedCaChainBuffer = Buffer.from(data.estConfig.encryptedCaChain, "base64");
if (encryptedCaChainBuffer.toString("base64") !== data.estConfig.encryptedCaChain) {
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
}
} catch (error) {
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
}
const estConfig = await estEnrollmentConfigDAL.create( const estConfig = await estEnrollmentConfigDAL.create(
{ {
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation, disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation,
hashedPassphrase, hashedPassphrase,
encryptedCaChain: Buffer.from(data.estConfig.encryptedCaChain, "base64") encryptedCaChain: encryptedCaChainBuffer
}, },
tx tx
); );
@@ -233,9 +246,21 @@ export const certificateProfileServiceFactory = ({
...(estConfig.passphrase && { ...(estConfig.passphrase && {
hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS) hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS)
}), }),
...(estConfig.caChain && { ...(estConfig.caChain &&
encryptedCaChain: Buffer.from(estConfig.caChain, "base64") (() => {
}) try {
if (typeof estConfig.caChain !== "string") {
throw new BadRequestError({ message: "CA chain must be a string" });
}
const buffer = Buffer.from(estConfig.caChain, "base64");
if (buffer.toString("base64") !== estConfig.caChain) {
throw new BadRequestError({ message: "Invalid Base64 encoding in CA chain data" });
}
return { encryptedCaChain: buffer };
} catch (error) {
throw new BadRequestError({ message: "Failed to decode CA chain data: Invalid Base64 format" });
}
})())
}, },
tx tx
); );
@@ -596,28 +621,54 @@ export const certificateProfileServiceFactory = ({
return metrics; return metrics;
}; };
const getEstConfigurationByProfile = async ({ profileId }: { profileId: string }) => { const getEstConfigurationByProfile = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
profileId: string;
}) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId); const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) { if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" }); throw new NotFoundError({ message: "Certificate profile not found" });
} }
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
if (profile.enrollmentType !== EnrollmentType.EST) { if (profile.enrollmentType !== EnrollmentType.EST) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: "Profile is not configured for EST enrollment" message: "Profile is not configured for EST enrollment"
}); });
} }
if (!profile.estConfigEncryptedCaChain) { if (!profile.estConfig) {
throw new NotFoundError({ message: "EST configuration not found for this profile" }); throw new NotFoundError({ message: "EST configuration not found for this profile" });
} }
return { return {
orgId: profile.projectId, orgId: profile.projectId,
isEnabled: true, isEnabled: true,
caChain: profile.estConfigEncryptedCaChain.toString("base64"), caChain: profile.estConfig.encryptedCaChain,
disableBootstrapCertValidation: profile.estConfigDisableBootstrapCaValidation, disableBootstrapCertValidation: profile.estConfig.disableBootstrapCaValidation,
hashedPassphrase: profile.estConfigHashedPassphrase hashedPassphrase: profile.estConfig.hashedPassphrase
}; };
}; };
@@ -1,23 +1,23 @@
import { import {
TCertificateProfiles, TPkiCertificateProfiles,
TCertificateProfilesInsert, TPkiCertificateProfilesInsert,
TCertificateProfilesUpdate TPkiCertificateProfilesUpdate
} from "@app/db/schemas/certificate-profiles"; } from "@app/db/schemas/pki-certificate-profiles";
export enum EnrollmentType { export enum EnrollmentType {
API = "api", API = "api",
EST = "est" EST = "est"
} }
export type TCertificateProfile = Omit<TCertificateProfiles, "enrollmentType"> & { export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType"> & {
enrollmentType: EnrollmentType; enrollmentType: EnrollmentType;
}; };
export type TCertificateProfileInsert = Omit<TCertificateProfilesInsert, "enrollmentType"> & { export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
enrollmentType: EnrollmentType; enrollmentType: EnrollmentType;
}; };
export type TCertificateProfileUpdate = Omit<TCertificateProfilesUpdate, "enrollmentType"> & { export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
enrollmentType?: EnrollmentType; enrollmentType?: EnrollmentType;
estConfig?: { estConfig?: {
disableBootstrapCaValidation?: boolean; disableBootstrapCaValidation?: boolean;
@@ -47,7 +47,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
id: string; id: string;
disableBootstrapCaValidation: boolean; disableBootstrapCaValidation: boolean;
hashedPassphrase: string; hashedPassphrase: string;
encryptedCaChain: Buffer; encryptedCaChain: string;
}; };
apiConfig?: { apiConfig?: {
id: string; id: string;
@@ -73,7 +73,7 @@ export interface TCertificateProfileCertificate {
status: string; status: string;
notBefore: Date; notBefore: Date;
notAfter: Date; notAfter: Date;
revokedAt: Date | null | undefined; revokedAt: Date | null;
createdAt: Date; createdAt: Date;
} }
@@ -2,7 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2"; import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
@@ -19,7 +19,7 @@ interface CountResult {
} }
export const certificateTemplateV2DALFactory = (db: TDbClient) => { export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const certificateTemplateV2Orm = ormify(db, TableName.CertificateTemplateV2); const certificateTemplateV2Orm = ormify(db, TableName.PkiCertificateTemplateV2);
const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => { const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => {
const serialized = { ...data } as Record<string, unknown>; const serialized = { ...data } as Record<string, unknown>;
@@ -43,7 +43,17 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
jsonFields.forEach((field) => { jsonFields.forEach((field) => {
const value = raw[field]; const value = raw[field];
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
parsed[field] = typeof value === "string" ? JSON.parse(value) : value; if (typeof value === "string") {
try {
parsed[field] = JSON.parse(value);
} catch (error) {
throw new Error(
`Invalid JSON in field '${field}': ${error instanceof Error ? error.message : "Parse error"}`
);
}
} else {
parsed[field] = value;
}
} else { } else {
parsed[field] = undefined; parsed[field] = undefined;
} }
@@ -55,9 +65,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const create = async (data: TCertificateTemplateV2Insert, tx?: Knex) => { const create = async (data: TCertificateTemplateV2Insert, tx?: Knex) => {
try { try {
const serializedData = serializeJsonFields(data); const serializedData = serializeJsonFields(data);
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.insert(serializedData as TCertificateTemplatesV2Insert) .insert(serializedData as TPkiCertificateTemplatesV2Insert)
.returning("*"); .returning("*")) as Record<string, unknown>[];
if (!certificateTemplateV2) { if (!certificateTemplateV2) {
throw new Error("Failed to create certificate template v2"); throw new Error("Failed to create certificate template v2");
@@ -72,10 +82,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const updateById = async (id: string, data: TCertificateTemplateV2Update, tx?: Knex) => { const updateById = async (id: string, data: TCertificateTemplateV2Update, tx?: Knex) => {
try { try {
const serializedData = serializeJsonFields(data); const serializedData = serializeJsonFields(data);
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ id }) .where({ id })
.update(serializedData) .update(serializedData)
.returning("*"); .returning("*")) as Record<string, unknown>[];
if (!certificateTemplateV2) { if (!certificateTemplateV2) {
return null; return null;
@@ -89,10 +99,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const deleteById = async (id: string, tx?: Knex) => { const deleteById = async (id: string, tx?: Knex) => {
try { try {
const [certificateTemplateV2] = await (tx || db)(TableName.CertificateTemplateV2) const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ id }) .where({ id })
.del() .del()
.returning("*"); .returning("*")) as Record<string, unknown>[];
return certificateTemplateV2; return certificateTemplateV2;
} catch (error) { } catch (error) {
@@ -102,7 +112,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex) => {
try { try {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2).where({ id }).first(); const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2).where({ id }).first()) as
| Record<string, unknown>
| undefined;
if (!certificateTemplateV2) { if (!certificateTemplateV2) {
return null; return null;
@@ -126,7 +138,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
try { try {
const { offset = 0, limit = 20, search } = options; const { offset = 0, limit = 20, search } = options;
let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId }); let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId });
if (search) { if (search) {
query = query.where((builder) => { query = query.where((builder) => {
@@ -152,7 +164,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
try { try {
const { search } = options; const { search } = options;
let query = (tx || db)(TableName.CertificateTemplateV2).where({ projectId }); let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId });
if (search) { if (search) {
query = query.where((builder) => { query = query.where((builder) => {
@@ -169,9 +181,9 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => { const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
try { try {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2) const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ name, projectId }) .where({ name, projectId })
.first(); .first()) as Record<string, unknown> | undefined;
if (!certificateTemplateV2) { if (!certificateTemplateV2) {
return null; return null;
@@ -185,7 +197,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const isTemplateInUse = async (templateId: string, tx?: Knex) => { const isTemplateInUse = async (templateId: string, tx?: Knex) => {
try { try {
const profileCount = await (tx || db)(TableName.CertificateProfile) const profileCount = await (tx || db)(TableName.PkiCertificateProfile)
.where({ certificateTemplateId: templateId }) .where({ certificateTemplateId: templateId })
.count("*") .count("*")
.first(); .first();
@@ -207,11 +219,11 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const getProfilesUsingTemplate = async (templateId: string, tx?: Knex) => { const getProfilesUsingTemplate = async (templateId: string, tx?: Knex) => {
try { try {
const profiles = await (tx || db)(TableName.CertificateProfile) const profiles = await (tx || db)(TableName.PkiCertificateProfile)
.select("id", "slug", "description") .select("id", "slug", "description")
.where({ certificateTemplateId: templateId }); .where({ certificateTemplateId: templateId });
return profiles; return profiles as Array<{ id: string; slug: string; description?: string }>;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Get profiles using certificate template v2" }); throw new DatabaseError({ error, name: "Get profiles using certificate template v2" });
} }
@@ -100,7 +100,7 @@ const templateV2AlgorithmsSchema = z.object({
export const certificateTemplateV2ResponseSchema = z.object({ export const certificateTemplateV2ResponseSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
projectId: z.string(), projectId: z.string().uuid("Project ID must be valid"),
name: z.string(), name: z.string(),
description: z.string().nullable().optional(), description: z.string().nullable().optional(),
subject: z.array(templateV2SubjectSchema).optional(), subject: z.array(templateV2SubjectSchema).optional(),
@@ -116,7 +116,6 @@ export const certificateTemplateV2ResponseSchema = z.object({
export const certificateRequestSchema = z.object({ export const certificateRequestSchema = z.object({
commonName: z.string().optional(), commonName: z.string().optional(),
organization: z.string().optional(), organization: z.string().optional(),
organizationName: z.string().optional(),
country: z.string().optional(), country: z.string().optional(),
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(), keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(), extendedKeyUsages: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
@@ -581,18 +581,20 @@ describe("CertificateTemplateV2Service", () => {
{ ttl: "invalid", shouldThrow: true } { ttl: "invalid", shouldThrow: true }
]; ];
for (const testCase of testCases) { await Promise.all(
const request = { ...validRequest, validity: { ttl: testCase.ttl } }; testCases.map(async (testCase) => {
const request = { ...validRequest, validity: { ttl: testCase.ttl } };
if (testCase.shouldThrow) { if (testCase.shouldThrow) {
await expect(service.validateCertificateRequest("template-123", request)).rejects.toThrow( await expect(service.validateCertificateRequest("template-123", request)).rejects.toThrow(
`Invalid TTL format: ${testCase.ttl}` `Invalid TTL format: ${testCase.ttl}`
); );
} else { } else {
const result = await service.validateCertificateRequest("template-123", request); const result = await service.validateCertificateRequest("template-123", request);
expect(result.isValid).toBe(testCase.shouldBeValid); expect(result.isValid).toBe(testCase.shouldBeValid);
} }
} })
);
}); });
it("should allow optional key usages and extended key usages", async () => { it("should allow optional key usages and extended key usages", async () => {
@@ -1108,21 +1110,23 @@ describe("CertificateTemplateV2Service", () => {
{ ttl: "1y", shouldBeValid: true, description: "exactly 365 days in years" } { ttl: "1y", shouldBeValid: true, description: "exactly 365 days in years" }
]; ];
for (const testCase of testCases) { await Promise.all(
const request = { testCases.map(async (testCase) => {
commonName: "example.com", const request = {
keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT], commonName: "example.com",
extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH], keyUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT],
validity: { ttl: testCase.ttl } extendedKeyUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
}; validity: { ttl: testCase.ttl }
};
const result = await service.validateCertificateRequest("template-123", request); const result = await service.validateCertificateRequest("template-123", request);
expect(result.isValid).toBe(testCase.shouldBeValid); expect(result.isValid).toBe(testCase.shouldBeValid);
if (!testCase.shouldBeValid) { if (!testCase.shouldBeValid) {
expect(result.errors.length).toBeGreaterThan(0); expect(result.errors.length).toBeGreaterThan(0);
} }
} })
);
}); });
}); });
@@ -290,8 +290,8 @@ export const certificateTemplateV2ServiceFactory = ({
const subjectPolicies = template.subject; const subjectPolicies = template.subject;
const requestAttributes = new Map<string, string>(); const requestAttributes = new Map<string, string>();
if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName); if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName);
if (request.organization || request.organizationName) { if (request.organization) {
requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization || request.organizationName!); requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization);
} }
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country); if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
@@ -1,4 +1,7 @@
import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/schemas/certificate-templates-v2"; import {
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert
} from "@app/db/schemas/pki-certificate-templates-v2";
import { import {
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CertKeyUsageType, CertKeyUsageType,
@@ -38,7 +41,7 @@ export interface TTemplateV2Policy {
}; };
} }
export type TCertificateTemplateV2 = TCertificateTemplatesV2 & { export type TCertificateTemplateV2 = TPkiCertificateTemplatesV2 & {
subject?: TTemplateV2Policy["subject"]; subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"]; sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"]; keyUsages?: TTemplateV2Policy["keyUsages"];
@@ -47,7 +50,7 @@ export type TCertificateTemplateV2 = TCertificateTemplatesV2 & {
validity?: TTemplateV2Policy["validity"]; validity?: TTemplateV2Policy["validity"];
}; };
export type TCertificateTemplateV2Insert = TCertificateTemplatesV2Insert & { export type TCertificateTemplateV2Insert = TPkiCertificateTemplatesV2Insert & {
subject?: TTemplateV2Policy["subject"]; subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"]; sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"]; keyUsages?: TTemplateV2Policy["keyUsages"];
@@ -66,7 +69,6 @@ export type TCertificateTemplateV2Update = Partial<
export interface TCertificateRequest { export interface TCertificateRequest {
commonName?: string; commonName?: string;
organization?: string; organization?: string;
organizationName?: string;
organizationUnit?: string; organizationUnit?: string;
locality?: string; locality?: string;
state?: string; state?: string;
@@ -420,7 +420,7 @@ export const certificateTemplateServiceFactory = ({
}; };
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => { const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
const { certificateTemplateId } = dto; const { certificateTemplateId, isInternal } = dto;
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) { if (!certTemplate) {
@@ -429,7 +429,7 @@ export const certificateTemplateServiceFactory = ({
}); });
} }
if (!dto.isInternal) { if (!isInternal) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: dto.actor, actor: dto.actor,
actorId: dto.actorId, actorId: dto.actorId,
@@ -440,7 +440,7 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
} }
File diff suppressed because it is too large Load Diff
@@ -116,24 +116,25 @@ const validateAlgorithmCompatibility = (
throw new BadRequestError({ message: "CA key algorithm not found" }); throw new BadRequestError({ message: "CA key algorithm not found" });
} }
const compatibleAlgorithms = template.algorithms.signature.filter((sigAlg: string) => { const compatibleAlgorithms =
const parts = sigAlg.split("-"); template.algorithms?.signature?.filter((sigAlg: string) => {
const keyType = parts[parts.length - 1]; const parts = sigAlg.split("-");
const keyType = parts[parts.length - 1];
if (caKeyAlgorithm.startsWith("RSA")) { if (caKeyAlgorithm.startsWith("RSA")) {
return keyType === "RSA"; return keyType === "RSA";
} }
if (caKeyAlgorithm.startsWith("EC")) { if (caKeyAlgorithm.startsWith("EC")) {
return keyType === "ECDSA"; return keyType === "ECDSA";
} }
return false; return false;
}); }) || [];
if (compatibleAlgorithms.length === 0) { if (compatibleAlgorithms.length === 0) {
throw new BadRequestError({ throw new BadRequestError({
message: `Template signature algorithms (${template.algorithms.signature.join(", ")}) are not compatible with CA key algorithm (${caKeyAlgorithm})` message: `Template signature algorithms (${template.algorithms?.signature?.join(", ") || "none"}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
}); });
} }
}; };
@@ -54,28 +54,26 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, tx?: Knex) => { const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, tx?: Knex) => {
try { try {
const now = new Date(); const profiles = await (tx || db)(TableName.PkiCertificateProfile)
const renewalDate = new Date();
renewalDate.setDate(now.getDate() + renewalThresholdDays);
const profiles = await (tx || db)(TableName.CertificateProfile)
.join( .join(
TableName.PkiApiEnrollmentConfig, TableName.PkiApiEnrollmentConfig,
`${TableName.CertificateProfile}.apiConfigId`, `${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id` `${TableName.PkiApiEnrollmentConfig}.id`
) )
.where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true) .where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true)
.where((query) => { .where((query) => {
void query void query.where((qb) => {
.whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`) void qb
.orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays); .whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`)
.orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays);
});
}) })
.select((tx || db).ref("id").withSchema(TableName.CertificateProfile)) .select((tx || db).ref("id").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("name").withSchema(TableName.CertificateProfile)) .select((tx || db).ref("name").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("projectId").withSchema(TableName.CertificateProfile)) .select((tx || db).ref("projectId").withSchema(TableName.PkiCertificateProfile))
.select((tx || db).ref("autoRenewDays").withSchema(TableName.CertificateProfile)); .select((tx || db).ref("autoRenewDays").withSchema(TableName.PkiCertificateProfile));
return profiles; return profiles as Array<{ id: string; name: string; projectId: string; autoRenewDays?: number }>;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find profiles for auto renewal" }); throw new DatabaseError({ error, name: "Find profiles for auto renewal" });
} }
@@ -83,9 +81,9 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
const isConfigInUse = async (configId: string, tx?: Knex) => { const isConfigInUse = async (configId: string, tx?: Knex) => {
try { try {
const doc = await (tx || db)(TableName.CertificateProfile).where({ apiConfigId: configId }).count("*").first(); const doc = await (tx || db)(TableName.PkiCertificateProfile).where({ apiConfigId: configId }).count("*").first();
return parseInt(doc || "0", 10); return parseInt((doc as { count?: string })?.count || "0", 10);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Check if API enrollment config is in use" }); throw new DatabaseError({ error, name: "Check if API enrollment config is in use" });
} }
@@ -14,7 +14,12 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => { const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => {
try { try {
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*"); const result = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*");
const [estConfig] = result;
if (!estConfig) {
throw new Error("Failed to create EST enrollment config");
}
return estConfig; return estConfig;
} catch (error) { } catch (error) {
@@ -24,7 +29,12 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => { const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => {
try { try {
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*"); const result = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*");
const [estConfig] = result;
if (!estConfig) {
return null;
}
return estConfig; return estConfig;
} catch (error) { } catch (error) {
@@ -36,7 +46,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
try { try {
const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first(); const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first();
return estConfig; return estConfig || null;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find EST enrollment config by id" }); throw new DatabaseError({ error, name: "Find EST enrollment config by id" });
} }
+1 -1
View File
@@ -235,7 +235,7 @@ export type TOrderCertificateResponse = {
url: string; url: string;
token: string; token: string;
validated?: string; validated?: string;
error?: any; error?: string | Error;
}>; }>;
}>; }>;
certificate?: string; certificate?: string;
@@ -30,7 +30,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
id: string; id: string;
disableBootstrapCaValidation: boolean; disableBootstrapCaValidation: boolean;
hashedPassphrase: string; hashedPassphrase: string;
encryptedCaChain: any; encryptedCaChain: string;
}; };
apiConfig?: { apiConfig?: {
id: string; id: string;
@@ -108,10 +108,10 @@ export type TProfileCertificate = {
serialNumber: string; serialNumber: string;
cn: string; cn: string;
status: string; status: string;
notBefore: Date; notBefore: string;
notAfter: Date; notAfter: string;
isRevoked: boolean; isRevoked: boolean;
createdAt: Date; createdAt: string;
}; };
export type TGetProfileCertificatesDTO = { export type TGetProfileCertificatesDTO = {
@@ -146,8 +146,10 @@ export type TCertificateTemplateV2Policy = {
denied?: string[]; denied?: string[];
}; };
algorithms?: { algorithms?: {
signature?: string[]; signature?: Array<
keyAlgorithm?: string[]; "SHA256-RSA" | "SHA384-RSA" | "SHA512-RSA" | "SHA256-ECDSA" | "SHA384-ECDSA" | "SHA512-ECDSA"
>;
keyAlgorithm?: Array<"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384">;
}; };
validity?: { validity?: {
max?: string; max?: string;
@@ -109,7 +109,7 @@ export const PkiManagerLayout = () => {
<div className="w-6"> <div className="w-6">
<FontAwesomeIcon icon={faStamp} /> <FontAwesomeIcon icon={faStamp} />
</div> </div>
Certificates Authorities Certificate Authorities
</div> </div>
</MenuItem> </MenuItem>
)} )}
@@ -60,12 +60,7 @@ const schema = z
commonName: z.string(), commonName: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }), notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string(), maxPathLength: z.string(),
keyAlgorithm: z.enum([ keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
}) })
.required() .required()
}) })
@@ -145,13 +140,11 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
maxPathLength: ca.configuration.maxPathLength maxPathLength: ca.configuration.maxPathLength
? String(ca.configuration.maxPathLength) ? String(ca.configuration.maxPathLength)
: "", : "",
keyAlgorithm: keyAlgorithm: (Object.values(CertKeyAlgorithm) as string[]).includes(
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_2048 || ca.configuration.keyAlgorithm
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_4096 || )
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P256 || ? ca.configuration.keyAlgorithm
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 : CertKeyAlgorithm.RSA_2048
? ca.configuration.keyAlgorithm
: CertKeyAlgorithm.RSA_2048
} }
}); });
} else { } else {
@@ -112,9 +112,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]); const [allowedKeyAlgorithms, setAllowedKeyAlgorithms] = useState<string[]>([]);
const { currentProject } = useProject(); const { currentProject } = useProject();
const { data: cert } = useGetCert( const inputSerialNumber =
(popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "" (popUp?.certificateIssuance?.data as { serialNumber: string })?.serialNumber || "";
); const sanitizedSerialNumber = inputSerialNumber.replace(/[^a-fA-F0-9:]/g, "");
const { data: cert } = useGetCert(sanitizedSerialNumber);
const { data: profilesData } = useListCertificateProfiles({ const { data: profilesData } = useListCertificateProfiles({
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
@@ -263,15 +265,14 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }
useEffect(() => { useEffect(() => {
if (cert) { if (cert) {
const subjectAttrs: Array<{ type: string; value: string }> = []; const subjectAttrs: Array<{ type: "common_name"; value: string }> = [];
if (cert.commonName) subjectAttrs.push({ type: "common_name", value: cert.commonName }); if (cert.commonName)
subjectAttrs.push({ type: "common_name" as const, value: cert.commonName });
reset({ reset({
profileId: "", profileId: "",
subjectAttributes: subjectAttributes:
subjectAttrs.length > 0 subjectAttrs.length > 0 ? subjectAttrs : [{ type: "common_name" as const, value: "" }],
? (subjectAttrs as any)
: [{ type: "common_name" as const, value: "" }],
subjectAltNames: cert.subjectAltNames subjectAltNames: cert.subjectAltNames
? cert.subjectAltNames.split(",").map((name) => { ? cert.subjectAltNames.split(",").map((name) => {
const trimmed = name.trim(); const trimmed = name.trim();
@@ -2,15 +2,16 @@ import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CertificateProfilesTab } from "./components/CertificateProfilesTab"; import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab"; import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
enum TabSections { enum TabSections {
CertificateTemplatesV2 = "templates-v2", CertificateProfiles = "profiles",
CertificateProfiles = "profiles" CertificateTemplatesV2 = "templates-v2"
} }
export const PoliciesPage = () => { export const PoliciesPage = () => {
@@ -23,34 +24,53 @@ export const PoliciesPage = () => {
} }
return ( return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white"> <ProjectPermissionCan
<Helmet> I={ProjectPermissionActions.Read}
<title>{t("common.head-title", { title: "Certificate Policies" })}</title> a={ProjectPermissionSub.CertificateAuthorities}
</Helmet> >
<div className="mx-auto mb-6 w-full max-w-7xl"> {(isAllowed) => {
<PageHeader if (!isAllowed) {
scope="project" return (
title="Certificate Policies" <div className="container mx-auto flex h-full flex-col justify-center bg-bunker-800 text-white">
description="Manage certificate templates and profiles for unified certificate issuance" <div className="mx-auto mb-6 w-full max-w-7xl text-center">
/> <p>You don&apos;t have permission to access certificate policies.</p>
</div>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<TabList className="mb-6 w-full">
<div className="flex w-full border-b border-mineshaft-600">
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
</div> </div>
</TabList> );
}
<TabPanel value={TabSections.CertificateProfiles}> return (
<CertificateProfilesTab /> <div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
</TabPanel> <Helmet>
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
scope="project"
title="Certificate Policies"
description="Manage certificate templates and profiles for unified certificate issuance"
/>
<TabPanel value={TabSections.CertificateTemplatesV2}> <Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
<CertificateTemplatesV2Tab /> <TabList className="mb-6 w-full">
</TabPanel> <div className="flex w-full border-b border-mineshaft-600">
</Tabs> <Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
</div> <Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
</div> </div>
</TabList>
<TabPanel value={TabSections.CertificateProfiles}>
<CertificateProfilesTab />
</TabPanel>
<TabPanel value={TabSections.CertificateTemplatesV2}>
<CertificateTemplatesV2Tab />
</TabPanel>
</Tabs>
</div>
</div>
);
}}
</ProjectPermissionCan>
); );
}; };
@@ -60,7 +60,10 @@ export const CertificateProfilesTab = () => {
type: "success" type: "success"
}); });
} catch (error) { } catch (error) {
console.error("Failed to delete profile:", error); console.error(
`Failed to delete profile "${selectedProfile.slug}" (ID: ${selectedProfile.id}):`,
error
);
} }
}; };
@@ -78,7 +81,7 @@ export const CertificateProfilesTab = () => {
{canCreateProfile && ( {canCreateProfile && (
<Button <Button
colorSchema="primary" colorSchema="primary"
type="submit" type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateProfile} onClick={handleCreateProfile}
> >
@@ -18,6 +18,8 @@ import { useProject } from "@app/context";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries"; import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { import {
TCertificateProfileWithDetails, TCertificateProfileWithDetails,
TCreateCertificateProfileDTO,
TUpdateCertificateProfileDTO,
useCreateCertificateProfile, useCreateCertificateProfile,
useUpdateCertificateProfile useUpdateCertificateProfile
} from "@app/hooks/api/certificateProfiles"; } from "@app/hooks/api/certificateProfiles";
@@ -163,7 +165,7 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
if (!currentProject?.id && !isEdit) return; if (!currentProject?.id && !isEdit) return;
if (isEdit) { if (isEdit) {
const updateData: any = { const updateData: TUpdateCertificateProfileDTO = {
profileId: profile.id, profileId: profile.id,
slug: data.slug, slug: data.slug,
description: data.description description: data.description
@@ -177,8 +179,12 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
await updateProfile.mutateAsync(updateData); await updateProfile.mutateAsync(updateData);
} else { } else {
const createData: any = { if (!currentProject?.id) {
projectId: currentProject!.id, throw new Error("Project ID is required for creating a profile");
}
const createData: TCreateCertificateProfileDTO = {
projectId: currentProject.id,
slug: data.slug, slug: data.slug,
description: data.description, description: data.description,
enrollmentType: data.enrollmentType, enrollmentType: data.enrollmentType,
@@ -273,9 +279,11 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
position="popper" position="popper"
isDisabled={Boolean(isEdit)} isDisabled={Boolean(isEdit)}
> >
{certificateAuthorities.map((ca: any) => ( {certificateAuthorities.map((ca) => (
<SelectItem key={ca.id} value={ca.id}> <SelectItem key={ca.id} value={ca.id}>
{ca.friendlyName || ca.name || ca.commonName} {ca.type === "internal" && ca.configuration.friendlyName
? ca.configuration.friendlyName
: ca.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -481,7 +489,20 @@ export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }
max="365" max="365"
className="w-full" className="w-full"
isDisabled={!watchedAutoRenew} isDisabled={!watchedAutoRenew}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 30)} onChange={(e) => {
const { value } = e.target;
if (value === "") {
field.onChange("");
} else {
const parsed = parseInt(value, 10);
if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 365) {
field.onChange(parsed);
} else {
// Preserve the original field value instead of defaulting to 30
field.onChange(field.value || "");
}
}
}}
/> />
</FormControl> </FormControl>
)} )}
@@ -34,6 +34,32 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
const profiles = data?.certificateProfiles || []; const profiles = data?.certificateProfiles || [];
if (!currentProject?.id) {
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Enrollment Type</Th>
<Th>Issuing CA</Th>
<Th>Certificate Template</Th>
<Th>Certificates</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
<Tr>
<Td colSpan={6}>
<EmptyState title="No Project Selected" />
</Td>
</Tr>
</TBody>
</Table>
</TableContainer>
);
}
return ( return (
<TableContainer> <TableContainer>
<Table> <Table>
@@ -62,7 +62,7 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isIdCopied]); }, [isIdCopied, setIsIdCopied]);
const { data: templateData } = useGetCertificateTemplateV2ById({ const { data: templateData } = useGetCertificateTemplateV2ById({
templateId: profile.certificateTemplateId templateId: profile.certificateTemplateId
@@ -87,9 +87,12 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
const config = { const config = {
api: { variant: "success" as const, label: "API" }, api: { variant: "success" as const, label: "API" },
est: { variant: "primary" as const, label: "EST" } est: { variant: "primary" as const, label: "EST" }
}; } as const;
const { variant, label } = config[enrollmentType as keyof typeof config] || config.api; const configKey = Object.keys(config).includes(enrollmentType)
? (enrollmentType as keyof typeof config)
: "api";
const { variant, label } = config[configKey];
return <Badge variant={variant}>{label}</Badge>; return <Badge variant={variant}>{label}</Badge>;
}; };
@@ -57,8 +57,12 @@ export const CertificateTemplatesV2Tab = () => {
text: `Certificate template "${selectedTemplate.name}" deleted successfully`, text: `Certificate template "${selectedTemplate.name}" deleted successfully`,
type: "success" type: "success"
}); });
} catch (error: any) { } catch (error) {
console.error("Failed to delete template:", error); console.error("Failed to delete template:", error);
createNotification({
text: "Failed to delete certificate template",
type: "error"
});
} }
}; };
@@ -76,7 +80,7 @@ export const CertificateTemplatesV2Tab = () => {
{canCreateTemplate && ( {canCreateTemplate && (
<Button <Button
colorSchema="primary" colorSchema="primary"
type="submit" type="button"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={handleCreateTemplate} onClick={handleCreateTemplate}
> >
@@ -27,12 +27,17 @@ import {
useCreateCertificateTemplateV2New, useCreateCertificateTemplateV2New,
useUpdateCertificateTemplateV2New useUpdateCertificateTemplateV2New
} from "@app/hooks/api/certificateTemplates/mutations"; } from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types"; import {
TCertificateTemplateV2New,
TCertificateTemplateV2Policy
} from "@app/hooks/api/certificateTemplates/types";
import { import {
CertDurationUnit, CertDurationUnit,
CertExtendedKeyUsageType, CertExtendedKeyUsageType,
CertKeyUsageType, CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType,
SAN_INCLUDE_OPTIONS, SAN_INCLUDE_OPTIONS,
SAN_TYPE_OPTIONS, SAN_TYPE_OPTIONS,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS, SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
@@ -42,6 +47,13 @@ import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
export type FormData = TemplateFormData; export type FormData = TemplateFormData;
type AttributeTransform = NonNullable<TCertificateTemplateV2Policy["subject"]>[0];
type SanTransform = NonNullable<TCertificateTemplateV2Policy["sans"]>[0];
type KeyUsagesTransform = TCertificateTemplateV2Policy["keyUsages"];
type ExtendedKeyUsagesTransform = TCertificateTemplateV2Policy["extendedKeyUsages"];
type AlgorithmsTransform = TCertificateTemplateV2Policy["algorithms"];
type ValidityTransform = TCertificateTemplateV2Policy["validity"];
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
@@ -100,13 +112,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const isEdit = mode === "edit" && template; const isEdit = mode === "edit" && template;
const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => { const convertApiToUiFormat = (templateData: TCertificateTemplateV2New): FormData => {
const attributes: any[] = []; const attributes: FormData["attributes"] = [];
if (templateData.subject && Array.isArray(templateData.subject)) { if (templateData.subject && Array.isArray(templateData.subject)) {
templateData.subject.forEach((subj) => { templateData.subject.forEach((subj) => {
if (subj.allowed && Array.isArray(subj.allowed)) { if (subj.allowed && Array.isArray(subj.allowed)) {
subj.allowed.forEach((allowedValue) => { subj.allowed.forEach((allowedValue) => {
attributes.push({ attributes.push({
type: subj.type, type: subj.type as CertSubjectAttributeType,
include: "optional", include: "optional",
value: [allowedValue] value: [allowedValue]
}); });
@@ -115,7 +127,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (subj.denied && Array.isArray(subj.denied)) { if (subj.denied && Array.isArray(subj.denied)) {
subj.denied.forEach((deniedValue) => { subj.denied.forEach((deniedValue) => {
attributes.push({ attributes.push({
type: subj.type, type: subj.type as CertSubjectAttributeType,
include: "prohibit", include: "prohibit",
value: [deniedValue] value: [deniedValue]
}); });
@@ -124,13 +136,13 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}); });
} }
const subjectAlternativeNames: any[] = []; const subjectAlternativeNames: FormData["subjectAlternativeNames"] = [];
if (templateData.sans && Array.isArray(templateData.sans)) { if (templateData.sans && Array.isArray(templateData.sans)) {
templateData.sans.forEach((san) => { templateData.sans.forEach((san) => {
if (san.required && Array.isArray(san.required)) { if (san.required && Array.isArray(san.required)) {
san.required.forEach((requiredValue) => { san.required.forEach((requiredValue) => {
subjectAlternativeNames.push({ subjectAlternativeNames.push({
type: san.type, type: san.type as CertSubjectAlternativeNameType,
include: "mandatory", include: "mandatory",
value: [requiredValue] value: [requiredValue]
}); });
@@ -139,7 +151,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (san.allowed && Array.isArray(san.allowed)) { if (san.allowed && Array.isArray(san.allowed)) {
san.allowed.forEach((allowedValue) => { san.allowed.forEach((allowedValue) => {
subjectAlternativeNames.push({ subjectAlternativeNames.push({
type: san.type, type: san.type as CertSubjectAlternativeNameType,
include: "optional", include: "optional",
value: [allowedValue] value: [allowedValue]
}); });
@@ -148,7 +160,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
if (san.denied && Array.isArray(san.denied)) { if (san.denied && Array.isArray(san.denied)) {
san.denied.forEach((deniedValue) => { san.denied.forEach((deniedValue) => {
subjectAlternativeNames.push({ subjectAlternativeNames.push({
type: san.type, type: san.type as CertSubjectAlternativeNameType,
include: "prohibit", include: "prohibit",
value: [deniedValue] value: [deniedValue]
}); });
@@ -171,23 +183,30 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const validity = templateData.validity?.max const validity = templateData.validity?.max
? (() => { ? (() => {
const maxValue = templateData.validity.max; const maxValue = templateData.validity.max;
const match = maxValue.match(/^(\d+)([dmy])$/); if (maxValue.length < 2) return undefined;
if (match) {
const value = parseInt(match[1], 10); const lastChar = maxValue.slice(-1);
const unitChar = match[2]; const numberPart = maxValue.slice(0, -1);
let unit: CertDurationUnit = CertDurationUnit.DAYS; const value = parseInt(numberPart, 10);
if (unitChar === "d") {
unit = CertDurationUnit.DAYS; if (Number.isNaN(value) || value <= 0 || numberPart !== value.toString()) {
} else if (unitChar === "m") { return undefined;
unit = CertDurationUnit.MONTHS;
} else {
unit = CertDurationUnit.YEARS;
}
return {
maxDuration: { value, unit }
};
} }
return { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
let unit: CertDurationUnit = CertDurationUnit.DAYS;
if (lastChar === "d") {
unit = CertDurationUnit.DAYS;
} else if (lastChar === "m") {
unit = CertDurationUnit.MONTHS;
} else if (lastChar === "y") {
unit = CertDurationUnit.YEARS;
} else {
return undefined;
}
return {
maxDuration: { value, unit }
};
})() })()
: { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } }; : { maxDuration: { value: 365, unit: CertDurationUnit.DAYS } };
@@ -202,7 +221,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}; };
return { return {
slug: templateData.name || "", name: templateData.name || "",
description: templateData.description || "", description: templateData.description || "",
attributes, attributes,
subjectAlternativeNames, subjectAlternativeNames,
@@ -215,7 +234,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}; };
const getDefaultValues = (): FormData => ({ const getDefaultValues = (): FormData => ({
slug: "", name: "",
description: "", description: "",
attributes: [], attributes: [],
keyUsages: { requiredUsages: [], optionalUsages: [] }, keyUsages: { requiredUsages: [], optionalUsages: [] },
@@ -259,7 +278,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const transformToNewApiFormat = (data: FormData) => { const transformToNewApiFormat = (data: FormData) => {
const subject = const subject =
data.attributes?.map((attr) => { data.attributes?.map((attr) => {
const result: any = { type: attr.type }; const result: AttributeTransform = { type: attr.type };
if (attr.include === "optional" && attr.value && attr.value.length > 0) { if (attr.include === "optional" && attr.value && attr.value.length > 0) {
result.allowed = attr.value; result.allowed = attr.value;
@@ -272,7 +291,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
const sans = const sans =
data.subjectAlternativeNames?.map((san) => { data.subjectAlternativeNames?.map((san) => {
const result: any = { type: san.type }; const result: SanTransform = { type: san.type };
if (san.include === "mandatory" && san.value && san.value.length > 0) { if (san.include === "mandatory" && san.value && san.value.length > 0) {
result.required = san.value; result.required = san.value;
@@ -285,7 +304,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
return result; return result;
}) || []; }) || [];
const keyUsages: any = {}; const keyUsages: KeyUsagesTransform = {};
if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) { if (data.keyUsages?.requiredUsages && data.keyUsages.requiredUsages.length > 0) {
keyUsages.required = data.keyUsages.requiredUsages; keyUsages.required = data.keyUsages.requiredUsages;
} }
@@ -293,7 +312,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
keyUsages.allowed = data.keyUsages.optionalUsages; keyUsages.allowed = data.keyUsages.optionalUsages;
} }
const extendedKeyUsages: any = {}; const extendedKeyUsages: ExtendedKeyUsagesTransform = {};
if ( if (
data.extendedKeyUsages?.requiredUsages && data.extendedKeyUsages?.requiredUsages &&
data.extendedKeyUsages.requiredUsages.length > 0 data.extendedKeyUsages.requiredUsages.length > 0
@@ -307,18 +326,27 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
extendedKeyUsages.allowed = data.extendedKeyUsages.optionalUsages; extendedKeyUsages.allowed = data.extendedKeyUsages.optionalUsages;
} }
const algorithms: any = {}; const algorithms: AlgorithmsTransform = {};
if ( if (
data.signatureAlgorithm?.allowedAlgorithms && data.signatureAlgorithm?.allowedAlgorithms &&
data.signatureAlgorithm.allowedAlgorithms.length > 0 data.signatureAlgorithm.allowedAlgorithms.length > 0
) { ) {
algorithms.signature = data.signatureAlgorithm.allowedAlgorithms; algorithms.signature = data.signatureAlgorithm.allowedAlgorithms as Array<
| "SHA256-RSA"
| "SHA384-RSA"
| "SHA512-RSA"
| "SHA256-ECDSA"
| "SHA384-ECDSA"
| "SHA512-ECDSA"
>;
} }
if (data.keyAlgorithm?.allowedKeyTypes && data.keyAlgorithm.allowedKeyTypes.length > 0) { if (data.keyAlgorithm?.allowedKeyTypes && data.keyAlgorithm.allowedKeyTypes.length > 0) {
algorithms.keyAlgorithm = data.keyAlgorithm.allowedKeyTypes; algorithms.keyAlgorithm = data.keyAlgorithm.allowedKeyTypes as Array<
"RSA-2048" | "RSA-3072" | "RSA-4096" | "ECDSA-P256" | "ECDSA-P384"
>;
} }
const validity: any = {}; const validity: ValidityTransform = {};
if (data.validity?.maxDuration) { if (data.validity?.maxDuration) {
let unit = "d"; let unit = "d";
if (data.validity.maxDuration.unit === CertDurationUnit.DAYS) { if (data.validity.maxDuration.unit === CertDurationUnit.DAYS) {
@@ -332,7 +360,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
} }
return { return {
name: data.slug, name: data.name,
description: data.description, description: data.description,
subject: subject.length > 0 ? subject : undefined, subject: subject.length > 0 ? subject : undefined,
sans: sans.length > 0 ? sans : undefined, sans: sans.length > 0 ? sans : undefined,
@@ -372,8 +400,12 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}; };
await updateTemplate.mutateAsync(updateData); await updateTemplate.mutateAsync(updateData);
} else { } else {
if (!currentProject?.id) {
throw new Error("Project ID is required for creating a template");
}
const createData = { const createData = {
projectId: currentProject!.id, projectId: currentProject.id,
...transformedData ...transformedData
}; };
await createTemplate.mutateAsync(createData); await createTemplate.mutateAsync(createData);
@@ -424,22 +456,22 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
}; };
const handleKeyUsagesChange = (usages: { const handleKeyUsagesChange = (usages: {
requiredUsages: string[]; requiredUsages: CertKeyUsageType[];
optionalUsages: string[]; optionalUsages: CertKeyUsageType[];
}) => { }) => {
setValue("keyUsages", { setValue("keyUsages", {
requiredUsages: usages.requiredUsages as any, requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages as any optionalUsages: usages.optionalUsages
}); });
}; };
const handleExtendedKeyUsagesChange = (usages: { const handleExtendedKeyUsagesChange = (usages: {
requiredUsages: string[]; requiredUsages: CertExtendedKeyUsageType[];
optionalUsages: string[]; optionalUsages: CertExtendedKeyUsageType[];
}) => { }) => {
setValue("extendedKeyUsages", { setValue("extendedKeyUsages", {
requiredUsages: usages.requiredUsages as any, requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages as any optionalUsages: usages.optionalUsages
}); });
}; };
@@ -467,7 +499,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
<div className="space-y-4"> <div className="space-y-4">
<Controller <Controller
control={control} control={control}
name="slug" name="name"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Template Name" label="Template Name"
@@ -528,7 +560,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={attr.type} value={attr.type}
onValueChange={(value) => { onValueChange={(value) => {
const newAttributes = [...watchedAttributes]; const newAttributes = [...watchedAttributes];
newAttributes[index] = { ...attr, type: value as any }; newAttributes[index] = {
...attr,
type: value as CertSubjectAttributeType
};
setValue("attributes", newAttributes); setValue("attributes", newAttributes);
}} }}
className="w-48" className="w-48"
@@ -544,7 +579,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
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 (typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number]
};
setValue("attributes", newAttributes); setValue("attributes", newAttributes);
}} }}
className="w-32" className="w-32"
@@ -627,7 +666,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value={san.type} value={san.type}
onValueChange={(value) => { onValueChange={(value) => {
const newSans = [...watchedSans]; const newSans = [...watchedSans];
newSans[index] = { ...san, type: value as any }; newSans[index] = {
...san,
type: value as CertSubjectAlternativeNameType
};
setValue("subjectAlternativeNames", newSans); setValue("subjectAlternativeNames", newSans);
}} }}
className="w-24" className="w-24"
@@ -643,7 +685,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
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 (typeof SAN_INCLUDE_OPTIONS)[number]
};
setValue("subjectAlternativeNames", newSans); setValue("subjectAlternativeNames", newSans);
}} }}
className="w-32" className="w-32"
@@ -674,14 +719,16 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
required required
/> />
<IconButton {watchedSans.length > 1 && (
ariaLabel="Remove SAN" <IconButton
variant="plain" ariaLabel="Remove SAN"
size="sm" variant="plain"
onClick={() => removeSan(index)} size="sm"
> onClick={() => removeSan(index)}
<FontAwesomeIcon icon={faTrash} /> >
</IconButton> <FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</div> </div>
)) ))
)} )}
@@ -19,7 +19,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context"; import { useProject, useProjectPermission } from "@app/context";
import { import {
ProjectPermissionActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries"; import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
@@ -43,19 +43,21 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
const templates = data?.certificateTemplates || []; const templates = data?.certificateTemplates || [];
const canEditTemplate = permission.can( const canEditTemplate = permission.can(
ProjectPermissionActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateTemplates
); );
const canDeleteTemplate = permission.can( const canDeleteTemplate = permission.can(
ProjectPermissionActions.Delete, ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateTemplates
); );
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString(); return new Date(dateString).toLocaleDateString();
}; };
const hasTemplates = !isLoading && templates && templates.length > 0;
return ( return (
<TableContainer> <TableContainer>
<Table> <Table>
@@ -75,9 +77,7 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
</Td> </Td>
</Tr> </Tr>
)} )}
{!isLoading && {hasTemplates &&
templates &&
templates.length > 0 &&
templates.map((template) => ( templates.map((template) => (
<Tr <Tr
key={template.id} key={template.id}
@@ -166,42 +166,63 @@ export const USAGE_STATES = {
export type UsageState = (typeof USAGE_STATES)[keyof typeof USAGE_STATES] | undefined; export type UsageState = (typeof USAGE_STATES)[keyof typeof USAGE_STATES] | undefined;
export const TEMPLATE_SIGNATURE_ALGORITHMS = [ export enum CertKeyAlgorithm {
"SHA256-RSA", RSA_2048 = "RSA_2048",
"SHA384-RSA", RSA_3072 = "RSA_3072",
"SHA512-RSA", RSA_4096 = "RSA_4096",
"SHA256-ECDSA", ECDSA_P256 = "EC_prime256v1",
"SHA384-ECDSA", ECDSA_P384 = "EC_secp384r1"
"SHA512-ECDSA" }
] as const;
export const TEMPLATE_KEY_ALGORITHMS = [ export enum CertSignatureAlgorithm {
"RSA-2048", RSA_SHA256 = "RSA-SHA256",
"RSA-3072", RSA_SHA384 = "RSA-SHA384",
"RSA-4096", RSA_SHA512 = "RSA-SHA512",
"ECDSA-P256", ECDSA_SHA256 = "ECDSA-SHA256",
"ECDSA-P384" ECDSA_SHA384 = "ECDSA-SHA384",
] as const; ECDSA_SHA512 = "ECDSA-SHA512"
}
// API format algorithm constants export const SIGNATURE_ALGORITHM_OPTIONS = Object.values(CertSignatureAlgorithm);
export const API_SIGNATURE_ALGORITHMS = [ export const KEY_ALGORITHM_OPTIONS = Object.values(CertKeyAlgorithm);
"RSA-SHA256",
"RSA-SHA384",
"RSA-SHA512",
"ECDSA-SHA256",
"ECDSA-SHA384",
"ECDSA-SHA512"
] as const;
export const API_KEY_ALGORITHMS = [ // Display name mappings for UI
"RSA_2048", export const getSignatureAlgorithmDisplayName = (algorithm: CertSignatureAlgorithm): string => {
"RSA_3072", switch (algorithm) {
"RSA_4096", case CertSignatureAlgorithm.RSA_SHA256:
"EC_prime256v1", return "RSA with SHA-256";
"EC_secp384r1" case CertSignatureAlgorithm.RSA_SHA384:
] as const; return "RSA with SHA-384";
case CertSignatureAlgorithm.RSA_SHA512:
return "RSA with SHA-512";
case CertSignatureAlgorithm.ECDSA_SHA256:
return "ECDSA with SHA-256";
case CertSignatureAlgorithm.ECDSA_SHA384:
return "ECDSA with SHA-384";
case CertSignatureAlgorithm.ECDSA_SHA512:
return "ECDSA with SHA-512";
default:
return algorithm;
}
};
export const getKeyAlgorithmDisplayName = (algorithm: CertKeyAlgorithm): string => {
switch (algorithm) {
case CertKeyAlgorithm.RSA_2048:
return "RSA 2048";
case CertKeyAlgorithm.RSA_3072:
return "RSA 3072";
case CertKeyAlgorithm.RSA_4096:
return "RSA 4096";
case CertKeyAlgorithm.ECDSA_P256:
return "ECDSA P-256";
case CertKeyAlgorithm.ECDSA_P384:
return "ECDSA P-384";
default:
return algorithm;
}
};
// Mapping functions between template and API formats
export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => { export const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = { const mapping: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256", "SHA256-RSA": "RSA-SHA256",
@@ -11,7 +11,7 @@ import {
KEY_USAGE_OPTIONS KEY_USAGE_OPTIONS
} from "./certificate-constants"; } from "./certificate-constants";
type KeyUsagePolicy = "allow" | "require" | "deny" | "none"; type KeyUsagePolicy = "allow" | "require" | "deny";
interface KeyUsagesSectionProps { interface KeyUsagesSectionProps {
watchedKeyUsages: { watchedKeyUsages: {
@@ -85,13 +85,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
}); });
}; };
const keyUsagePolicyOptions = [ const policyOptions = [
{ value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" },
{ value: "require", label: "Require" }
];
const extendedKeyUsagePolicyOptions = [
{ value: "deny", label: "Deny" }, { value: "deny", label: "Deny" },
{ value: "allow", label: "Allow" }, { value: "allow", label: "Allow" },
{ value: "require", label: "Require" } { value: "require", label: "Require" }
@@ -116,7 +110,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
} }
className="w-32" className="w-32"
> >
{keyUsagePolicyOptions.map((option) => ( {policyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>
@@ -145,7 +139,7 @@ export const KeyUsagesSection: React.FC<KeyUsagesSectionProps> = ({
} }
className="w-32" className="w-32"
> >
{extendedKeyUsagePolicyOptions.map((option) => ( {policyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
</SelectItem> </SelectItem>
@@ -50,7 +50,7 @@ export const uiKeyAlgorithmSchema = z.object({
}); });
export const templateSchema = z.object({ export const templateSchema = z.object({
slug: z.string().trim().min(1, "Template name is required"), name: z.string().trim().min(1, "Template name is required"),
description: z.string().optional(), description: z.string().optional(),
attributes: z.array(uiAttributeSchema).optional(), attributes: z.array(uiAttributeSchema).optional(),
subjectAlternativeNames: z.array(uiSanSchema).optional(), subjectAlternativeNames: z.array(uiSanSchema).optional(),
@@ -114,7 +114,17 @@ export const apiTemplateSchema = z.object({
.object({ .object({
max: z max: z
.string() .string()
.regex(/^\d+[dhmy]$/, "Must be in format like '365d', '12m', '1y', or '24h'") .refine((val) => {
if (!val) return true;
if (val.length < 2 || val.length > 10) return false;
const lastChar = val.slice(-1);
if (!["d", "h", "m", "y"].includes(lastChar)) return false;
const numberPart = val.slice(0, -1);
const num = parseInt(numberPart, 10);
return !Number.isNaN(num) && num > 0 && numberPart === num.toString();
}, "Must be in format like '365d', '12m', '1y', or '24h'")
.optional() .optional()
}) })
.optional() .optional()
@@ -8,15 +8,11 @@ import {
} from "./certificate-constants"; } from "./certificate-constants";
export const formatUsageName = (usage: string): string => { export const formatUsageName = (usage: string): string => {
try { if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) {
if (Object.values(CertKeyUsageType).includes(usage as CertKeyUsageType)) { return formatKeyUsage(usage as CertKeyUsageType);
return formatKeyUsage(usage as CertKeyUsageType); }
} if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) {
if (Object.values(CertExtendedKeyUsageType).includes(usage as CertExtendedKeyUsageType)) { return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
return formatExtendedKeyUsage(usage as CertExtendedKeyUsageType);
}
} catch {
// Handle any errors in type checking
} }
return usage.replace(/_/g, " "); return usage.replace(/_/g, " ");
}; };