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