Merge branch 'heads/main' into daniel/hsm-improvements-prep-work

This commit is contained in:
Daniel Hougaard
2025-10-21 23:19:40 +04:00
427 changed files with 26805 additions and 3823 deletions
@@ -40,7 +40,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root { name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones { name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" }, { name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing { name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => { ])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name }); const createdFolder = await createFolder({ path, name });
// check for default environments // check for default environments
@@ -57,7 +57,7 @@ describe("Secret Folder Router", async () => {
{ {
path: "/", path: "/",
expected: { expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }], folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3 length: 3
} }
}, },
@@ -162,4 +162,25 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0); expect(updatedFolderList.folders.length).toEqual(0);
}); });
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
}); });
@@ -18,7 +18,7 @@ const createFolder = async (dto: { path: string; name: string }) => {
return res.json().folder; return res.json().folder;
}; };
const deleteFolder = async (dto: { path: string; id: string }) => { const deleteFolder = async (dto: { path: string; id: string; forceDelete?: boolean }) => {
const res = await testServer.inject({ const res = await testServer.inject({
method: "DELETE", method: "DELETE",
url: `/api/v2/folders/${dto.id}`, url: `/api/v2/folders/${dto.id}`,
@@ -28,7 +28,8 @@ const deleteFolder = async (dto: { path: string; id: string }) => {
body: { body: {
projectId: seedData1.project.id, projectId: seedData1.project.id,
environment: seedData1.environment.slug, environment: seedData1.environment.slug,
path: dto.path path: dto.path,
forceDelete: dto.forceDelete ?? false
} }
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
@@ -40,7 +41,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root { name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones { name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" }, { name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing { name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => { ])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name }); const createdFolder = await createFolder({ path, name });
// check for default environments // check for default environments
@@ -57,7 +58,7 @@ describe("Secret Folder Router", async () => {
{ {
path: "/", path: "/",
expected: { expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }], folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3 length: 3
} }
}, },
@@ -86,7 +87,7 @@ describe("Secret Folder Router", async () => {
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el))) folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
}); });
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id }))); await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id, forceDelete: true })));
}); });
test("Update a deep folder", async () => { test("Update a deep folder", async () => {
@@ -162,4 +163,26 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0); expect(updatedFolderList.folders.length).toEqual(0);
}); });
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v2/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
}); });
+8
View File
@@ -61,7 +61,11 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service"; import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
@@ -262,7 +266,10 @@ declare module "fastify" {
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory; auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory; certificate: TCertificateServiceFactory;
certificateV3: TCertificateV3ServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory;
certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
certificateProfile: TCertificateProfileServiceFactory;
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory; sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
sshCertificateTemplate: TSshCertificateTemplateServiceFactory; sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
sshHost: TSshHostServiceFactory; sshHost: TSshHostServiceFactory;
@@ -270,6 +277,7 @@ declare module "fastify" {
certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory; certificateEst: TCertificateEstServiceFactory;
certificateEstV3: TCertificateEstV3ServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
pkiSubscriber: TPkiSubscriberServiceFactory; pkiSubscriber: TPkiSubscriberServiceFactory;
pkiSync: TPkiSyncServiceFactory; pkiSync: TPkiSyncServiceFactory;
+40
View File
@@ -266,12 +266,24 @@ import {
TPkiAlerts, TPkiAlerts,
TPkiAlertsInsert, TPkiAlertsInsert,
TPkiAlertsUpdate, TPkiAlertsUpdate,
TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate,
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate,
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert,
TPkiCertificateTemplatesV2Update,
TPkiCollectionItems, TPkiCollectionItems,
TPkiCollectionItemsInsert, TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate, TPkiCollectionItemsUpdate,
TPkiCollections, TPkiCollections,
TPkiCollectionsInsert, TPkiCollectionsInsert,
TPkiCollectionsUpdate, TPkiCollectionsUpdate,
TPkiEstEnrollmentConfigs,
TPkiEstEnrollmentConfigsInsert,
TPkiEstEnrollmentConfigsUpdate,
TPkiSubscribers, TPkiSubscribers,
TPkiSubscribersInsert, TPkiSubscribersInsert,
TPkiSubscribersUpdate, TPkiSubscribersUpdate,
@@ -518,6 +530,9 @@ import {
TUsers, TUsers,
TUsersInsert, TUsersInsert,
TUsersUpdate, TUsersUpdate,
TVaultExternalMigrationConfigs,
TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate,
TWebhooks, TWebhooks,
TWebhooksInsert, TWebhooksInsert,
TWebhooksUpdate, TWebhooksUpdate,
@@ -671,6 +686,26 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate TCertificateTemplatesUpdate
>; >;
[TableName.PkiCertificateTemplateV2]: KnexOriginal.CompositeTableType<
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert,
TPkiCertificateTemplatesV2Update
>;
[TableName.PkiCertificateProfile]: KnexOriginal.CompositeTableType<
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate
>;
[TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType<
TPkiEstEnrollmentConfigs,
TPkiEstEnrollmentConfigsInsert,
TPkiEstEnrollmentConfigsUpdate
>;
[TableName.PkiApiEnrollmentConfig]: KnexOriginal.CompositeTableType<
TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate
>;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs, TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert, TCertificateTemplateEstConfigsInsert,
@@ -1345,5 +1380,10 @@ declare module "knex/types/tables" {
TAdditionalPrivilegesInsert, TAdditionalPrivilegesInsert,
TAdditionalPrivilegesUpdate TAdditionalPrivilegesUpdate
>; >;
[TableName.VaultExternalMigrationConfig]: KnexOriginal.CompositeTableType<
TVaultExternalMigrationConfigs,
TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate
>;
} }
} }
@@ -0,0 +1,117 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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);
t.string("name").notNullable();
t.string("description");
t.jsonb("subject");
t.jsonb("sans");
t.jsonb("keyUsages");
t.jsonb("extendedKeyUsages");
t.jsonb("algorithms");
t.jsonb("validity");
t.timestamps(true, true, true);
t.unique(["name", "projectId"]);
});
await createOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2);
}
if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) {
await knex.schema.createTable(TableName.PkiEstEnrollmentConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.boolean("disableBootstrapCaValidation").defaultTo(false);
t.text("hashedPassphrase").notNullable();
t.binary("encryptedCaChain");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig);
}
if (!(await knex.schema.hasTable(TableName.PkiApiEnrollmentConfig))) {
await knex.schema.createTable(TableName.PkiApiEnrollmentConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.boolean("autoRenew").defaultTo(false);
t.integer("autoRenewDays");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
}
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).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.PkiCertificateTemplateV2);
t.string("slug").notNullable();
t.string("description");
t.string("enrollmentType").notNullable().checkIn(["api", "est"]);
t.uuid("estConfigId");
t.foreign("estConfigId").references("id").inTable(TableName.PkiEstEnrollmentConfig).onDelete("SET NULL");
t.uuid("apiConfigId");
t.foreign("apiConfigId").references("id").inTable(TableName.PkiApiEnrollmentConfig).onDelete("SET NULL");
t.timestamps(true, true, true);
t.unique(["slug", "projectId"]);
});
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.PkiCertificateProfile).onDelete("SET NULL");
t.index("profileId");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Certificate, "profileId")) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropForeign(["profileId"]);
t.dropIndex("profileId");
t.dropColumn("profileId");
});
}
await knex.schema.dropTableIfExists(TableName.PkiCertificateProfile);
await dropOnUpdateTrigger(knex, TableName.PkiCertificateProfile);
await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig);
await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig);
await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig);
await knex.schema.dropTableIfExists(TableName.PkiCertificateTemplateV2);
await dropOnUpdateTrigger(knex, TableName.PkiCertificateTemplateV2);
}
@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.VaultExternalMigrationConfig))) {
await knex.schema.createTable(TableName.VaultExternalMigrationConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("namespace").notNullable();
t.uuid("connectionId");
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
t.unique(["orgId", "namespace"]);
});
await createOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.VaultExternalMigrationConfig);
await dropOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig);
}
+2 -1
View File
@@ -26,7 +26,8 @@ export const CertificatesSchema = z.object({
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(),
projectId: z.string(), projectId: z.string(),
pkiSubscriberId: z.string().uuid().nullable().optional() pkiSubscriberId: z.string().uuid().nullable().optional(),
profileId: z.string().uuid().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
+5
View File
@@ -92,8 +92,12 @@ export * from "./pam-folders";
export * from "./pam-resources"; export * from "./pam-resources";
export * from "./pam-sessions"; export * from "./pam-sessions";
export * from "./pki-alerts"; export * from "./pki-alerts";
export * from "./pki-api-enrollment-configs";
export * from "./pki-certificate-profiles";
export * from "./pki-certificate-templates-v2";
export * from "./pki-collection-items"; export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./pki-est-enrollment-configs";
export * from "./pki-subscribers"; export * from "./pki-subscribers";
export * from "./pki-syncs"; export * from "./pki-syncs";
export * from "./project-bots"; export * from "./project-bots";
@@ -176,5 +180,6 @@ export * from "./user-aliases";
export * from "./user-encryption-keys"; export * from "./user-encryption-keys";
export * from "./user-group-membership"; export * from "./user-group-membership";
export * from "./users"; export * from "./users";
export * from "./vault-external-migration-configs";
export * from "./webhooks"; export * from "./webhooks";
export * from "./workflow-integrations"; export * from "./workflow-integrations";
+7 -1
View File
@@ -23,6 +23,10 @@ export enum TableName {
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates", CertificateTemplate = "certificate_templates",
PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
PkiCertificateProfile = "pki_certificate_profiles",
PkiEstEnrollmentConfig = "pki_est_enrollment_configs",
PkiApiEnrollmentConfig = "pki_api_enrollment_configs",
PkiSubscriber = "pki_subscribers", PkiSubscriber = "pki_subscribers",
PkiAlert = "pki_alerts", PkiAlert = "pki_alerts",
PkiCollection = "pki_collections", PkiCollection = "pki_collections",
@@ -203,7 +207,9 @@ export enum TableName {
PamFolder = "pam_folders", PamFolder = "pam_folders",
PamResource = "pam_resources", PamResource = "pam_resources",
PamAccount = "pam_accounts", PamAccount = "pam_accounts",
PamSession = "pam_sessions" PamSession = "pam_sessions",
VaultExternalMigrationConfig = "vault_external_migration_configs"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
@@ -0,0 +1,22 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiApiEnrollmentConfigsSchema = z.object({
id: z.string().uuid(),
autoRenew: z.boolean().default(false).nullable().optional(),
autoRenewDays: z.number().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TPkiApiEnrollmentConfigs = z.infer<typeof PkiApiEnrollmentConfigsSchema>;
export type TPkiApiEnrollmentConfigsInsert = Omit<z.input<typeof PkiApiEnrollmentConfigsSchema>, TImmutableDBKeys>;
export type TPkiApiEnrollmentConfigsUpdate = Partial<
Omit<z.input<typeof PkiApiEnrollmentConfigsSchema>, TImmutableDBKeys>
>;
@@ -0,0 +1,28 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiCertificateProfilesSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
caId: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
slug: z.string(),
description: z.string().nullable().optional(),
enrollmentType: z.string(),
estConfigId: z.string().uuid().nullable().optional(),
apiConfigId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
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>
>;
@@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiCertificateTemplatesV2Schema = z.object({
id: z.string().uuid(),
projectId: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
subject: z.unknown().nullable().optional(),
sans: z.unknown().nullable().optional(),
keyUsages: z.unknown().nullable().optional(),
extendedKeyUsages: z.unknown().nullable().optional(),
algorithms: z.unknown().nullable().optional(),
validity: z.unknown().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
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>
>;
@@ -0,0 +1,25 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const PkiEstEnrollmentConfigsSchema = z.object({
id: z.string().uuid(),
disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(),
hashedPassphrase: z.string(),
encryptedCaChain: zodBuffer.nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TPkiEstEnrollmentConfigs = z.infer<typeof PkiEstEnrollmentConfigsSchema>;
export type TPkiEstEnrollmentConfigsInsert = Omit<z.input<typeof PkiEstEnrollmentConfigsSchema>, TImmutableDBKeys>;
export type TPkiEstEnrollmentConfigsUpdate = Partial<
Omit<z.input<typeof PkiEstEnrollmentConfigsSchema>, TImmutableDBKeys>
>;
@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const VaultExternalMigrationConfigsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
namespace: z.string(),
connectionId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TVaultExternalMigrationConfigs = z.infer<typeof VaultExternalMigrationConfigsSchema>;
export type TVaultExternalMigrationConfigsInsert = Omit<
z.input<typeof VaultExternalMigrationConfigsSchema>,
TImmutableDBKeys
>;
export type TVaultExternalMigrationConfigsUpdate = Partial<
Omit<z.input<typeof VaultExternalMigrationConfigsSchema>, TImmutableDBKeys>
>;
@@ -8,6 +8,35 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => { export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => {
try {
// Try to find as profile first using internal access
await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
});
return "profile";
} catch (profileError) {
try {
await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
return "template";
} 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;
}
}
};
// add support for CSR bodies // add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => { server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try { try {
@@ -59,11 +88,29 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
return; return;
} }
const certificateTemplateId = urlFragments.slice(-2)[0]; const identifier = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true, const identifierType = await getIdentifierType(identifier);
certificateTemplateId if (!identifierType) {
res.raw.statusCode = 404;
res.raw.setHeader("Content-Type", "text/plain");
res.raw.write("Certificate template or profile not found");
res.raw.flushHeaders();
return;
}
let estConfig;
if (identifierType === "profile") {
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
}); });
} else {
estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
}
if (!estConfig.isEnabled) { if (!estConfig.isEnabled) {
throw new BadRequestError({ throw new BadRequestError({
@@ -95,14 +142,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "POST", method: "POST",
url: "/:certificateTemplateId/simpleenroll", url: "/:identifier/simpleenroll",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.string().min(1), body: z.string().min(1),
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -112,9 +159,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleEnrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleEnroll({ return server.services.certificateEst.simpleEnroll({
csr: req.body, csr: req.body,
certificateTemplateId: req.params.certificateTemplateId, certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
}); });
} }
@@ -122,14 +183,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "POST", method: "POST",
url: "/:certificateTemplateId/simplereenroll", url: "/:identifier/simplereenroll",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.string().min(1), body: z.string().min(1),
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -139,9 +200,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleReenrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleReenroll({ return server.services.certificateEst.simpleReenroll({
csr: req.body, csr: req.body,
certificateTemplateId: req.params.certificateTemplateId, certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
}); });
} }
@@ -149,13 +224,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "GET", method: "GET",
url: "/:certificateTemplateId/cacerts", url: "/:identifier/cacerts",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -165,8 +240,20 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.getCaCertsByProfile({
profileId: identifier
});
}
return server.services.certificateEst.getCaCerts({ return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId certificateTemplateId: identifier
}); });
} }
}); });
+2 -2
View File
@@ -129,7 +129,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
id: z.string(), id: z.string(),
value: z.string(), value: z.string(),
algorithm: z.string(), algorithm: z.string(),
kmipMetadata: z.record(z.any()).optional() kmipMetadata: z.record(z.any()).nullish()
}) })
} }
}, },
@@ -435,7 +435,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
key: z.string(), key: z.string(),
name: z.string(), name: z.string(),
algorithm: z.nativeEnum(SymmetricKeyAlgorithm), algorithm: z.nativeEnum(SymmetricKeyAlgorithm),
kmipMetadata: z.record(z.any()).optional() kmipMetadata: z.record(z.any()).nullish()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -352,9 +352,18 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template", GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
CREATE_CERTIFICATE_PROFILE = "create-certificate-profile",
UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile",
DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile",
GET_CERTIFICATE_PROFILE = "get-certificate-profile",
LIST_CERTIFICATE_PROFILES = "list-certificate-profiles",
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -2512,46 +2521,6 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet metadata: Record<string, string>; // no metadata yet
} }
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface OrgAdminAccessProjectEvent { interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT; type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: { metadata: {
@@ -2598,6 +2567,138 @@ interface GetCertificateTemplateEstConfig {
}; };
} }
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata:
| {
certificateTemplateId: string;
name: string;
projectId: string;
}
| {
certificateTemplateId: string;
caId: string;
pkiCollectionId: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata:
| {
certificateTemplateId: string;
name: string;
}
| {
certificateTemplateId: string;
caId: string;
pkiCollectionId: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
projectId: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
name: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
name: string;
};
}
interface ListCertificateTemplates {
type: EventType.LIST_CERTIFICATE_TEMPLATES;
metadata: {
projectId: string;
};
}
interface CreateCertificateProfile {
type: EventType.CREATE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
projectId: string;
enrollmentType: string;
};
}
interface UpdateCertificateProfile {
type: EventType.UPDATE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface DeleteCertificateProfile {
type: EventType.DELETE_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface GetCertificateProfile {
type: EventType.GET_CERTIFICATE_PROFILE;
metadata: {
certificateProfileId: string;
name: string;
};
}
interface ListCertificateProfiles {
type: EventType.LIST_CERTIFICATE_PROFILES;
metadata: {
projectId: string;
};
}
interface IssueCertificateFromProfile {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
certificateId: string;
commonName: string;
profileName: string;
};
}
interface SignCertificateFromProfile {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
certificateId: string;
profileName: string;
commonName: string;
};
}
interface OrderCertificateFromProfile {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE;
metadata: {
certificateProfileId: string;
orderId: string;
profileName: string;
};
}
interface AttemptCreateSlackIntegration { interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION; type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: { metadata: {
@@ -4051,13 +4152,22 @@ export type Event =
| LoadProjectKmsBackupEvent | LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent | OrgAdminAccessProjectEvent
| OrgAdminBypassSSOEvent | OrgAdminBypassSSOEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| CreateCertificateTemplate
| UpdateCertificateTemplate
| DeleteCertificateTemplate
| GetCertificateTemplate
| ListCertificateTemplates
| CreateCertificateProfile
| UpdateCertificateProfile
| DeleteCertificateProfile
| GetCertificateProfile
| ListCertificateProfiles
| IssueCertificateFromProfile
| SignCertificateFromProfile
| OrderCertificateFromProfile
| GetAzureAdCsTemplatesEvent | GetAzureAdCsTemplatesEvent
| AttemptCreateSlackIntegration | AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration | AttemptReinstallSlackIntegration
+1 -1
View File
@@ -78,7 +78,7 @@ export type TKmipRegisterDTO = {
name: string; name: string;
key: string; key: string;
algorithm: SymmetricKeyAlgorithm; algorithm: SymmetricKeyAlgorithm;
kmipMetadata?: Record<string, unknown>; kmipMetadata?: Record<string, unknown> | null;
} & KmipOperationBaseDTO; } & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = { export type TSetupOrgKmipDTO = {
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false, enterpriseCertificateSyncs: false,
enterpriseAppConnections: true, enterpriseAppConnections: true,
machineIdentityAuthTemplates: false machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false
}; };
}; };
@@ -67,6 +67,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
fips: false, fips: false,
eventSubscriptions: false, eventSubscriptions: false,
machineIdentityAuthTemplates: false, machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false,
pam: false pam: false
}); });
@@ -218,6 +218,20 @@ export const licenseServiceFactory = ({
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
currentPlan.identitiesUsed = identityUsed; currentPlan.identitiesUsed = identityUsed;
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
try {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity: membersUsed,
quantityIdentities: identityUsed
});
} catch (error) {
logger.error(
error,
`Update seats used: encountered an error when updating plan for customer [customerId=${org.customerId}]`
);
}
}
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(org.id), FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL, LICENSE_SERVER_CLOUD_PLAN_TTL,
@@ -78,6 +78,7 @@ export type TFeatureSet = {
enterpriseCertificateSyncs: false; enterpriseCertificateSyncs: false;
enterpriseAppConnections: false; enterpriseAppConnections: false;
machineIdentityAuthTemplates: false; machineIdentityAuthTemplates: false;
pkiLegacyTemplates: false;
fips: false; fips: false;
eventSubscriptions: false; eventSubscriptions: false;
pam: false; pam: false;
@@ -5,6 +5,7 @@ import {
ProjectPermissionAppConnectionActions, ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionCommitsActions, ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -72,8 +73,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionPkiTemplateActions.Create, ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionPkiTemplateActions.Delete, ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionPkiTemplateActions.IssueCert, ProjectPermissionPkiTemplateActions.IssueCert, // deprecated
ProjectPermissionPkiTemplateActions.ListCerts ProjectPermissionPkiTemplateActions.ListCerts // deprecated
], ],
ProjectPermissionSub.CertificateTemplates ProjectPermissionSub.CertificateTemplates
); );
@@ -99,6 +100,17 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete,
ProjectPermissionCertificateProfileActions.IssueCert
],
ProjectPermissionSub.CertificateProfiles
);
can( can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback], [ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits ProjectPermissionSub.Commits
@@ -443,6 +455,7 @@ const buildMemberPermissionRules = () => {
// double check if all CRUD are needed for CA and Certificates // double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can( can(
[ [
@@ -454,7 +467,15 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates); can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete
],
ProjectPermissionSub.CertificateProfiles
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
@@ -111,6 +111,14 @@ export enum ProjectPermissionPkiSubscriberActions {
ListCerts = "list-certs" ListCerts = "list-certs"
} }
export enum ProjectPermissionCertificateProfileActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert"
}
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -249,7 +257,8 @@ export enum ProjectPermissionSub {
PamFolders = "pam-folders", PamFolders = "pam-folders",
PamResources = "pam-resources", PamResources = "pam-resources",
PamAccounts = "pam-accounts", PamAccounts = "pam-accounts",
PamSessions = "pam-sessions" PamSessions = "pam-sessions",
CertificateProfiles = "certificate-profiles"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -438,7 +447,8 @@ export type ProjectPermissionSet =
ProjectPermissionPamAccountActions, ProjectPermissionPamAccountActions,
ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields) ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
] ]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -1109,6 +1119,13 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateProfiles).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
+97 -84
View File
@@ -57,6 +57,7 @@ export enum ApiDocsTags {
PkiCertificateAuthorities = "PKI Certificate Authorities", PkiCertificateAuthorities = "PKI Certificate Authorities",
PkiCertificates = "PKI Certificates", PkiCertificates = "PKI Certificates",
PkiCertificateTemplates = "PKI Certificate Templates", PkiCertificateTemplates = "PKI Certificate Templates",
PkiCertificateProfiles = "PKI Certificate Profiles",
PkiCertificateCollections = "PKI Certificate Collections", PkiCertificateCollections = "PKI Certificate Collections",
PkiAlerting = "PKI Alerting", PkiAlerting = "PKI Alerting",
PkiSubscribers = "PKI Subscribers", PkiSubscribers = "PKI Subscribers",
@@ -122,16 +123,16 @@ export const IDENTITIES = {
hasDeleteProtection: "Prevents deletion of the identity when enabled." hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update.", identityId: "The ID of the machine identity to update.",
name: "The new name of the identity.", name: "The new name of the identity.",
role: "The new role of the identity.", role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled." hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
DELETE: { DELETE: {
identityId: "The ID of the identity to delete." identityId: "The ID of the machine identity to delete."
}, },
GET_BY_ID: { GET_BY_ID: {
identityId: "The ID of the identity to get details.", identityId: "The ID of the machine identity to get details.",
orgId: "The ID of the org of the identity" orgId: "The ID of the org of the identity"
}, },
LIST: { LIST: {
@@ -156,7 +157,7 @@ export const UNIVERSAL_AUTH = {
clientSecret: "Your Machine Identity Client Secret." clientSecret: "Your Machine Identity Client Secret."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
clientSecretTrustedIps: clientSecretTrustedIps:
"A list of IPs or CIDR ranges that the Client Secret can be used from together with the Client ID to get back an access token. You can use 0.0.0.0/0, to allow usage from any network address.", "A list of IPs or CIDR ranges that the Client Secret can be used from together with the Client ID to get back an access token. You can use 0.0.0.0/0, to allow usage from any network address.",
accessTokenTrustedIps: accessTokenTrustedIps:
@@ -175,13 +176,13 @@ export const UNIVERSAL_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter." "How long to wait from the most recent failed login until resetting the lockout counter."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.", clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.", accessTokenTTL: "The new lifetime for an access token in seconds.",
@@ -195,25 +196,25 @@ export const UNIVERSAL_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter." "How long to wait from the most recent failed login until resetting the lockout counter."
}, },
CREATE_CLIENT_SECRET: { CREATE_CLIENT_SECRET: {
identityId: "The ID of the identity to create a client secret for.", identityId: "The ID of the machine identity to create a client secret for.",
description: "The description of the client secret.", description: "The description of the client secret.",
numUsesLimit: numUsesLimit:
"The maximum number of times that the client secret can be used; a value of 0 implies infinite number of uses.", "The maximum number of times that the client secret can be used; a value of 0 implies infinite number of uses.",
ttl: "The lifetime for the client secret in seconds." ttl: "The lifetime for the client secret in seconds."
}, },
LIST_CLIENT_SECRETS: { LIST_CLIENT_SECRETS: {
identityId: "The ID of the identity to list client secrets for." identityId: "The ID of the machine identity to list client secrets for."
}, },
GET_CLIENT_SECRET: { GET_CLIENT_SECRET: {
identityId: "The ID of the identity to get the client secret from.", identityId: "The ID of the machine identity to get the client secret from.",
clientSecretId: "The ID of the client secret to get details." clientSecretId: "The ID of the client secret to get details."
}, },
REVOKE_CLIENT_SECRET: { REVOKE_CLIENT_SECRET: {
identityId: "The ID of the identity to revoke the client secret from.", identityId: "The ID of the machine identity to revoke the client secret from.",
clientSecretId: "The ID of the client secret to revoke." clientSecretId: "The ID of the client secret to revoke."
}, },
CLEAR_CLIENT_LOCKOUTS: { CLEAR_CLIENT_LOCKOUTS: {
identityId: "The ID of the identity to clear the client lockouts from." identityId: "The ID of the machine identity to clear the client lockouts from."
}, },
RENEW_ACCESS_TOKEN: { RENEW_ACCESS_TOKEN: {
accessToken: "The access token to renew." accessToken: "The access token to renew."
@@ -225,13 +226,13 @@ export const UNIVERSAL_AUTH = {
export const LDAP_AUTH = { export const LDAP_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login.", identityId: "The ID of the machine identity to login.",
username: "The username of the LDAP user to login.", username: "The username of the LDAP user to login.",
password: "The password of the LDAP user to login." password: "The password of the LDAP user to login."
}, },
ATTACH: { ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.", templateId: "The ID of the identity auth template to attach the configuration onto.",
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
url: "The URL of the LDAP server.", url: "The URL of the LDAP server.",
allowedFields: allowedFields:
"The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.", "The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.",
@@ -251,7 +252,7 @@ export const LDAP_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter." "How long to wait from the most recent failed login until resetting the lockout counter."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the configuration for.", identityId: "The ID of the machine identity to update the configuration for.",
url: "The new URL of the LDAP server.", url: "The new URL of the LDAP server.",
allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.", allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.",
searchBase: "The new base DN to search for the LDAP user.", searchBase: "The new base DN to search for the LDAP user.",
@@ -271,19 +272,19 @@ export const LDAP_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter." "How long to wait from the most recent failed login until resetting the lockout counter."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for." identityId: "The ID of the machine identity to retrieve the configuration for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the configuration for." identityId: "The ID of the machine identity to revoke the configuration for."
}, },
CLEAR_CLIENT_LOCKOUTS: { CLEAR_CLIENT_LOCKOUTS: {
identityId: "The ID of the identity to clear the client lockouts from." identityId: "The ID of the machine identity to clear the client lockouts from."
} }
} as const; } as const;
export const ALICLOUD_AUTH = { export const ALICLOUD_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login.", identityId: "The ID of the machine identity to login.",
Action: "The Alibaba Cloud API action. For STS GetCallerIdentity, this should be 'GetCallerIdentity'.", Action: "The Alibaba Cloud API action. For STS GetCallerIdentity, this should be 'GetCallerIdentity'.",
Format: "The response format. For STS GetCallerIdentity, this should be 'JSON'.", Format: "The response format. For STS GetCallerIdentity, this should be 'JSON'.",
Version: "The API version. This should be in 'YYYY-MM-DD' format (e.g., '2015-04-01').", Version: "The API version. This should be in 'YYYY-MM-DD' format (e.g., '2015-04-01').",
@@ -295,7 +296,7 @@ export const ALICLOUD_AUTH = {
Signature: "The signature string calculated based on the request parameters and AccessKey Secret." Signature: "The signature string calculated based on the request parameters and AccessKey Secret."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.", allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -303,7 +304,7 @@ export const ALICLOUD_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.", allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.", accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
@@ -311,19 +312,19 @@ export const ALICLOUD_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const TLS_CERT_AUTH = { export const TLS_CERT_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
allowedCommonNames: allowedCommonNames:
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.", "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
caCertificate: "The PEM-encoded CA certificate to validate client certificates.", caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
@@ -333,7 +334,7 @@ export const TLS_CERT_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
allowedCommonNames: allowedCommonNames:
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.", "The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
caCertificate: "The PEM-encoded CA certificate to validate client certificates.", caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
@@ -343,16 +344,16 @@ export const TLS_CERT_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const AWS_AUTH = { export const AWS_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login.", identityId: "The ID of the machine identity to login.",
iamHttpRequestMethod: "The HTTP request method used in the signed request.", iamHttpRequestMethod: "The HTTP request method used in the signed request.",
iamRequestUrl: iamRequestUrl:
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.", "The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.",
@@ -361,7 +362,7 @@ export const AWS_AUTH = {
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
allowedPrincipalArns: allowedPrincipalArns:
"The comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.", "The comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds: allowedAccountIds:
@@ -373,7 +374,7 @@ export const AWS_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
allowedPrincipalArns: allowedPrincipalArns:
"The new comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.", "The new comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds: allowedAccountIds:
@@ -385,21 +386,21 @@ export const AWS_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const OCI_AUTH = { export const OCI_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login.", identityId: "The ID of the machine identity to login.",
userOcid: "The OCID of the user attempting login.", userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request." headers: "The headers of the signed request."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
tenancyOcid: "The OCID of your tenancy.", tenancyOcid: "The OCID of your tenancy.",
allowedUsernames: allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.", "The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
@@ -409,7 +410,7 @@ export const OCI_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
tenancyOcid: "The OCID of your tenancy.", tenancyOcid: "The OCID of your tenancy.",
allowedUsernames: allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.", "The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
@@ -419,19 +420,19 @@ export const OCI_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const AZURE_AUTH = { export const AZURE_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
tenantId: "The tenant ID for the Azure AD organization.", tenantId: "The tenant ID for the Azure AD organization.",
resource: "The resource URL for the application registered in Azure AD.", resource: "The resource URL for the application registered in Azure AD.",
allowedServicePrincipalIds: allowedServicePrincipalIds:
@@ -442,7 +443,7 @@ export const AZURE_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
tenantId: "The new tenant ID for the Azure AD organization.", tenantId: "The new tenant ID for the Azure AD organization.",
resource: "The new resource URL for the application registered in Azure AD.", resource: "The new resource URL for the application registered in Azure AD.",
allowedServicePrincipalIds: allowedServicePrincipalIds:
@@ -453,19 +454,19 @@ export const AZURE_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const GCP_AUTH = { export const GCP_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
allowedServiceAccounts: allowedServiceAccounts:
"The comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.", "The comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.",
allowedProjects: allowedProjects:
@@ -478,7 +479,7 @@ export const GCP_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
allowedServiceAccounts: allowedServiceAccounts:
"The new comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.", "The new comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.",
allowedProjects: allowedProjects:
@@ -491,19 +492,19 @@ export const GCP_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const KUBERNETES_AUTH = { export const KUBERNETES_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.", kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The PEM-encoded CA cert for the Kubernetes API server.", caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt: tokenReviewerJwt:
@@ -522,7 +523,7 @@ export const KUBERNETES_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.", kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.", caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt: tokenReviewerJwt:
@@ -541,41 +542,41 @@ export const KUBERNETES_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const TOKEN_AUTH = { export const TOKEN_AUTH = {
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.", accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
}, },
GET_TOKENS: { GET_TOKENS: {
identityId: "The ID of the identity to list token metadata for.", identityId: "The ID of the machine identity to list token metadata for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th token.", offset: "The offset to start from. If you enter 10, it will start from the 10th token.",
limit: "The number of tokens to return." limit: "The number of tokens to return."
}, },
CREATE_TOKEN: { CREATE_TOKEN: {
identityId: "The ID of the identity to create the token for.", identityId: "The ID of the machine identity to create the token for.",
name: "The name of the token to create." name: "The name of the token to create."
}, },
UPDATE_TOKEN: { UPDATE_TOKEN: {
@@ -589,10 +590,10 @@ export const TOKEN_AUTH = {
export const OIDC_AUTH = { export const OIDC_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
oidcDiscoveryUrl: "The URL used to retrieve the OpenID Connect configuration from the identity provider.", oidcDiscoveryUrl: "The URL used to retrieve the OpenID Connect configuration from the identity provider.",
caCert: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.", caCert: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.",
boundIssuer: "The unique identifier of the identity provider issuing the JWT.", boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
@@ -606,7 +607,7 @@ export const OIDC_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
oidcDiscoveryUrl: "The new URL used to retrieve the OpenID Connect configuration from the identity provider.", oidcDiscoveryUrl: "The new URL used to retrieve the OpenID Connect configuration from the identity provider.",
caCert: "The new PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.", caCert: "The new PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.",
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.", boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
@@ -620,19 +621,19 @@ export const OIDC_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
export const JWT_AUTH = { export const JWT_AUTH = {
LOGIN: { LOGIN: {
identityId: "The ID of the identity to login." identityId: "The ID of the machine identity to login."
}, },
ATTACH: { ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the machine identity to attach the configuration onto.",
configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'", configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl: jwksUrl:
"The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", "The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
@@ -649,7 +650,7 @@ export const JWT_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update the auth method for.", identityId: "The ID of the machine identity to update the auth method for.",
configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'", configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl: jwksUrl:
"The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.", "The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
@@ -666,10 +667,10 @@ export const JWT_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used." accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for." identityId: "The ID of the machine identity to retrieve the auth method for."
}, },
REVOKE: { REVOKE: {
identityId: "The ID of the identity to revoke the auth method for." identityId: "The ID of the machine identity to revoke the auth method for."
} }
} as const; } as const;
@@ -853,12 +854,12 @@ export const PROJECT_IDENTITIES = {
search: "The text string that identity membership names will be filtered by." search: "The text string that identity membership names will be filtered by."
}, },
GET_IDENTITY_MEMBERSHIP_BY_ID: { GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.", identityId: "The ID of the machine identity to get the membership for.",
projectId: "The ID of the project to get the identity membership for." projectId: "The ID of the project to get the identity membership for."
}, },
UPDATE_IDENTITY_MEMBERSHIP: { UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.", projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the identity to update the membership for.", identityId: "The ID of the machine identity to update the membership for.",
roles: { roles: {
description: "A list of role slugs to assign to the identity project membership.", description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.", role: "The role slug to assign to the newly created identity project membership.",
@@ -871,11 +872,11 @@ export const PROJECT_IDENTITIES = {
}, },
DELETE_IDENTITY_MEMBERSHIP: { DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.", projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the identity to delete the membership from." identityId: "The ID of the machine identity to delete the membership from."
}, },
CREATE_IDENTITY_MEMBERSHIP: { CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership from.", projectId: "The ID of the project to create the identity membership from.",
identityId: "The ID of the identity to create the membership from.", identityId: "The ID of the machine identity to create the membership from.",
role: "The role slug to assign to the newly created identity project membership.", role: "The role slug to assign to the newly created identity project membership.",
roles: { roles: {
description: "A list of role slugs to assign to the newly created identity project membership.", description: "A list of role slugs to assign to the newly created identity project membership.",
@@ -949,7 +950,8 @@ export const FOLDERS = {
projectId: "The ID of the project to delete the folder from.", projectId: "The ID of the project to delete the folder from.",
environment: "The slug of the environment where the folder is located.", environment: "The slug of the environment where the folder is located.",
directory: "The directory of the folder to delete. (Deprecated in favor of path)", directory: "The directory of the folder to delete. (Deprecated in favor of path)",
path: "The path of the folder to delete." path: "The path of the folder to delete.",
forceDelete: "Whether to force delete the folder even if it contains resources."
} }
} as const; } as const;
@@ -1270,7 +1272,7 @@ export const SECRET_TAGS = {
export const IDENTITY_ADDITIONAL_PRIVILEGE = { export const IDENTITY_ADDITIONAL_PRIVILEGE = {
CREATE: { CREATE: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to create.", identityId: "The ID of the machine identity to create.",
slug: "The slug of the privilege to create.", slug: "The slug of the privilege to create.",
permissions: `@deprecated - use privilegePermission permissions: `@deprecated - use privilegePermission
The permission object for the privilege. The permission object for the privilege.
@@ -1296,7 +1298,7 @@ The permission object for the privilege.
}, },
UPDATE: { UPDATE: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to update.", identityId: "The ID of the machine identity to update.",
slug: "The slug of the privilege to update.", slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.", newSlug: "The new slug of the privilege to update.",
permissions: `@deprecated - use privilegePermission permissions: `@deprecated - use privilegePermission
@@ -1322,17 +1324,17 @@ The permission object for the privilege.
}, },
DELETE: { DELETE: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to delete.", identityId: "The ID of the machine identity to delete.",
slug: "The slug of the privilege to delete." slug: "The slug of the privilege to delete."
}, },
GET_BY_SLUG: { GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.", identityId: "The ID of the machine identity to list.",
slug: "The slug of the privilege." slug: "The slug of the privilege."
}, },
LIST: { LIST: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.", identityId: "The ID of the machine identity to list.",
unpacked: "Whether the system should send the permissions as unpacked." unpacked: "Whether the system should send the permissions as unpacked."
} }
}; };
@@ -1374,7 +1376,7 @@ export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = { export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
CREATE: { CREATE: {
identityId: "The ID of the identity to create the privilege for.", identityId: "The ID of the machine identity to create the privilege for.",
projectId: "The ID of the project of the identity in.", projectId: "The ID of the project of the identity in.",
slug: "The slug of the privilege to create.", slug: "The slug of the privilege to create.",
permission: "The permission for the privilege.", permission: "The permission for the privilege.",
@@ -1385,7 +1387,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
}, },
UPDATE: { UPDATE: {
id: "The ID of the identity privilege.", id: "The ID of the identity privilege.",
identityId: "The ID of the identity to update.", identityId: "The ID of the machine identity to update.",
slug: "The slug of the privilege to update.", slug: "The slug of the privilege to update.",
privilegePermission: "The permission for the privilege.", privilegePermission: "The permission for the privilege.",
isTemporary: "Whether the privilege is temporary.", isTemporary: "Whether the privilege is temporary.",
@@ -1395,12 +1397,12 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
}, },
DELETE: { DELETE: {
id: "The ID of the identity privilege.", id: "The ID of the identity privilege.",
identityId: "The ID of the identity to delete.", identityId: "The ID of the machine identity to delete.",
slug: "The slug of the privilege to delete." slug: "The slug of the privilege to delete."
}, },
GET_BY_SLUG: { GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.", projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.", identityId: "The ID of the machine identity to list.",
slug: "The slug of the privilege." slug: "The slug of the privilege."
}, },
GET_BY_ID: { GET_BY_ID: {
@@ -1408,7 +1410,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
}, },
LIST: { LIST: {
projectId: "The ID of the project that the identity is in.", projectId: "The ID of the project that the identity is in.",
identityId: "The ID of the identity to list." identityId: "The ID of the machine identity to list."
} }
}; };
@@ -2355,6 +2357,9 @@ export const AppConnections = {
sslRejectUnauthorized: sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false). Set to false only in test environments with self-signed certificates.", "Whether or not to reject unauthorized SSL certificates (true/false). Set to false only in test environments with self-signed certificates.",
sslCertificate: "The SSL certificate (PEM format) to use for secure connection." sslCertificate: "The SSL certificate (PEM format) to use for secure connection."
},
LARAVEL_FORGE: {
apiToken: "The API token used to authenticate with Laravel Forge."
} }
} }
}; };
@@ -2507,6 +2512,14 @@ export const SecretSyncs = {
branch: "The branch to sync preview secrets to.", branch: "The branch to sync preview secrets to.",
teamId: "The ID of the Vercel team to sync secrets to." teamId: "The ID of the Vercel team to sync secrets to."
}, },
LARAVEL_FORGE: {
orgSlug: "The slug of the Laravel Forge org to sync secrets to.",
orgName: "The name of the Laravel Forge org to sync secrets to.",
serverId: "The ID of the Laravel Forge server to sync secrets to.",
serverName: "The name of the Laravel Forge server to sync secrets to.",
siteId: "The ID of the Laravel Forge site to sync secrets to.",
siteName: "The name of the Laravel Forge site to sync secrets to."
},
WINDMILL: { WINDMILL: {
workspace: "The Windmill workspace to sync secrets to.", workspace: "The Windmill workspace to sync secrets to.",
path: "The Windmill workspace path to sync secrets to." path: "The Windmill workspace path to sync secrets to."
+69 -8
View File
@@ -169,15 +169,24 @@ import { externalCertificateAuthorityDALFactory } from "@app/services/certificat
import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal"; import { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal";
import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns"; import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { convertorServiceFactory } from "@app/services/convertor/convertor-service"; import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
import { apiEnrollmentConfigDALFactory } from "@app/services/enrollment-config/api-enrollment-config-dal";
import { estEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { vaultExternalMigrationConfigDALFactory } from "@app/services/external-migration/vault-external-migration-config-dal";
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal"; import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal"; import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
@@ -535,6 +544,8 @@ export const registerRoutes = async (
const membershipRoleDAL = membershipRoleDALFactory(db); const membershipRoleDAL = membershipRoleDALFactory(db);
const roleDAL = roleDALFactory(db); const roleDAL = roleDALFactory(db);
const vaultExternalMigrationConfigDAL = vaultExternalMigrationConfigDALFactory(db);
const eventBusService = eventBusFactory(server.redis); const eventBusService = eventBusFactory(server.redis);
const sseService = sseServiceFactory(eventBusService, server.redis); const sseService = sseServiceFactory(eventBusService, server.redis);
@@ -1034,6 +1045,10 @@ export const registerRoutes = async (
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db); const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db); const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
const certificateTemplateV2DAL = certificateTemplateV2DALFactory(db);
const certificateProfileDAL = certificateProfileDALFactory(db);
const apiEnrollmentConfigDAL = apiEnrollmentConfigDALFactory(db);
const estEnrollmentConfigDAL = estEnrollmentConfigDALFactory(db);
const certificateDAL = certificateDALFactory(db); const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
@@ -1118,6 +1133,21 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const certificateTemplateV2Service = certificateTemplateV2ServiceFactory({
certificateTemplateV2DAL,
permissionService
});
const certificateProfileService = certificateProfileServiceFactory({
certificateProfileDAL,
certificateTemplateV2DAL,
apiEnrollmentConfigDAL,
estEnrollmentConfigDAL,
permissionService,
kmsService,
projectDAL
});
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
@@ -1345,7 +1375,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
folderCommitService, folderCommitService,
secretApprovalPolicyService, secretApprovalPolicyService,
secretV2BridgeDAL secretV2BridgeDAL,
dynamicSecretDAL
}); });
const secretImportService = secretImportServiceFactory({ const secretImportService = secretImportServiceFactory({
@@ -1882,13 +1913,6 @@ export const registerRoutes = async (
notificationService notificationService
}); });
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService,
gatewayService
});
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({ const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
permissionService, permissionService,
licenseService, licenseService,
@@ -2090,6 +2114,27 @@ export const registerRoutes = async (
pkiSyncQueue pkiSyncQueue
}); });
const certificateV3Service = certificateV3ServiceFactory({
certificateDAL,
certificateAuthorityDAL,
certificateProfileDAL,
certificateTemplateV2Service,
internalCaService: internalCertificateAuthorityService,
permissionService
});
const certificateEstV3Service = certificateEstV3ServiceFactory({
internalCertificateAuthorityService,
certificateTemplateV2Service,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService,
licenseService,
certificateProfileDAL,
estEnrollmentConfigDAL
});
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL, pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -2205,6 +2250,18 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService,
gatewayService,
kmsService,
appConnectionService,
vaultExternalMigrationConfigDAL,
secretService,
auditLogService
});
// setup the communication with license key server // setup the communication with license key server
await licenseService.init(); await licenseService.init();
@@ -2309,6 +2366,8 @@ export const registerRoutes = async (
auditLog: auditLogService, auditLog: auditLogService,
auditLogStream: auditLogStreamService, auditLogStream: auditLogStreamService,
certificate: certificateService, certificate: certificateService,
certificateV3: certificateV3Service,
certificateEstV3: certificateEstV3Service,
sshCertificateAuthority: sshCertificateAuthorityService, sshCertificateAuthority: sshCertificateAuthorityService,
sshCertificateTemplate: sshCertificateTemplateService, sshCertificateTemplate: sshCertificateTemplateService,
sshHost: sshHostService, sshHost: sshHostService,
@@ -2316,6 +2375,8 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService, certificateAuthority: certificateAuthorityService,
internalCertificateAuthority: internalCertificateAuthorityService, internalCertificateAuthority: internalCertificateAuthorityService,
certificateTemplate: certificateTemplateService, certificateTemplate: certificateTemplateService,
certificateTemplateV2: certificateTemplateV2Service,
certificateProfile: certificateProfileService,
certificateAuthorityCrl: certificateAuthorityCrlService, certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService, certificateEst: certificateEstService,
pit: pitService, pit: pitService,
@@ -77,6 +77,10 @@ import {
HumanitecConnectionListItemSchema, HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec"; } from "@app/services/app-connection/humanitec";
import {
LaravelForgeConnectionListItemSchema,
SanitizedLaravelForgeConnectionSchema
} from "@app/services/app-connection/laravel-forge";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql"; import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
@@ -158,7 +162,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedNetlifyConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options, ...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options ...SanitizedRedisConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -200,7 +205,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
NetlifyConnectionListItemSchema, NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema, OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema, AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema RedisConnectionListItemSchema,
LaravelForgeConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -24,6 +24,7 @@ import { registerGitLabConnectionRouter } from "./gitlab-connection-router";
import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHerokuConnectionRouter } from "./heroku-connection-router"; import { registerHerokuConnectionRouter } from "./heroku-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router";
@@ -71,6 +72,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.OnePass]: registerOnePassConnectionRouter, [AppConnection.OnePass]: registerOnePassConnectionRouter,
[AppConnection.Heroku]: registerHerokuConnectionRouter, [AppConnection.Heroku]: registerHerokuConnectionRouter,
[AppConnection.Render]: registerRenderConnectionRouter, [AppConnection.Render]: registerRenderConnectionRouter,
[AppConnection.LaravelForge]: registerLaravelForgeConnectionRouter,
[AppConnection.Flyio]: registerFlyioConnectionRouter, [AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter, [AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter, [AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
@@ -0,0 +1,128 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLaravelForgeConnectionSchema,
SanitizedLaravelForgeConnectionSchema,
UpdateLaravelForgeConnectionSchema
} from "@app/services/app-connection/laravel-forge";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerLaravelForgeConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.LaravelForge,
server,
sanitizedResponseSchema: SanitizedLaravelForgeConnectionSchema,
createSchema: CreateLaravelForgeConnectionSchema,
updateSchema: UpdateLaravelForgeConnectionSchema
});
server.route({
method: "GET",
url: `/:connectionId/organizations`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string(),
slug: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const organizations = await server.services.appConnection.laravelForge.listOrganizations(
connectionId,
req.permission
);
return organizations;
}
});
server.route({
method: "GET",
url: `/:connectionId/servers`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
organizationSlug: z.string()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { organizationSlug } = req.query;
const servers = await server.services.appConnection.laravelForge.listServers(
connectionId,
req.permission,
organizationSlug
);
return servers;
}
});
server.route({
method: "GET",
url: `/:connectionId/sites`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
organizationSlug: z.string(),
serverId: z.string()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { organizationSlug, serverId } = req.query;
const sites = await server.services.appConnection.laravelForge.listSites(
connectionId,
req.permission,
organizationSlug,
serverId
);
return sites;
}
});
};
@@ -0,0 +1,506 @@
import RE2 from "re2";
import { z } from "zod";
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";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertStatus } from "@app/services/certificate/certificate-types";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
export const registerCertificateProfilesRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
body: z
.object({
projectId: z.string().min(1),
caId: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1),
caChain: z.string().optional()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
}
),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateProfile = await server.services.certificateProfile.createProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.body.projectId,
data: req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_PROFILE,
metadata: {
certificateProfileId: certificateProfile.id,
name: certificateProfile.slug,
projectId: certificateProfile.projectId,
enrollmentType: certificateProfile.enrollmentType
}
}
});
return { certificateProfile };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
querystring: z.object({
projectId: z.string().min(1),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
caId: z.string().uuid().optional(),
includeMetrics: z.coerce.boolean().optional().default(false),
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
}),
response: {
200: z.object({
certificateProfiles: PkiCertificateProfilesSchema.extend({
metrics: z
.object({
profileId: z.string(),
totalCertificates: z.number(),
activeCertificates: z.number(),
expiredCertificates: z.number(),
expiringCertificates: z.number(),
revokedCertificates: z.number()
})
.optional(),
estConfig: z
.object({
id: z.string(),
disableBootstrapCaValidation: z.boolean(),
passphrase: z.string().optional(),
caChain: z.string().optional()
})
.optional(),
apiConfig: z
.object({
id: z.string(),
autoRenew: z.boolean(),
autoRenewDays: z.number().optional()
})
.optional()
}).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { profiles, totalCount } = await server.services.certificateProfile.listProfiles({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.LIST_CERTIFICATE_PROFILES,
metadata: {
projectId: req.query.projectId
}
}
});
return { certificateProfiles: profiles, totalCount };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
includeMetrics: z.coerce.boolean().optional().default(false),
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
}),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema.extend({
certificateAuthority: z
.object({
id: z.string(),
projectId: z.string(),
status: z.string(),
name: z.string()
})
.optional(),
certificateTemplate: z
.object({
id: z.string(),
projectId: z.string(),
name: z.string(),
description: z.string().optional()
})
.optional(),
estConfig: z
.object({
id: z.string(),
disableBootstrapCaValidation: z.boolean(),
passphrase: z.string(),
caChain: z.string().optional()
})
.optional(),
apiConfig: z
.object({
id: z.string(),
autoRenew: z.boolean(),
autoRenewDays: z.number().optional()
})
.optional(),
metrics: z
.object({
profileId: z.string(),
totalCertificates: z.number(),
activeCertificates: z.number(),
expiredCertificates: z.number(),
expiringCertificates: z.number(),
revokedCertificates: z.number()
})
.optional()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateProfile = await server.services.certificateProfile.getProfileByIdWithConfigs({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id
});
let result = certificateProfile;
if (req.query.includeMetrics) {
const metrics = await server.services.certificateProfile.getProfileMetrics({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id,
expiringDays: req.query.expiringDays
});
result = { ...certificateProfile, metrics };
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateProfile.projectId,
event: {
type: EventType.GET_CERTIFICATE_PROFILE,
metadata: {
certificateProfileId: certificateProfile.id,
name: certificateProfile.slug
}
}
});
return { certificateProfile: result };
}
});
server.route({
method: "GET",
url: "/slug/:slug",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({
slug: z.string().min(1)
}),
querystring: z.object({
projectId: z.string().min(1)
}),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateProfile = await server.services.certificateProfile.getProfileBySlug({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
projectId: req.query.projectId,
slug: req.params.slug
});
return { certificateProfile };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({
id: z.string().uuid()
}),
body: z
.object({
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
.optional(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1).optional(),
caChain: z.string().optional()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
}
return true;
},
{
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
}
),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateProfile = await server.services.certificateProfile.updateProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id,
data: req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateProfile.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_PROFILE,
metadata: {
certificateProfileId: certificateProfile.id,
name: certificateProfile.slug
}
}
});
return { certificateProfile };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({
id: z.string().uuid()
}),
response: {
200: z.object({
certificateProfile: PkiCertificateProfilesSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateProfile = await server.services.certificateProfile.deleteProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateProfile.projectId,
event: {
type: EventType.DELETE_CERTIFICATE_PROFILE,
metadata: {
certificateProfileId: certificateProfile.id,
name: certificateProfile.slug
}
}
});
return { certificateProfile };
}
});
server.route({
method: "GET",
url: "/:id/certificates",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateProfiles],
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
status: z.nativeEnum(CertStatus).optional(),
search: z.string().optional()
}),
response: {
200: z.object({
certificates: z.array(
z.object({
id: z.string(),
serialNumber: z.string(),
cn: z.string(),
status: z.string(),
notBefore: z.date(),
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
createdAt: z.date()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificates = await server.services.certificateProfile.getProfileCertificates({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.params.id,
...req.query
});
return { certificates };
}
});
};
@@ -52,7 +52,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
event: { event: {
type: EventType.GET_CERTIFICATE_TEMPLATE, type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
} }
} }
}); });
@@ -121,7 +122,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
name: certificateTemplate.name, name: certificateTemplate.name,
commonName: certificateTemplate.commonName, commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName, subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl ttl: certificateTemplate.ttl,
projectId: certificateTemplate.projectId
} }
} }
}); });
@@ -184,9 +186,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
type: EventType.UPDATE_CERTIFICATE_TEMPLATE, type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id, certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name,
caId: certificateTemplate.caId, caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string, pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName, commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName, subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl ttl: certificateTemplate.ttl
@@ -230,7 +232,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
event: { event: {
type: EventType.DELETE_CERTIFICATE_TEMPLATE, type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
} }
} }
}); });
@@ -318,7 +318,8 @@ export const registerDeprecatedSecretFolderRouter = async (server: FastifyZodPro
...req.body, ...req.body,
projectId: req.body.workspaceId, projectId: req.body.workspaceId,
idOrName: req.params.folderIdOrName, idOrName: req.params.folderIdOrName,
path path,
forceDelete: true
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -13,7 +13,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Renew access token", description: "Renew machine identity access token",
body: z.object({ body: z.object({
accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken) accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken)
}), }),
@@ -48,7 +48,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Revoke access token", description: "Revoke machine identity access token",
body: z.object({ body: z.object({
accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken) accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken)
}), }),
@@ -21,7 +21,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AliCloudAuth], tags: [ApiDocsTags.AliCloudAuth],
description: "Login with Alibaba Cloud Auth", description: "Login with Alibaba Cloud Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(ALICLOUD_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(ALICLOUD_AUTH.LOGIN.identityId),
Action: z.enum(["GetCallerIdentity"]).describe(ALICLOUD_AUTH.LOGIN.Action), Action: z.enum(["GetCallerIdentity"]).describe(ALICLOUD_AUTH.LOGIN.Action),
@@ -108,7 +108,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AliCloudAuth], tags: [ApiDocsTags.AliCloudAuth],
description: "Attach Alibaba Cloud Auth configuration onto identity", description: "Attach Alibaba Cloud Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -200,7 +200,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AliCloudAuth], tags: [ApiDocsTags.AliCloudAuth],
description: "Update Alibaba Cloud Auth configuration on identity", description: "Update Alibaba Cloud Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -292,7 +292,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AliCloudAuth], tags: [ApiDocsTags.AliCloudAuth],
description: "Retrieve Alibaba Cloud Auth configuration on identity", description: "Retrieve Alibaba Cloud Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -340,7 +340,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AliCloudAuth], tags: [ApiDocsTags.AliCloudAuth],
description: "Delete Alibaba Cloud Auth configuration on identity", description: "Delete Alibaba Cloud Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -23,7 +23,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AwsAuth], tags: [ApiDocsTags.AwsAuth],
description: "Login with AWS Auth", description: "Login with AWS Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod), iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
@@ -75,7 +75,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AwsAuth], tags: [ApiDocsTags.AwsAuth],
description: "Attach AWS Auth configuration onto identity", description: "Attach AWS Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -171,7 +171,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AwsAuth], tags: [ApiDocsTags.AwsAuth],
description: "Update AWS Auth configuration on identity", description: "Update AWS Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -255,7 +255,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AwsAuth], tags: [ApiDocsTags.AwsAuth],
description: "Retrieve AWS Auth configuration on identity", description: "Retrieve AWS Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -303,7 +303,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AwsAuth], tags: [ApiDocsTags.AwsAuth],
description: "Delete AWS Auth configuration on identity", description: "Delete AWS Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -20,7 +20,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AzureAuth], tags: [ApiDocsTags.AzureAuth],
description: "Login with Azure Auth", description: "Login with Azure Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string() jwt: z.string()
@@ -70,7 +70,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AzureAuth], tags: [ApiDocsTags.AzureAuth],
description: "Attach Azure Auth configuration onto identity", description: "Attach Azure Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -165,7 +165,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AzureAuth], tags: [ApiDocsTags.AzureAuth],
description: "Update Azure Auth configuration on identity", description: "Update Azure Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -255,7 +255,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AzureAuth], tags: [ApiDocsTags.AzureAuth],
description: "Retrieve Azure Auth configuration on identity", description: "Retrieve Azure Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -304,7 +304,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.AzureAuth], tags: [ApiDocsTags.AzureAuth],
description: "Delete Azure Auth configuration on identity", description: "Delete Azure Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -20,7 +20,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.GcpAuth], tags: [ApiDocsTags.GcpAuth],
description: "Login with GCP Auth", description: "Login with GCP Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId),
jwt: z.string() jwt: z.string()
@@ -70,7 +70,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.GcpAuth], tags: [ApiDocsTags.GcpAuth],
description: "Attach GCP Auth configuration onto identity", description: "Attach GCP Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -163,7 +163,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.GcpAuth], tags: [ApiDocsTags.GcpAuth],
description: "Update GCP Auth configuration on identity", description: "Update GCP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -249,7 +249,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.GcpAuth], tags: [ApiDocsTags.GcpAuth],
description: "Retrieve GCP Auth configuration on identity", description: "Retrieve GCP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -298,7 +298,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.GcpAuth], tags: [ApiDocsTags.GcpAuth],
description: "Delete GCP Auth configuration on identity", description: "Delete GCP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -96,7 +96,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.JwtAuth], tags: [ApiDocsTags.JwtAuth],
description: "Login with JWT Auth", description: "Login with JWT Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim()
@@ -148,7 +148,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.JwtAuth], tags: [ApiDocsTags.JwtAuth],
description: "Attach JWT Auth configuration onto identity", description: "Attach JWT Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -217,7 +217,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.JwtAuth], tags: [ApiDocsTags.JwtAuth],
description: "Update JWT Auth configuration on identity", description: "Update JWT Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -283,7 +283,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.JwtAuth], tags: [ApiDocsTags.JwtAuth],
description: "Retrieve JWT Auth configuration on identity", description: "Retrieve JWT Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -332,7 +332,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.JwtAuth], tags: [ApiDocsTags.JwtAuth],
description: "Delete JWT Auth configuration on identity", description: "Delete JWT Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -41,7 +41,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.KubernetesAuth], tags: [ApiDocsTags.KubernetesAuth],
description: "Login with Kubernetes Auth", description: "Login with Kubernetes Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim()
@@ -93,7 +93,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.KubernetesAuth], tags: [ApiDocsTags.KubernetesAuth],
description: "Attach Kubernetes Auth configuration onto identity", description: "Attach Kubernetes Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -240,7 +240,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.KubernetesAuth], tags: [ApiDocsTags.KubernetesAuth],
description: "Update Kubernetes Auth configuration on identity", description: "Update Kubernetes Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -383,7 +383,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.KubernetesAuth], tags: [ApiDocsTags.KubernetesAuth],
description: "Retrieve Kubernetes Auth configuration on identity", description: "Retrieve Kubernetes Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -432,7 +432,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.KubernetesAuth], tags: [ApiDocsTags.KubernetesAuth],
description: "Delete Kubernetes Auth configuration on identity", description: "Delete Kubernetes Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -120,7 +120,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Login with LDAP Auth", description: "Login with LDAP Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
username: z.string().describe(LDAP_AUTH.LOGIN.username), username: z.string().describe(LDAP_AUTH.LOGIN.username),
@@ -198,7 +198,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Attach LDAP Auth configuration onto identity", description: "Attach LDAP Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -389,7 +389,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Update LDAP Auth configuration on identity", description: "Update LDAP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -510,7 +510,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Retrieve LDAP Auth configuration on identity", description: "Retrieve LDAP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -568,7 +568,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Delete LDAP Auth configuration on identity", description: "Delete LDAP Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -621,7 +621,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.LdapAuth], tags: [ApiDocsTags.LdapAuth],
description: "Clear LDAP Auth Lockouts for identity", description: "Clear LDAP Auth Lockouts for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -20,7 +20,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OciAuth], tags: [ApiDocsTags.OciAuth],
description: "Login with OCI Auth", description: "Login with OCI Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid), userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
@@ -87,7 +87,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OciAuth], tags: [ApiDocsTags.OciAuth],
description: "Attach OCI Auth configuration onto identity", description: "Attach OCI Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -176,7 +176,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OciAuth], tags: [ApiDocsTags.OciAuth],
description: "Update OCI Auth configuration on identity", description: "Update OCI Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -259,7 +259,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OciAuth], tags: [ApiDocsTags.OciAuth],
description: "Retrieve OCI Auth configuration on identity", description: "Retrieve OCI Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -307,7 +307,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OciAuth], tags: [ApiDocsTags.OciAuth],
description: "Delete OCI Auth configuration on identity", description: "Delete OCI Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -44,7 +44,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OidcAuth], tags: [ApiDocsTags.OidcAuth],
description: "Login with OIDC Auth", description: "Login with OIDC Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId),
jwt: z.string().trim() jwt: z.string().trim()
@@ -100,7 +100,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OidcAuth], tags: [ApiDocsTags.OidcAuth],
description: "Attach OIDC Auth configuration onto identity", description: "Attach OIDC Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -201,7 +201,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OidcAuth], tags: [ApiDocsTags.OidcAuth],
description: "Update OIDC Auth configuration on identity", description: "Update OIDC Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -300,7 +300,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OidcAuth], tags: [ApiDocsTags.OidcAuth],
description: "Retrieve OIDC Auth configuration on identity", description: "Retrieve OIDC Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -349,7 +349,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.OidcAuth], tags: [ApiDocsTags.OidcAuth],
description: "Delete OIDC Auth configuration on identity", description: "Delete OIDC Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -34,7 +34,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "Create identity", description: "Create machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -109,7 +109,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "Update identity", description: "Update machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -173,7 +173,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "Delete identity", description: "Delete machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -222,7 +222,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "Get an identity by id", description: "Get a machine identity by id",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -280,7 +280,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "List identities", description: "List machine identities",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -330,7 +330,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.Identities], tags: [ApiDocsTags.Identities],
description: "Search identities", description: "Search machine identities",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -427,7 +427,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "List project memberships that identity with id is part of", description: "List project memberships that machine identity with id is part of",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -44,7 +44,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Login with TLS Certificate Auth", description: "Login with TLS Certificate Auth for machine identity",
body: z.object({ body: z.object({
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId) identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
}), }),
@@ -102,7 +102,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Attach TLS Certificate Auth configuration onto identity", description: "Attach TLS Certificate Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -203,7 +203,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Update TLS Certificate Auth configuration on identity", description: "Update TLS Certificate Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -304,7 +304,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Retrieve TLS Certificate Auth configuration on identity", description: "Retrieve TLS Certificate Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -354,7 +354,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TlsCertAuth], tags: [ApiDocsTags.TlsCertAuth],
description: "Delete TLS Certificate Auth configuration on identity", description: "Delete TLS Certificate Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -20,7 +20,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Attach Token Auth configuration onto identity", description: "Attach Token Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -112,7 +112,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Update Token Auth configuration on identity", description: "Update Token Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -198,7 +198,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Retrieve Token Auth configuration on identity", description: "Retrieve Token Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -247,7 +247,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Delete Token Auth configuration on identity", description: "Delete Token Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -297,7 +297,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Create token for identity with Token Auth", description: "Create token for machine identity with Token Auth",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -361,7 +361,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Get tokens for identity with Token Auth", description: "Get tokens for machine identity with Token Auth",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -416,7 +416,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Update token for identity with Token Auth", description: "Update token for machine identity with Token Auth",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -472,7 +472,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.TokenAuth], tags: [ApiDocsTags.TokenAuth],
description: "Revoke token for identity with Token Auth", description: "Revoke token for machine identity with Token Auth",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -32,7 +32,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Login with Universal Auth", description: "Login with Universal Auth for machine identity",
body: z.object({ body: z.object({
clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId),
clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret) clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret)
@@ -90,7 +90,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Attach Universal Auth configuration onto identity", description: "Attach Universal Auth configuration onto machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -208,7 +208,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Update Universal Auth configuration on identity", description: "Update Universal Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -331,7 +331,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Retrieve Universal Auth configuration on identity", description: "Retrieve Universal Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -380,7 +380,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Delete Universal Auth configuration on identity", description: "Delete Universal Auth configuration on machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -429,7 +429,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Create Universal Auth Client Secret for identity", description: "Create Universal Auth Client Secret for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -487,7 +487,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "List Universal Auth Client Secrets for identity", description: "List Universal Auth Client Secrets for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -537,7 +537,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Get Universal Auth Client Secret for identity", description: "Get Universal Auth Client Secret for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -567,7 +567,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo, ...req.auditLogInfo,
orgId: clientSecretData.orgId, orgId: clientSecretData.orgId,
event: { event: {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID,
metadata: { metadata: {
identityId: clientSecretData.identityId, identityId: clientSecretData.identityId,
clientSecretId: clientSecretData.id clientSecretId: clientSecretData.id
@@ -589,7 +589,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Revoke Universal Auth Client Secrets for identity", description: "Revoke Universal Auth Client Secrets for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
@@ -641,7 +641,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
hide: false, hide: false,
tags: [ApiDocsTags.UniversalAuth], tags: [ApiDocsTags.UniversalAuth],
description: "Clear Universal Auth Lockouts for identity", description: "Clear Universal Auth Lockouts for machine identity",
security: [ security: [
{ {
bearerAuth: [] bearerAuth: []
+2
View File
@@ -11,6 +11,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
@@ -146,6 +147,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
); );
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" }); await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" });
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
@@ -21,6 +21,7 @@ import { registerGitLabSyncRouter } from "./gitlab-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHerokuSyncRouter } from "./heroku-sync-router"; import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
import { registerNetlifySyncRouter } from "./netlify-sync-router"; import { registerNetlifySyncRouter } from "./netlify-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router"; import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router"; import { registerRenderSyncRouter } from "./render-sync-router";
@@ -63,5 +64,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Checkly]: registerChecklySyncRouter, [SecretSync.Checkly]: registerChecklySyncRouter,
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter, [SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
[SecretSync.Netlify]: registerNetlifySyncRouter, [SecretSync.Netlify]: registerNetlifySyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter [SecretSync.Bitbucket]: registerBitbucketSyncRouter,
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
}; };
@@ -0,0 +1,17 @@
import {
CreateLaravelForgeSyncSchema,
LaravelForgeSyncSchema,
UpdateLaravelForgeSyncSchema
} from "@app/services/secret-sync/laravel-forge";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerLaravelForgeSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.LaravelForge,
server,
responseSchema: LaravelForgeSyncSchema,
createSchema: CreateLaravelForgeSyncSchema,
updateSchema: UpdateLaravelForgeSyncSchema
});
@@ -44,6 +44,7 @@ import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku"; import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify"; import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas"; import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas"; import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
@@ -84,7 +85,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
ChecklySyncSchema, ChecklySyncSchema,
DigitalOceanAppPlatformSyncSchema, DigitalOceanAppPlatformSyncSchema,
NetlifySyncSchema, NetlifySyncSchema,
BitbucketSyncSchema BitbucketSyncSchema,
LaravelForgeSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -117,7 +119,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
ChecklySyncListItemSchema, ChecklySyncListItemSchema,
SupabaseSyncListItemSchema, SupabaseSyncListItemSchema,
NetlifySyncListItemSchema, NetlifySyncListItemSchema,
BitbucketSyncListItemSchema BitbucketSyncListItemSchema,
LaravelForgeSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,367 @@
import RE2 from "re2";
import { z } from "zod";
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";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
import { certificateTemplateV2ResponseSchema } from "@app/services/certificate-template-v2/certificate-template-v2-schemas";
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
const templateV2SubjectSchema = z
.object({
type: attributeTypeSchema,
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Subject attribute must have at least one allowed, required, or denied value"
}
);
const templateV2KeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Key usages must have at least one allowed, required, or denied value"
}
);
const templateV2ExtendedKeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Extended key usages must have at least one allowed, required, or denied value"
}
);
const templateV2SanSchema = z
.object({
type: sanTypeSchema,
allowed: z.array(z.string()).optional(),
required: z.array(z.string()).optional(),
denied: z.array(z.string()).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "SAN must have at least one allowed, required, or denied value"
}
);
const templateV2ValiditySchema = z.object({
max: z
.string()
.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()
});
const templateV2AlgorithmsSchema = z.object({
signature: z.array(z.string()).min(1, "At least one signature algorithm must be provided").optional(),
keyAlgorithm: z.array(z.string()).min(1, "At least one key algorithm must be provided").optional()
});
const createCertificateTemplateV2Schema = z.object({
projectId: z.string().min(1),
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters"),
description: z.string().max(1000).optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional()
});
const updateCertificateTemplateV2Schema = z.object({
name: z.string().min(1).max(255, "Name must be between 1 and 255 characters").optional(),
description: z.string().max(1000).optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional()
});
export const registerCertificateTemplatesV2Router = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
body: createCertificateTemplateV2Schema,
response: {
200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projectId, ...data } = req.body;
const certificateTemplate = await server.services.certificateTemplateV2.createTemplateV2({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId,
projectId,
data
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name,
projectId: certificateTemplate.projectId
}
}
});
return { certificateTemplate };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
querystring: z.object({
projectId: z.string().min(1),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional()
}),
response: {
200: z.object({
certificateTemplates: certificateTemplateV2ResponseSchema.array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { templates, totalCount } = await server.services.certificateTemplateV2.listTemplatesV2({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.projectId,
event: {
type: EventType.LIST_CERTIFICATE_TEMPLATES,
metadata: {
projectId: req.query.projectId
}
}
});
return { certificateTemplates: templates, totalCount };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
id: z.string().uuid()
}),
response: {
200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplateV2.getTemplateV2ById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId,
templateId: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
}
}
});
return { certificateTemplate };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
id: z.string().uuid()
}),
body: updateCertificateTemplateV2Schema,
response: {
200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplateV2.updateTemplateV2({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId,
templateId: req.params.id,
data: req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
}
}
});
return { certificateTemplate };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificateTemplates],
params: z.object({
id: z.string().uuid()
}),
response: {
200: z.object({
certificateTemplate: certificateTemplateV2ResponseSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateTemplate = await server.services.certificateTemplateV2.deleteTemplateV2({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod!,
actorOrgId: req.permission.orgId,
templateId: req.params.id
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: certificateTemplate.projectId,
event: {
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: {
certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
}
}
});
return { certificateTemplate };
}
});
};
+3
View File
@@ -1,4 +1,5 @@
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { registerCertificateTemplatesV2Router } from "./certificate-templates-v2-router";
import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router"; import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router"; import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
@@ -19,6 +20,8 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerCertificateTemplatesV2Router, { prefix: "/certificate-templates" });
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -263,7 +263,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/") .default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash) .transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path) .describe(FOLDERS.DELETE.path),
forceDelete: z.boolean().optional().default(false).describe(FOLDERS.DELETE.forceDelete)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -279,7 +280,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
...req.body, ...req.body,
idOrName: req.params.folderIdOrName idOrName: req.params.folderIdOrName,
forceDelete: req.body.forceDelete
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
@@ -0,0 +1,346 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
ACMESANType,
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType
} from "@app/services/certificate-common/certificate-constants";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
interface CertificateRequestForService {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
altNames?: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
validity: {
ttl: string;
};
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
}
const validateTtlAndDateFields = (data: { notBefore?: string; notAfter?: string; ttl?: string }) => {
const hasDateFields = data.notBefore || data.notAfter;
const hasTtl = data.ttl;
return !(hasDateFields && hasTtl);
};
const validateDateOrder = (data: { notBefore?: string; notAfter?: string }) => {
if (data.notBefore && data.notAfter) {
const notBefore = new Date(data.notBefore);
const notAfter = new Date(data.notAfter);
return notBefore < notAfter;
}
return true;
};
export const registerCertificatesRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/issue-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
commonName: validateTemplateRegexField.optional(),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
altNames: z
.array(
z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
value: z.string().min(1, "SAN value cannot be empty")
})
)
.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
privateKey: z.string().trim().optional(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const certificateRequestForService: CertificateRequestForService = {
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
altNames: req.body.altNames,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
};
const mappedCertificateRequest = mapEnumsForValidation(certificateRequestForService);
const data = await server.services.certificateV3.issueCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateRequest: mappedCertificateRequest
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ISSUE_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
certificateId: data.certificateId,
commonName: req.body.commonName || "",
profileName: data.profileName
}
}
});
return data;
}
});
server.route({
method: "POST",
url: "/sign-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
csr: z.string().trim().min(1, "CSR cannot be empty").max(4096, "CSR cannot exceed 4096 characters"),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
certificate: z.string().trim(),
issuingCaCertificate: z.string().trim(),
certificateChain: z.string().trim(),
serialNumber: z.string().trim(),
certificateId: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateV3.signCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
csr: req.body.csr,
validity: {
ttl: req.body.ttl
},
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.SIGN_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
certificateId: data.certificateId,
profileName: data.profileName,
commonName: ""
}
}
});
return data;
}
});
server.route({
method: "POST",
url: "/order-certificate",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiCertificates],
body: z
.object({
profileId: z.string().uuid(),
subjectAlternativeNames: z
.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z
.string()
.trim()
.min(1, "SAN value cannot be empty")
.max(255, "SAN value must be less than 255 characters")
})
)
.min(1, "At least one subject alternative name must be provided"),
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
keyUsages: z.nativeEnum(CertKeyUsageType).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsageType).array().optional(),
notBefore: validateCaDateField.optional(),
notAfter: validateCaDateField.optional(),
commonName: validateTemplateRegexField.optional(),
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
})
.refine(validateTtlAndDateFields, {
message:
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
})
.refine(validateDateOrder, {
message: "notBefore must be earlier than notAfter"
}),
response: {
200: z.object({
orderId: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
subjectAlternativeNames: z.array(
z.object({
type: z.nativeEnum(ACMESANType),
value: z.string(),
status: z.nativeEnum(CertificateOrderStatus)
})
),
authorizations: z.array(
z.object({
identifier: z.object({
type: z.nativeEnum(ACMESANType),
value: z.string()
}),
status: z.nativeEnum(CertificateOrderStatus),
expires: z.string().optional(),
challenges: z.array(
z.object({
type: z.string(),
status: z.nativeEnum(CertificateOrderStatus),
url: z.string(),
token: z.string()
})
)
})
),
finalize: z.string(),
certificate: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const data = await server.services.certificateV3.orderCertificateFromProfile({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
profileId: req.body.profileId,
certificateOrder: {
altNames: req.body.subjectAlternativeNames,
validity: {
ttl: req.body.ttl
},
commonName: req.body.commonName,
keyUsages: req.body.keyUsages,
extendedKeyUsages: req.body.extendedKeyUsages,
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
signatureAlgorithm: req.body.signatureAlgorithm,
keyAlgorithm: req.body.keyAlgorithm
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: data.projectId,
event: {
type: EventType.ORDER_CERTIFICATE_FROM_PROFILE,
metadata: {
certificateProfileId: req.body.profileId,
orderId: data.orderId,
profileName: data.profileName
}
}
});
return data;
}
});
};
@@ -7,6 +7,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { import {
ExternalMigrationProviders, ExternalMigrationProviders,
VaultImportStatus,
VaultMappingType VaultMappingType
} from "@app/services/external-migration/external-migration-types"; } from "@app/services/external-migration/external-migration-types";
@@ -113,4 +114,366 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
return { enabled }; return { enabled };
} }
}); });
server.route({
method: "GET",
url: "/vault/configs",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
configs: z
.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const configs = await server.services.migration.getVaultExternalMigrationConfigs({
actor: req.permission
});
return { configs };
}
});
server.route({
method: "POST",
url: "/vault/configs",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
connectionId: z.string(),
namespace: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.createVaultExternalMigration({
...req.body,
actor: req.permission
});
return { config };
}
});
server.route({
method: "PUT",
url: "/vault/configs/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
connectionId: z.string(),
namespace: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.updateVaultExternalMigration({
id: req.params.id,
...req.body,
actor: req.permission
});
return { config };
}
});
server.route({
method: "DELETE",
url: "/vault/configs/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.deleteVaultExternalMigration({
id: req.params.id,
actor: req.permission
});
return { config };
}
});
server.route({
method: "GET",
url: "/vault/namespaces",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
namespaces: z.array(z.object({ id: z.string(), name: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const namespaces = await server.services.migration.getVaultNamespaces({
actor: req.permission
});
return { namespaces };
}
});
server.route({
method: "GET",
url: "/vault/policies",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string()
}),
response: {
200: z.object({
policies: z.array(z.object({ name: z.string(), rules: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const policies = await server.services.migration.getVaultPolicies({
actor: req.permission,
namespace: req.query.namespace
});
return { policies };
}
});
server.route({
method: "GET",
url: "/vault/mounts",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string()
}),
response: {
200: z.object({
mounts: z.array(z.object({ path: z.string(), type: z.string(), version: z.string().nullish() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mounts = await server.services.migration.getVaultMounts({
actor: req.permission,
namespace: req.query.namespace
});
return { mounts };
}
});
server.route({
method: "GET",
url: "/vault/auth-mounts",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
authType: z.string().optional()
}),
response: {
200: z.object({
mounts: z.array(z.object({ path: z.string(), type: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mounts = await server.services.migration.getVaultAuthMounts({
actor: req.permission,
namespace: req.query.namespace,
authType: req.query.authType
});
return { mounts };
}
});
server.route({
method: "POST",
url: "/vault/import-secrets",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
projectId: z.string(),
environment: z.string(),
secretPath: z.string(),
vaultNamespace: z.string(),
vaultSecretPath: z.string()
}),
response: {
200: z.object({
status: z.nativeEnum(VaultImportStatus)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const result = await server.services.migration.importVaultSecrets({
actor: req.permission,
auditLogInfo: req.auditLogInfo,
...req.body
});
return result;
}
});
server.route({
method: "GET",
url: "/vault/secret-paths",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
secretPaths: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretPaths = await server.services.migration.getVaultSecretPaths({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { secretPaths };
}
});
server.route({
method: "GET",
url: "/vault/auth-roles/kubernetes",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
roles: z.array(
z.object({
name: z.string(),
mountPath: z.string(),
bound_service_account_names: z.array(z.string()),
bound_service_account_namespaces: z.array(z.string()),
token_ttl: z.number().optional(),
token_max_ttl: z.number().optional(),
token_policies: z.array(z.string()).optional(),
token_bound_cidrs: z.array(z.string()).optional(),
token_explicit_max_ttl: z.number().optional(),
token_no_default_policy: z.boolean().optional(),
token_num_uses: z.number().optional(),
token_period: z.number().optional(),
token_type: z.string().optional(),
audience: z.string().optional(),
alias_name_source: z.string().optional(),
config: z.object({
kubernetes_host: z.string(),
kubernetes_ca_cert: z.string().optional(),
issuer: z.string().optional(),
disable_iss_validation: z.boolean().optional(),
disable_local_ca_jwt: z.boolean().optional()
})
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = await server.services.migration.getVaultKubernetesAuthRoles({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { roles };
}
});
}; };
+2
View File
@@ -1,3 +1,4 @@
import { registerCertificatesRouter } from "./certificates-router";
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router"; import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerExternalMigrationRouter } from "./external-migration-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
@@ -10,4 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" }); await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" });
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
await server.register(registerCertificatesRouter, { prefix: "/certificates" });
}; };
@@ -37,7 +37,8 @@ export enum AppConnection {
DigitalOcean = "digital-ocean", DigitalOcean = "digital-ocean",
Netlify = "netlify", Netlify = "netlify",
Okta = "okta", Okta = "okta",
Redis = "redis" Redis = "redis",
LaravelForge = "laravel-forge"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -103,6 +103,11 @@ import {
HumanitecConnectionMethod, HumanitecConnectionMethod,
validateHumanitecConnectionCredentials validateHumanitecConnectionCredentials
} from "./humanitec"; } from "./humanitec";
import {
getLaravelForgeConnectionListItem,
LaravelForgeConnectionMethod,
validateLaravelForgeConnectionCredentials
} from "./laravel-forge";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
@@ -187,6 +192,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getOnePassConnectionListItem(), getOnePassConnectionListItem(),
getHerokuConnectionListItem(), getHerokuConnectionListItem(),
getRenderConnectionListItem(), getRenderConnectionListItem(),
getLaravelForgeConnectionListItem(),
getFlyioConnectionListItem(), getFlyioConnectionListItem(),
getGitLabConnectionListItem(), getGitLabConnectionListItem(),
getCloudflareConnectionListItem(), getCloudflareConnectionListItem(),
@@ -316,6 +322,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LaravelForge]: validateLaravelForgeConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -368,6 +375,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case ZabbixConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken:
case DigitalOceanConnectionMethod.ApiToken: case DigitalOceanConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -463,7 +471,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported, [AppConnection.Netlify]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported,
[AppConnection.Redis]: platformManagedCredentialsNotSupported [AppConnection.Redis]: platformManagedCredentialsNotSupported,
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (
@@ -28,6 +28,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.OnePass]: "1Password", [AppConnection.OnePass]: "1Password",
[AppConnection.Heroku]: "Heroku", [AppConnection.Heroku]: "Heroku",
[AppConnection.Render]: "Render", [AppConnection.Render]: "Render",
[AppConnection.LaravelForge]: "Laravel Forge",
[AppConnection.Flyio]: "Fly.io", [AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab", [AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Cloudflare]: "Cloudflare",
@@ -70,6 +71,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.MySql]: AppConnectionPlanType.Regular, [AppConnection.MySql]: AppConnectionPlanType.Regular,
[AppConnection.Heroku]: AppConnectionPlanType.Regular, [AppConnection.Heroku]: AppConnectionPlanType.Regular,
[AppConnection.Render]: AppConnectionPlanType.Regular, [AppConnection.Render]: AppConnectionPlanType.Regular,
[AppConnection.LaravelForge]: AppConnectionPlanType.Regular,
[AppConnection.Flyio]: AppConnectionPlanType.Regular, [AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular, [AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular, [AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
@@ -89,6 +89,8 @@ import { ValidateHerokuConnectionCredentialsSchema } from "./heroku";
import { herokuConnectionService } from "./heroku/heroku-connection-service"; import { herokuConnectionService } from "./heroku/heroku-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLaravelForgeConnectionCredentialsSchema } from "./laravel-forge";
import { laravelForgeConnectionService } from "./laravel-forge/laravel-forge-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
@@ -157,6 +159,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema, [AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema,
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema, [AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema, [AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
[AppConnection.LaravelForge]: ValidateLaravelForgeConnectionCredentialsSchema,
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema, [AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema, [AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema, [AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
@@ -864,6 +867,7 @@ export const appConnectionServiceFactory = ({
supabase: supabaseConnectionService(connectAppConnectionById), supabase: supabaseConnectionService(connectAppConnectionById),
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById), digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
netlify: netlifyConnectionService(connectAppConnectionById), netlify: netlifyConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById) okta: oktaConnectionService(connectAppConnectionById),
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
}; };
}; };
@@ -148,6 +148,12 @@ import {
THumanitecConnectionInput, THumanitecConnectionInput,
TValidateHumanitecConnectionCredentialsSchema TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec"; } from "./humanitec";
import {
TLaravelForgeConnection,
TLaravelForgeConnectionConfig,
TLaravelForgeConnectionInput,
TValidateLaravelForgeConnectionCredentialsSchema
} from "./laravel-forge";
import { import {
TLdapConnection, TLdapConnection,
TLdapConnectionConfig, TLdapConnectionConfig,
@@ -256,6 +262,7 @@ export type TAppConnection = { id: string } & (
| TOnePassConnection | TOnePassConnection
| THerokuConnection | THerokuConnection
| TRenderConnection | TRenderConnection
| TLaravelForgeConnection
| TFlyioConnection | TFlyioConnection
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
@@ -302,6 +309,7 @@ export type TAppConnectionInput = { id: string } & (
| TOnePassConnectionInput | TOnePassConnectionInput
| THerokuConnectionInput | THerokuConnectionInput
| TRenderConnectionInput | TRenderConnectionInput
| TLaravelForgeConnectionInput
| TFlyioConnectionInput | TFlyioConnectionInput
| TGitLabConnectionInput | TGitLabConnectionInput
| TCloudflareConnectionInput | TCloudflareConnectionInput
@@ -366,6 +374,7 @@ export type TAppConnectionConfig =
| TOnePassConnectionConfig | TOnePassConnectionConfig
| THerokuConnectionConfig | THerokuConnectionConfig
| TRenderConnectionConfig | TRenderConnectionConfig
| TLaravelForgeConnectionConfig
| TFlyioConnectionConfig | TFlyioConnectionConfig
| TGitLabConnectionConfig | TGitLabConnectionConfig
| TCloudflareConnectionConfig | TCloudflareConnectionConfig
@@ -407,6 +416,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateOnePassConnectionCredentialsSchema | TValidateOnePassConnectionCredentialsSchema
| TValidateHerokuConnectionCredentialsSchema | TValidateHerokuConnectionCredentialsSchema
| TValidateRenderConnectionCredentialsSchema | TValidateRenderConnectionCredentialsSchema
| TValidateLaravelForgeConnectionCredentialsSchema
| TValidateFlyioConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema
@@ -2,3 +2,7 @@ export enum HCVaultConnectionMethod {
AccessToken = "access-token", AccessToken = "access-token",
AppRole = "app-role" AppRole = "app-role"
} }
export enum HCVaultAuthType {
Kubernetes = "kubernetes"
}
@@ -12,8 +12,56 @@ import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; import { HCVaultAuthType, HCVaultConnectionMethod } from "./hc-vault-connection-enums";
import { THCVaultConnection, THCVaultConnectionConfig, THCVaultMountResponse } from "./hc-vault-connection-types"; import {
THCVaultAuthMount,
THCVaultAuthMountResponse,
THCVaultConnection,
THCVaultConnectionConfig,
THCVaultKubernetesAuthConfig,
THCVaultKubernetesAuthRole,
THCVaultKubernetesAuthRoleWithConfig,
THCVaultMount,
THCVaultMountResponse
} from "./hc-vault-connection-types";
// Concurrency limit for HC Vault API requests to avoid rate limiting
const HC_VAULT_CONCURRENCY_LIMIT = 20;
/**
* Creates a concurrency limiter that restricts the number of concurrent async operations
* @param limit - Maximum number of concurrent operations
* @returns A function that takes an async function and executes it with concurrency control
*/
const createConcurrencyLimiter = (limit: number) => {
let activeCount = 0;
const queue: Array<() => void> = [];
const next = () => {
activeCount -= 1;
if (queue.length > 0) {
const resolve = queue.shift();
resolve?.();
}
};
return async <T>(fn: () => Promise<T>): Promise<T> => {
// If we're at the limit, wait in queue
if (activeCount >= limit) {
await new Promise<void>((resolve) => {
queue.push(resolve);
});
}
activeCount += 1;
try {
return await fn();
} finally {
next();
}
};
};
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => { export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
@@ -181,30 +229,573 @@ export const validateHCVaultConnectionCredentials = async (
} }
}; };
export const listHCVaultMounts = async ( export const listHCVaultPolicies = async (
namespace: string,
connection: THCVaultConnection, connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => { ) => {
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
const { data: listData } = await requestWithHCVaultGateway<{
data: {
policies: string[];
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
const policyNames = listData.data.policies || [];
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const policies = await Promise.all(
policyNames.map((policyName) =>
limiter(async () => {
try {
const { data: policyData } = await requestWithHCVaultGateway<{
data: {
name: string;
rules: string;
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy/${policyName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return {
name: policyData.data.name,
rules: policyData.data.rules
};
} catch (error: unknown) {
logger.error(error, `Unable to fetch policy details for ${policyName}`);
return {
name: policyName,
rules: ""
};
}
})
)
);
return policies;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault policies");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list policies: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list policies from HashiCorp Vault"
});
}
};
export const listHCVaultNamespaces = async (
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const currentNamespace = connection.credentials.namespace || "/";
// Helper function to fetch namespaces at a specific path
const fetchNamespacesAtPath = async (namespacePath: string): Promise<string[] | null> => {
try {
const { data } = await requestWithHCVaultGateway<{
data: {
keys: string[];
key_info?: {
[key: string]: {
id: string;
path: string;
custom_metadata?: Record<string, unknown>;
};
};
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/namespaces?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespacePath
}
});
return data.data.keys || [];
} catch (error: unknown) {
if (error instanceof AxiosError && error.response?.status === 404) {
// No child namespaces at this path
return null;
}
throw error;
}
};
// Recursive function to get all namespaces at all depths with controlled parallelization
const recursivelyGetAllNamespaces = async (
parentPath: string,
limiter: ReturnType<typeof createConcurrencyLimiter>
): Promise<string[]> => {
const childKeys = await fetchNamespacesAtPath(parentPath);
if (childKeys === null || childKeys.length === 0) {
return [];
}
// Process namespaces in parallel with concurrency control
const namespacesArrays = await Promise.all(
childKeys.map((namespaceKey) =>
limiter(async () => {
// Remove trailing slash from the key
const cleanNamespaceKey = namespaceKey.replace(/\/$/, "");
// Build the full path
let fullNamespacePath: string;
if (parentPath === "/") {
fullNamespacePath = cleanNamespaceKey;
} else {
fullNamespacePath = `${parentPath}/${cleanNamespaceKey}`;
}
// Recursively fetch child namespaces
const childNamespaces = await recursivelyGetAllNamespaces(fullNamespacePath, limiter);
// Return this namespace and all its children
return [fullNamespacePath, ...childNamespaces];
})
)
);
// Flatten the arrays into a single array
return namespacesArrays.flat();
};
try {
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Get all namespaces starting from currentNamespace
const childNamespaces = await recursivelyGetAllNamespaces(currentNamespace, limiter);
// Build the result array with full paths
const namespaces = childNamespaces.map((path) => ({
id: path,
name: path
}));
// Always include the current/root namespace
namespaces.unshift({
id: currentNamespace,
name: currentNamespace
});
return namespaces;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault namespaces");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list namespaces: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list namespaces from HashiCorp Vault"
});
}
};
export const listHCVaultMounts = async (
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
namespace?: string
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const targetNamespace = namespace || connection.credentials.namespace;
const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, { const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/mounts`, url: `${instanceUrl}/v1/sys/mounts`,
method: "GET", method: "GET",
headers: { headers: {
"X-Vault-Token": accessToken, "X-Vault-Token": accessToken,
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) ...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
} }
}); });
const mounts: string[] = []; const mounts: THCVaultMount[] = [];
// Filter for "kv" version 2 type only
Object.entries(data.data).forEach(([path, mount]) => { Object.entries(data.data).forEach(([path, mount]) => {
if (mount.type === "kv" && mount.options?.version === "2") { mounts.push({
mounts.push(path); path,
} type: mount.type,
version: mount.options?.version
});
}); });
return mounts; return mounts;
}; };
export const listHCVaultSecretPaths = async (
namespace: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
filterMountPath?: string
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
try {
let path: string;
if (kvVersion === "2") {
// For KV v2: /v1/{mount}/metadata/{path}?list=true
path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
} else {
// For KV v1: /v1/{mount}/{path}?list=true
path = secretPath ? `${mountPath}/${secretPath}` : mountPath;
}
const { data } = await requestWithHCVaultGateway<{
data: {
keys: string[];
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${path}?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data.keys;
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 404) {
return null;
}
throw error;
}
};
// Recursive function to get all secret paths in a mount with controlled parallelization
const recursivelyGetAllPaths = async (
mountPath: string,
kvVersion: "1" | "2",
limiter: ReturnType<typeof createConcurrencyLimiter>,
currentPath: string = ""
): Promise<string[]> => {
const paths = await getPaths(mountPath, currentPath, kvVersion);
if (paths === null || paths.length === 0) {
return [];
}
// Process paths in parallel with concurrency control
const secretPathsArrays = await Promise.all(
paths.map((path) =>
limiter(async () => {
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
if (path.endsWith("/")) {
// it's a folder so we recurse into it
return recursivelyGetAllPaths(mountPath, kvVersion, limiter, fullItemPath);
}
// it's a secret so we return it
return [`${mountPath}/${fullItemPath}`];
})
)
);
// Flatten the arrays into a single array
return secretPathsArrays.flat();
};
// Get all mounts
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
// Filter for KV mounts (kv, kv-v1, kv-v2)
let kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
// If filterMountPath is provided, filter to only that mount
if (filterMountPath) {
const normalizedFilterPath = filterMountPath.replace(/\/$/, ""); // Remove trailing slash
kvMounts = kvMounts.filter((mount) => mount.path.replace(/\/$/, "") === normalizedFilterPath);
}
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Collect all secret paths from all KV mounts in parallel
const allSecretPathsArrays = await Promise.all(
kvMounts.map(async (mount) => {
const kvVersion = mount.version === "2" ? "2" : "1";
const cleanMountPath = mount.path.replace(/\/$/, ""); // Remove trailing slash
return recursivelyGetAllPaths(cleanMountPath, kvVersion, limiter);
})
);
// Flatten the arrays into a single array
const allSecretPaths = allSecretPathsArrays.flat();
return allSecretPaths;
};
export const getHCVaultSecretsForPath = async (
namespace: string,
secretPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
// Extract mount and path from the secretPath
// secretPath format: {mount}/{path}
const pathParts = secretPath.split("/");
const mountPath = pathParts[0];
const actualPath = pathParts.slice(1).join("/");
if (!mountPath || !actualPath) {
throw new BadRequestError({
message: "Invalid secret path format. Expected format: {mount}/{path}"
});
}
// Get mounts to determine KV version
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
const mount = mounts.find((m) => m.path.replace(/\/$/, "") === mountPath);
if (!mount) {
throw new BadRequestError({
message: `Mount '${mountPath}' not found in HashiCorp Vault`
});
}
const kvVersion = mount.version === "2" ? "2" : "1";
// Fetch secrets based on KV version
if (kvVersion === "2") {
// For KV v2: /v1/{mount}/data/{path}
const { data } = await requestWithHCVaultGateway<{
data: {
data: Record<string, string>; // KV v2 has nested data structure
metadata: {
created_time: string;
deletion_time: string;
destroyed: boolean;
version: number;
};
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${mountPath}/data/${actualPath}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data.data;
}
// For KV v1: /v1/{mount}/{path}
const { data } = await requestWithHCVaultGateway<{
data: Record<string, string>; // KV v1 has flat data structure
lease_duration: number;
lease_id: string;
renewable: boolean;
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${mountPath}/${actualPath}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data;
} catch (error: unknown) {
logger.error(error, "Unable to fetch secrets from HC Vault path");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to fetch secrets: ${error.message || "Unknown error"}`
});
}
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: "Unable to fetch secrets from HashiCorp Vault"
});
}
};
export const getHCVaultAuthMounts = async (
namespace: string,
authType: HCVaultAuthType | undefined,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultAuthMount[]> => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
const { data } = await requestWithHCVaultGateway<THCVaultAuthMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/auth`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
const authMounts: THCVaultAuthMount[] = [];
Object.entries(data.data).forEach(([path, authMethod]) => {
// If authType is specified, filter by it; otherwise, include all
if (!authType || authMethod.type === authType) {
authMounts.push({
path,
type: authMethod.type,
description: authMethod.description,
accessor: authMethod.accessor
});
}
});
return authMounts;
} catch (error: unknown) {
const authTypeStr = authType || "all";
logger.error(error, `Unable to list HC Vault ${authTypeStr} auth mounts`);
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list ${authTypeStr} auth mounts: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: `Unable to list ${authTypeStr} auth mounts from HashiCorp Vault`
});
}
};
export const getHCVaultKubernetesAuthRoles = async (
namespace: string,
mountPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultKubernetesAuthRoleWithConfig[]> => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
// Remove trailing slash from mount path
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
try {
// 1. Get the Kubernetes auth configuration for this mount
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthConfig }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/config`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const kubernetesConfig = configResponse.data;
// 2. List all roles in this mount
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role`,
method: "LIST",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const roleNames = roleListResponse.data.keys;
if (!roleNames || roleNames.length === 0) {
return [];
}
// 3. Fetch details for each role with concurrency control
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const roleDetailsPromises = roleNames.map((roleName) =>
limiter(async () => {
const { data: roleResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthRole }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role/${roleName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
// 4. Merge the role with the config
return {
...roleResponse.data,
name: roleName,
config: kubernetesConfig,
mountPath: cleanMountPath
} as THCVaultKubernetesAuthRoleWithConfig;
})
);
const roles = await Promise.all(roleDetailsPromises);
return roles;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault Kubernetes auth roles");
if (error instanceof AxiosError) {
const errorMessage =
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
throw new BadRequestError({
message: `Failed to list Kubernetes auth roles: ${errorMessage}`
});
}
throw new BadRequestError({
message: "Unable to list Kubernetes auth roles from HashiCorp Vault"
});
}
};
@@ -21,7 +21,8 @@ export const hcVaultConnectionService = (
try { try {
const mounts = await listHCVaultMounts(appConnection, gatewayService); const mounts = await listHCVaultMounts(appConnection, gatewayService);
return mounts; // Filter for KV version 2 mounts only and extract just the paths
return mounts.filter((mount) => mount.type === "kv" && mount.version === "2").map((mount) => mount.path);
} catch (error) { } catch (error) {
logger.error(error, "Failed to establish connection with Hashicorp Vault"); logger.error(error, "Failed to establish connection with Hashicorp Vault");
return []; return [];
@@ -33,3 +33,65 @@ export type THCVaultMountResponse = {
}; };
}; };
}; };
export type THCVaultMount = {
path: string;
type: string;
version?: string | null;
};
export type THCVaultAuthMountResponse = {
data: {
[key: string]: {
type: string;
description: string;
accessor: string;
config: {
default_lease_ttl: number;
max_lease_ttl: number;
force_no_cache: boolean;
};
local: boolean;
seal_wrap: boolean;
external_entropy_access: boolean;
options: Record<string, string> | null;
};
};
};
export type THCVaultAuthMount = {
path: string;
type: string;
description: string;
accessor: string;
};
export type THCVaultKubernetesAuthConfig = {
kubernetes_host: string;
kubernetes_ca_cert?: string;
issuer?: string;
disable_iss_validation?: boolean;
disable_local_ca_jwt?: boolean;
};
export type THCVaultKubernetesAuthRole = {
name: string;
bound_service_account_names: string[];
bound_service_account_namespaces: string[];
token_ttl?: number;
token_max_ttl?: number;
token_policies?: string[];
token_bound_cidrs?: string[];
token_explicit_max_ttl?: number;
token_no_default_policy?: boolean;
token_num_uses?: number;
token_period?: number;
token_type?: string;
audience?: string;
alias_name_source?: string;
};
export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole & {
config: THCVaultKubernetesAuthConfig;
mountPath: string;
};
@@ -0,0 +1,4 @@
export * from "./laravel-forge-connection-enums";
export * from "./laravel-forge-connection-fns";
export * from "./laravel-forge-connection-schemas";
export * from "./laravel-forge-connection-types";
@@ -0,0 +1,3 @@
export enum LaravelForgeConnectionMethod {
ApiToken = "api-token"
}
@@ -0,0 +1,165 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { LaravelForgeConnectionMethod } from "./laravel-forge-connection-enums";
import {
TLaravelForgeConnection,
TLaravelForgeConnectionConfig,
TLaravelForgeOrganization,
TLaravelForgeServer,
TLaravelForgeSite,
TRawLaravelForgeOrganization,
TRawLaravelForgeServer,
TRawLaravelForgeSite
} from "./laravel-forge-connection-types";
export const getLaravelForgeConnectionListItem = () => {
return {
name: "Laravel Forge" as const,
app: AppConnection.LaravelForge as const,
methods: Object.values(LaravelForgeConnectionMethod) as [LaravelForgeConnectionMethod.ApiToken]
};
};
export const validateLaravelForgeConnectionCredentials = async (config: TLaravelForgeConnectionConfig) => {
const { credentials: inputCredentials } = config;
try {
// Using the /api/me endpoint to validate the API token
await request.get(`${IntegrationUrls.LARAVELFORGE_API_URL}/api/me`, {
headers: {
Authorization: `Bearer ${inputCredentials.apiToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
});
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return inputCredentials;
};
type TLaravelForgeApiResponse<T> = {
data: T[];
links?: {
next?: string;
};
meta?: {
next_cursor?: string;
prev_cursor?: string | null;
};
};
const fetchAllPages = async <T>(
apiToken: string,
url: string,
params?: Record<string, string | number>
): Promise<T[]> => {
const allItems: T[] = [];
let nextUrl: string | null = url;
const queryParams = params || {};
while (nextUrl) {
try {
const response: { data: TLaravelForgeApiResponse<T> } = await request.get<TLaravelForgeApiResponse<T>>(nextUrl, {
params: queryParams,
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json",
"Content-Type": "application/json"
}
});
if (!response?.data?.data) {
throw new InternalServerError({
message: `Failed to fetch data from ${url}: Response was empty or malformed`
});
}
allItems.push(...response.data.data);
if (response.data.links?.next) {
nextUrl = response.data.links.next;
} else {
nextUrl = null;
}
} catch (error) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to fetch data from ${url}: ${error.message || "Unknown error"}`
});
}
throw error;
}
}
return allItems;
};
export const listLaravelForgeOrganizations = async (
appConnection: TLaravelForgeConnection
): Promise<TLaravelForgeOrganization[]> => {
const { credentials } = appConnection;
const { apiToken } = credentials;
const rawOrganizations = await fetchAllPages<TRawLaravelForgeOrganization>(
apiToken,
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs`
);
return rawOrganizations.map((org: TRawLaravelForgeOrganization) => ({
id: org.id,
name: org.attributes.name,
slug: org.attributes.slug
}));
};
export const listLaravelForgeServers = async (
appConnection: TLaravelForgeConnection,
organizationSlug: string
): Promise<TLaravelForgeServer[]> => {
const { credentials } = appConnection;
const { apiToken } = credentials;
const rawServers = await fetchAllPages<TRawLaravelForgeServer>(
apiToken,
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${organizationSlug}/servers`
);
return rawServers.map((server: TRawLaravelForgeServer) => ({
id: server.id,
name: server.attributes.name
}));
};
export const listLaravelForgeSites = async (
appConnection: TLaravelForgeConnection,
organizationSlug: string,
serverId: string
): Promise<TLaravelForgeSite[]> => {
const { credentials } = appConnection;
const { apiToken } = credentials;
const rawSites = await fetchAllPages<TRawLaravelForgeSite>(
apiToken,
`${IntegrationUrls.LARAVELFORGE_API_URL}/api/orgs/${organizationSlug}/servers/${serverId}/sites`
);
return rawSites.map((site: TRawLaravelForgeSite) => ({
id: site.id,
name: site.attributes.name
}));
};
@@ -0,0 +1,58 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { LaravelForgeConnectionMethod } from "./laravel-forge-connection-enums";
export const LaravelForgeConnectionApiTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API token required").describe(AppConnections.CREDENTIALS.LARAVEL_FORGE.apiToken)
});
const BaseLaravelForgeConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.LaravelForge) });
export const LaravelForgeConnectionSchema = BaseLaravelForgeConnectionSchema.extend({
method: z.literal(LaravelForgeConnectionMethod.ApiToken),
credentials: LaravelForgeConnectionApiTokenCredentialsSchema
});
export const SanitizedLaravelForgeConnectionSchema = z.discriminatedUnion("method", [
BaseLaravelForgeConnectionSchema.extend({
method: z.literal(LaravelForgeConnectionMethod.ApiToken),
credentials: LaravelForgeConnectionApiTokenCredentialsSchema.pick({})
})
]);
export const ValidateLaravelForgeConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(LaravelForgeConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.LaravelForge).method),
credentials: LaravelForgeConnectionApiTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.LaravelForge).credentials
)
})
]);
export const CreateLaravelForgeConnectionSchema = ValidateLaravelForgeConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LaravelForge)
);
export const UpdateLaravelForgeConnectionSchema = z
.object({
credentials: LaravelForgeConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.LaravelForge).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LaravelForge));
export const LaravelForgeConnectionListItemSchema = z.object({
name: z.literal("Laravel Forge"),
app: z.literal(AppConnection.LaravelForge),
methods: z.nativeEnum(LaravelForgeConnectionMethod).array()
});
@@ -0,0 +1,74 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
listLaravelForgeOrganizations,
listLaravelForgeServers,
listLaravelForgeSites
} from "./laravel-forge-connection-fns";
import {
TLaravelForgeConnection,
TLaravelForgeOrganization,
TLaravelForgeServer,
TLaravelForgeSite
} from "./laravel-forge-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TLaravelForgeConnection>;
export const laravelForgeConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listOrganizations = async (
connectionId: string,
actor: OrgServiceActor
): Promise<TLaravelForgeOrganization[]> => {
const appConnection = await getAppConnection(AppConnection.LaravelForge, connectionId, actor);
try {
const organizations = await listLaravelForgeOrganizations(appConnection);
return organizations;
} catch (error) {
logger.error(error, "Failed to list organizations for Laravel Forge connection");
return [];
}
};
const listServers = async (
connectionId: string,
actor: OrgServiceActor,
organizationSlug: string
): Promise<TLaravelForgeServer[]> => {
const appConnection = await getAppConnection(AppConnection.LaravelForge, connectionId, actor);
try {
const servers = await listLaravelForgeServers(appConnection, organizationSlug);
return servers;
} catch (error) {
logger.error(error, "Failed to list servers for Laravel Forge connection");
return [];
}
};
const listSites = async (
connectionId: string,
actor: OrgServiceActor,
organizationSlug: string,
serverId: string
): Promise<TLaravelForgeSite[]> => {
const appConnection = await getAppConnection(AppConnection.LaravelForge, connectionId, actor);
try {
const sites = await listLaravelForgeSites(appConnection, organizationSlug, serverId);
return sites;
} catch (error) {
logger.error(error, "Failed to list sites for Laravel Forge connection");
return [];
}
};
return {
listOrganizations,
listServers,
listSites
};
};
@@ -0,0 +1,63 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateLaravelForgeConnectionSchema,
LaravelForgeConnectionSchema,
ValidateLaravelForgeConnectionCredentialsSchema
} from "./laravel-forge-connection-schemas";
export type TLaravelForgeConnection = z.infer<typeof LaravelForgeConnectionSchema>;
export type TLaravelForgeConnectionInput = z.infer<typeof CreateLaravelForgeConnectionSchema> & {
app: AppConnection.LaravelForge;
};
export type TValidateLaravelForgeConnectionCredentialsSchema = typeof ValidateLaravelForgeConnectionCredentialsSchema;
export type TLaravelForgeConnectionConfig = DiscriminativePick<
TLaravelForgeConnectionInput,
"method" | "app" | "credentials"
> & {
orgSlug: string;
};
export type TLaravelForgeOrganization = {
id: string;
name: string;
slug: string;
};
export type TLaravelForgeServer = {
id: string;
name: string;
};
export type TLaravelForgeSite = {
id: string;
name: string;
};
export type TRawLaravelForgeOrganization = {
id: string;
attributes: {
name: string;
slug: string;
};
};
export type TRawLaravelForgeServer = {
id: string;
attributes: {
name: string;
};
};
export type TRawLaravelForgeSite = {
id: string;
attributes: {
name: string;
};
};
@@ -0,0 +1,164 @@
import { describe, expect, it } from "vitest";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { signatureAlgorithmToAlgCfg } from "./certificate-authority-fns";
describe("signatureAlgorithmToAlgCfg", () => {
describe("RSA algorithms", () => {
it("should handle RSA-SHA256 correctly", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", CertKeyAlgorithm.RSA_2048);
expect(result).toEqual({
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
});
});
it("should handle RSA-SHA384 correctly", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA384", CertKeyAlgorithm.RSA_4096);
expect(result).toEqual({
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-384",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 4096
});
});
it("should handle RSA-SHA256 with RSA_3072 correctly", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", CertKeyAlgorithm.RSA_3072);
expect(result).toEqual({
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 3072
});
});
it("should handle RSA-SHA512 correctly", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA512", CertKeyAlgorithm.RSA_2048);
expect(result).toEqual({
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-512",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 2048
});
});
});
describe("ECDSA algorithms", () => {
it("should handle ECDSA-SHA256 with P-256 curve", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", CertKeyAlgorithm.ECDSA_P256);
expect(result).toEqual({
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
});
});
it("should handle ECDSA-SHA384 with P-384 curve", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", CertKeyAlgorithm.ECDSA_P384);
expect(result).toEqual({
name: "ECDSA",
namedCurve: "P-384",
hash: "SHA-384"
});
});
it("should handle ECDSA-SHA256 with EC_prime256v1 string format", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_prime256v1");
expect(result).toEqual({
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
});
});
it("should handle ECDSA-SHA384 with EC_secp384r1 string format", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", "EC_secp384r1");
expect(result).toEqual({
name: "ECDSA",
namedCurve: "P-384",
hash: "SHA-384"
});
});
});
describe("hash format normalization", () => {
it("should normalize SHA256 to SHA-256", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", CertKeyAlgorithm.RSA_2048);
expect(result.hash).toBe("SHA-256");
});
it("should normalize SHA384 to SHA-384", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", CertKeyAlgorithm.ECDSA_P384);
expect(result.hash).toBe("SHA-384");
});
it("should normalize SHA512 to SHA-512", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA512", CertKeyAlgorithm.RSA_4096);
expect(result.hash).toBe("SHA-512");
});
it("should handle SHA1 format", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA1", CertKeyAlgorithm.RSA_2048);
expect(result.hash).toBe("SHA-1");
});
it("should handle SHA224 format", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA224", CertKeyAlgorithm.ECDSA_P256);
expect(result.hash).toBe("SHA-224");
});
it("should handle case insensitive hash normalization", () => {
const result = signatureAlgorithmToAlgCfg("RSA-sha256", CertKeyAlgorithm.RSA_2048);
expect(result.hash).toBe("SHA-256");
});
it("should handle already normalized hash formats", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", CertKeyAlgorithm.ECDSA_P256);
expect(result.hash).toBe("SHA-256");
});
it("should handle SHA-3 family hashes", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA3256", CertKeyAlgorithm.RSA_2048);
expect(result.hash).toBe("SHA3-256");
});
});
describe("dynamic key algorithm support", () => {
it("should support future RSA key sizes", () => {
const result = signatureAlgorithmToAlgCfg("RSA-SHA256", "RSA_8192");
expect(result.name).toBe("RSASSA-PKCS1-v1_5");
expect(result.hash).toBe("SHA-256");
});
it("should support future EC curves", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA256", "EC_secp521r1");
expect(result.name).toBe("ECDSA");
expect(result.namedCurve).toBe("P-521");
expect(result.hash).toBe("SHA-256");
});
it("should support EC_P384 string format", () => {
const result = signatureAlgorithmToAlgCfg("ECDSA-SHA384", "EC_P384");
expect(result).toEqual({
name: "ECDSA",
namedCurve: "P-384",
hash: "SHA-384"
});
});
});
});
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
@@ -68,6 +69,13 @@ export const parseDistinguishedName = (dn: string): TDNParts => {
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
switch (keyAlgorithm) { switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_3072:
return {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 3072
};
case CertKeyAlgorithm.RSA_4096: case CertKeyAlgorithm.RSA_4096:
return { return {
name: "RSASSA-PKCS1-v1_5", name: "RSASSA-PKCS1-v1_5",
@@ -99,6 +107,73 @@ 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();
if (upperHash === "SHA1" || upperHash === "SHA-1") return "SHA-1";
if (upperHash === "SHA224" || upperHash === "SHA-224") return "SHA-224";
if (upperHash === "SHA256" || upperHash === "SHA-256") return "SHA-256";
if (upperHash === "SHA384" || upperHash === "SHA-384") return "SHA-384";
if (upperHash === "SHA512" || upperHash === "SHA-512") return "SHA-512";
if (upperHash === "SHA3224" || upperHash === "SHA3-224") return "SHA3-224";
if (upperHash === "SHA3256" || upperHash === "SHA3-256") return "SHA3-256";
if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384";
if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512";
throw new Error(`Unsupported hash algorithm: ${hash}`);
};
const normalizedHash = hashType ? normalizeHashType(hashType) : undefined;
switch (keyType) {
case "RSA":
return {
name: "RSASSA-PKCS1-v1_5",
hash: normalizedHash || "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength:
keyAlgorithm === CertKeyAlgorithm.RSA_4096 ? 4096 : keyAlgorithm === CertKeyAlgorithm.RSA_3072 ? 3072 : 2048
};
case "ECDSA":
// eslint-disable-next-line no-case-declarations
const is384Curve =
keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384";
// eslint-disable-next-line no-case-declarations
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,
hash: normalizedHash || (namedCurve === "P-384" ? "SHA-384" : "SHA-256")
};
default:
// Fallback to key algorithm default
return keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
}
};
/** /**
* Return the public and private key of CA with id [caId] * Return the public and private key of CA with id [caId]
* Note: credentials are returned as crypto.webcrypto.CryptoKey * Note: credentials are returned as crypto.webcrypto.CryptoKey
@@ -111,7 +186,8 @@ export const getCaCredentials = async ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService kmsService,
signatureAlgorithm
}: TGetCaCredentialsDTO) => { }: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
@@ -132,7 +208,7 @@ export const getCaCredentials = async ({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const alg = signatureAlgorithm || keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const skObj = crypto.nativeCrypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); const skObj = crypto.nativeCrypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
const caPrivateKey = await crypto.nativeCrypto.subtle.importKey( const caPrivateKey = await crypto.nativeCrypto.subtle.importKey(
"pkcs8", "pkcs8",
@@ -18,7 +18,7 @@ export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick(
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
}); });
@@ -26,7 +26,7 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
}); });
@@ -32,6 +32,8 @@ import {
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertSignatureAlgorithm,
CertSignatureType,
CertStatus, CertStatus,
TAltNameMapping TAltNameMapping
} from "../../certificate/certificate-types"; } from "../../certificate/certificate-types";
@@ -48,7 +50,8 @@ import {
getCaCertChains, getCaCertChains,
getCaCredentials, getCaCredentials,
keyAlgorithmToAlgCfg, keyAlgorithmToAlgCfg,
parseDistinguishedName parseDistinguishedName,
signatureAlgorithmToAlgCfg
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue"; import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
@@ -1174,7 +1177,10 @@ export const internalCertificateAuthorityServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
signatureAlgorithm,
keyAlgorithm,
isFromProfile
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO) => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined; let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
@@ -1221,7 +1227,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa.activeCaCertId) if (!ca.internalCa.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" }); throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (!ca.enableDirectIssuance && !certificateTemplate) { if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" });
} }
@@ -1277,13 +1283,43 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
} }
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const effectiveKeyAlgorithm =
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); (keyAlgorithm as CertKeyAlgorithm) || (ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
const requestedKeyType = signatureAlgorithm.split("-")[0];
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
if (
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
: isEcdsaCa
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
: "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
}
// Determine signing algorithm for certificate signing
const signingAlg = signatureAlgorithm
? signatureAlgorithmToAlgCfg(signatureAlgorithm, ca.internalCa.keyAlgorithm as CertKeyAlgorithm)
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${commonName}`, name: `CN=${commonName}`,
keys: leafKeys, keys: leafKeys,
signingAlgorithm: alg, signingAlgorithm: keyGenAlg,
extensions: [ extensions: [
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
@@ -1296,7 +1332,8 @@ export const internalCertificateAuthorityServiceFactory = ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService kmsService,
signatureAlgorithm: signingAlg
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
@@ -1319,7 +1356,7 @@ export const internalCertificateAuthorityServiceFactory = ({
// handle key usages // handle key usages
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
if (keyUsages === undefined && !certificateTemplate) { if (keyUsages === undefined && !certificateTemplate) {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; selectedKeyUsages = isFromProfile ? [] : [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
} }
if (keyUsages === undefined && certificateTemplate) { if (keyUsages === undefined && certificateTemplate) {
@@ -1405,7 +1442,7 @@ export const internalCertificateAuthorityServiceFactory = ({
notAfter: notAfterDate, notAfter: notAfterDate,
signingKey: caPrivateKey, signingKey: caPrivateKey,
publicKey: csrObj.publicKey, publicKey: csrObj.publicKey,
signingAlgorithm: alg, signingAlgorithm: signingAlg,
extensions extensions
}); });
@@ -1517,7 +1554,9 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore, notBefore,
notAfter, notAfter,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
signatureAlgorithm,
keyAlgorithm
} = dto; } = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@@ -1563,7 +1602,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa.activeCaCertId) if (!ca.internalCa.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" }); throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (!ca.enableDirectIssuance && !certificateTemplate) { if (!dto.isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" });
} }
@@ -1622,7 +1661,29 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
} }
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
const isRsaCa = caKeyAlgorithm.startsWith(CertSignatureType.RSA);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertSignatureType.ECDSA);
if (
(requestedKeyType === CertSignatureType.RSA && !isRsaCa) ||
(requestedKeyType === CertSignatureType.ECDSA && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa ? CertSignatureType.RSA : isEcdsaCa ? CertSignatureType.ECDSA : "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
}
const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm;
const alg = signatureAlgorithm
? signatureAlgorithmToAlgCfg(signatureAlgorithm, effectiveKeyAlgorithm)
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr); const csrObj = new x509.Pkcs10CertificateRequest(csr);
@@ -1671,7 +1732,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (csrKeyUsageExtension) { if (csrKeyUsageExtension) {
selectedKeyUsages = csrKeyUsages; selectedKeyUsages = csrKeyUsages;
} else { } else {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; selectedKeyUsages = dto.isFromProfile ? [] : [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
} }
} }
@@ -3,7 +3,12 @@ import { z } from "zod";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -131,6 +136,9 @@ export type TIssueCertFromCaDTO = {
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: CertSignatureAlgorithm;
keyAlgorithm?: CertKeyAlgorithm;
isFromProfile?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = export type TSignCertFromCaDTO =
@@ -148,6 +156,9 @@ export type TSignCertFromCaDTO =
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: string;
keyAlgorithm?: string;
isFromProfile?: boolean;
} }
| ({ | ({
isInternal: false; isInternal: false;
@@ -163,6 +174,9 @@ export type TSignCertFromCaDTO =
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: string;
keyAlgorithm?: string;
isFromProfile?: boolean;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = { export type TGetCaCertificateTemplatesDTO = {
@@ -184,6 +198,7 @@ export type TGetCaCredentialsDTO = {
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
signatureAlgorithm?: RsaHashedImportParams | EcKeyImportParams;
}; };
export type TGetCaCertChainsDTO = { export type TGetCaCertChainsDTO = {
@@ -0,0 +1,187 @@
export enum CertSubjectAlternativeNameType {
DNS_NAME = "dns_name",
IP_ADDRESS = "ip_address",
EMAIL = "email",
URI = "uri"
}
export enum CertKeyUsageType {
DIGITAL_SIGNATURE = "digital_signature",
KEY_ENCIPHERMENT = "key_encipherment",
NON_REPUDIATION = "non_repudiation",
DATA_ENCIPHERMENT = "data_encipherment",
KEY_AGREEMENT = "key_agreement",
KEY_CERT_SIGN = "key_cert_sign",
CRL_SIGN = "crl_sign",
ENCIPHER_ONLY = "encipher_only",
DECIPHER_ONLY = "decipher_only"
}
export enum CertExtendedKeyUsageType {
CLIENT_AUTH = "client_auth",
SERVER_AUTH = "server_auth",
CODE_SIGNING = "code_signing",
EMAIL_PROTECTION = "email_protection",
OCSP_SIGNING = "ocsp_signing",
TIME_STAMPING = "time_stamping"
}
export enum CertIncludeType {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
}
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
DAYS = "days",
MONTHS = "months",
YEARS = "years"
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const mapKeyUsageToLegacy = (usage: CertKeyUsageType): string => {
switch (usage) {
case CertKeyUsageType.DIGITAL_SIGNATURE:
return "digitalSignature";
case CertKeyUsageType.KEY_ENCIPHERMENT:
return "keyEncipherment";
case CertKeyUsageType.NON_REPUDIATION:
return "nonRepudiation";
case CertKeyUsageType.DATA_ENCIPHERMENT:
return "dataEncipherment";
case CertKeyUsageType.KEY_AGREEMENT:
return "keyAgreement";
case CertKeyUsageType.KEY_CERT_SIGN:
return "keyCertSign";
case CertKeyUsageType.CRL_SIGN:
return "cRLSign";
case CertKeyUsageType.ENCIPHER_ONLY:
return "encipherOnly";
case CertKeyUsageType.DECIPHER_ONLY:
return "decipherOnly";
default:
return usage;
}
};
export const mapLegacyKeyUsageToStandard = (usage: string): CertKeyUsageType => {
switch (usage) {
case "digitalSignature":
case "digital_signature":
return CertKeyUsageType.DIGITAL_SIGNATURE;
case "keyEncipherment":
case "key_encipherment":
return CertKeyUsageType.KEY_ENCIPHERMENT;
case "nonRepudiation":
case "non_repudiation":
return CertKeyUsageType.NON_REPUDIATION;
case "dataEncipherment":
case "data_encipherment":
return CertKeyUsageType.DATA_ENCIPHERMENT;
case "keyAgreement":
case "key_agreement":
return CertKeyUsageType.KEY_AGREEMENT;
case "keyCertSign":
case "key_cert_sign":
return CertKeyUsageType.KEY_CERT_SIGN;
case "cRLSign":
case "crl_sign":
return CertKeyUsageType.CRL_SIGN;
case "encipherOnly":
case "encipher_only":
return CertKeyUsageType.ENCIPHER_ONLY;
case "decipherOnly":
case "decipher_only":
return CertKeyUsageType.DECIPHER_ONLY;
default:
throw new Error(`Unknown key usage: ${usage}`);
}
};
export const mapExtendedKeyUsageToLegacy = (usage: CertExtendedKeyUsageType): string => {
switch (usage) {
case CertExtendedKeyUsageType.CLIENT_AUTH:
return "clientAuth";
case CertExtendedKeyUsageType.SERVER_AUTH:
return "serverAuth";
case CertExtendedKeyUsageType.CODE_SIGNING:
return "codeSigning";
case CertExtendedKeyUsageType.EMAIL_PROTECTION:
return "emailProtection";
case CertExtendedKeyUsageType.OCSP_SIGNING:
return "ocspSigning";
case CertExtendedKeyUsageType.TIME_STAMPING:
return "timeStamping";
default:
return usage;
}
};
export const mapLegacyExtendedKeyUsageToStandard = (usage: string): CertExtendedKeyUsageType => {
switch (usage) {
case "clientAuth":
case "client_auth":
return CertExtendedKeyUsageType.CLIENT_AUTH;
case "serverAuth":
case "server_auth":
return CertExtendedKeyUsageType.SERVER_AUTH;
case "codeSigning":
case "code_signing":
return CertExtendedKeyUsageType.CODE_SIGNING;
case "emailProtection":
case "email_protection":
return CertExtendedKeyUsageType.EMAIL_PROTECTION;
case "ocspSigning":
case "ocsp_signing":
return CertExtendedKeyUsageType.OCSP_SIGNING;
case "timeStamping":
case "time_stamping":
return CertExtendedKeyUsageType.TIME_STAMPING;
default:
throw new Error(`Unknown extended key usage: ${usage}`);
}
};
export 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);
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
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);
@@ -0,0 +1,198 @@
import RE2 from "re2";
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
mapExtendedKeyUsageToLegacy,
mapKeyUsageToLegacy,
mapLegacyExtendedKeyUsageToStandard,
mapLegacyKeyUsageToStandard
} from "./certificate-constants";
interface CertificateRequestInput {
keyUsages?: string[];
extendedKeyUsages?: string[];
}
export const mapEnumsForValidation = <T extends CertificateRequestInput>(request: T): T => {
const mapKeyUsage = (usage: string): string => {
try {
return mapLegacyKeyUsageToStandard(usage);
} catch {
return usage;
}
};
const mapExtendedKeyUsage = (usage: string): string => {
try {
return mapLegacyExtendedKeyUsageToStandard(usage);
} catch {
return usage;
}
};
return {
...request,
keyUsages: request.keyUsages?.map(mapKeyUsage),
extendedKeyUsages: request.extendedKeyUsages?.map(mapExtendedKeyUsage)
} as T;
};
export const normalizeDateForApi = (date: Date | string | undefined): string | undefined => {
if (!date) return undefined;
return date instanceof Date ? date.toISOString() : date;
};
export const bufferToString = (data: Buffer | string): string => {
return String(data);
};
export const buildCertificateSubjectFromTemplate = (
request: Record<string, unknown>,
templateAttributes?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>
): Record<string, string | undefined> => {
const subject: Record<string, string> = {};
const attributeMap: Record<string, string> = {
common_name: "commonName",
organization: "organization",
country: "country"
};
if (!templateAttributes || templateAttributes.length === 0) {
return subject;
}
templateAttributes.forEach((attr) => {
const requestKey = attributeMap[attr.type];
const value = request[requestKey];
if (value && typeof value === "string" && (attr.allowed || attr.required)) {
subject[attr.type] = value;
}
});
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<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>
): string => {
if (!request.subjectAlternativeNames || request.subjectAlternativeNames.length === 0) {
return "";
}
if (!templateSans || templateSans.length === 0) {
return request.subjectAlternativeNames.map((san) => san.value).join(",");
}
const allowedSans: string[] = [];
request.subjectAlternativeNames.forEach((san) => {
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(",");
};
export const convertLegacyKeyUsage = (usage: CertKeyUsage): CertKeyUsageType => {
return mapLegacyKeyUsageToStandard(usage);
};
export const convertToLegacyKeyUsage = (usage: CertKeyUsageType): CertKeyUsage => {
return mapKeyUsageToLegacy(usage) as CertKeyUsage;
};
export const convertLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsage): CertExtendedKeyUsageType => {
return mapLegacyExtendedKeyUsageToStandard(usage);
};
export const convertToLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsageType): CertExtendedKeyUsage => {
return mapExtendedKeyUsageToLegacy(usage) as CertExtendedKeyUsage;
};
export const convertKeyUsageArrayFromLegacy = (usages?: CertKeyUsage[]): CertKeyUsageType[] | undefined => {
return usages?.map(convertLegacyKeyUsage);
};
export const convertKeyUsageArrayToLegacy = (usages?: CertKeyUsageType[]): CertKeyUsage[] | undefined => {
return usages?.map(convertToLegacyKeyUsage);
};
export const convertExtendedKeyUsageArrayFromLegacy = (
usages?: CertExtendedKeyUsage[]
): CertExtendedKeyUsageType[] | undefined => {
return usages?.map(convertLegacyExtendedKeyUsage);
};
export const convertExtendedKeyUsageArrayToLegacy = (
usages?: CertExtendedKeyUsageType[]
): CertExtendedKeyUsage[] | undefined => {
return usages?.map(convertToLegacyExtendedKeyUsage);
};
@@ -0,0 +1,737 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-bitwise */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { EnrollmentType } from "../certificate-profile/certificate-profile-types";
import { certificateEstV3ServiceFactory, TCertificateEstV3ServiceFactory } from "./certificate-est-v3-service";
// Mock the x509 module
vi.mock("@peculiar/x509", () => ({
Pkcs10CertificateRequest: vi.fn(),
GeneralNames: vi.fn(),
KeyUsagesExtension: vi.fn(),
ExtendedKeyUsageExtension: vi.fn(),
X509Certificate: vi.fn(),
KeyUsageFlags: {
digitalSignature: 1,
nonRepudiation: 2,
keyEncipherment: 4,
dataEncipherment: 8,
keyAgreement: 16,
keyCertSign: 32,
cRLSign: 64,
encipherOnly: 128,
decipherOnly: 256
}
}));
// Mock other dependencies
vi.mock("@app/services/certificate-authority/certificate-authority-fns", () => ({
parseDistinguishedName: vi.fn((subject: string) => {
const parts = subject.split(",");
const result: any = {};
parts.forEach((part) => {
const [key, value] = part.split("=");
switch (key.trim()) {
case "CN":
result.commonName = value;
break;
case "O":
result.organization = value;
break;
case "OU":
result.ou = value;
break;
case "L":
result.locality = value;
break;
case "ST":
result.province = value;
break;
case "C":
result.country = value;
break;
default:
break;
}
});
return result;
})
}));
vi.mock("@app/services/certificate-authority/certificate-authority-validators", () => ({
validateAndMapAltNameType: vi.fn((value: string) => {
if (value.includes(".") && !value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return { type: "dns", value };
}
if (value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return { type: "ip", value };
}
return null;
})
}));
vi.mock("@app/services/certificate-common/certificate-constants", () => ({
mapLegacyKeyUsageToStandard: vi.fn((usage: string) => {
const mapping: Record<string, string> = {
digitalSignature: "digital_signature",
keyEncipherment: "key_encipherment",
keyCertSign: "key_cert_sign"
};
return mapping[usage] || usage;
}),
mapLegacyExtendedKeyUsageToStandard: vi.fn((usage: string) => {
const mapping: Record<string, string> = {
clientAuth: "client_auth",
serverAuth: "server_auth",
codeSigning: "code_signing"
};
return mapping[usage] || usage;
}),
CertKeyUsageType: {
DIGITAL_SIGNATURE: "digital_signature",
KEY_ENCIPHERMENT: "key_encipherment",
KEY_CERT_SIGN: "key_cert_sign"
},
CertExtendedKeyUsageType: {
CLIENT_AUTH: "client_auth",
SERVER_AUTH: "server_auth",
CODE_SIGNING: "code_signing"
},
CertSubjectAlternativeNameType: {
DNS_NAME: "dns_name",
IP_ADDRESS: "ip_address",
RFC822_NAME: "rfc822_name",
UNIFORM_RESOURCE_IDENTIFIER: "uniform_resource_identifier"
}
}));
vi.mock("@app/services/certificate/certificate-types", () => ({
mapLegacyAltNameType: vi.fn((type: string) => {
const mapping: Record<string, string> = {
dns: "dns_name",
ip: "ip_address",
email: "rfc822_name",
url: "uniform_resource_identifier"
};
return mapping[type] || type;
}),
TAltNameType: {
EMAIL: "email",
DNS: "dns",
IP: "ip",
URL: "url"
},
CertExtendedKeyUsageOIDToName: {
"1.3.6.1.5.5.7.3.1": "serverAuth",
"1.3.6.1.5.5.7.3.2": "clientAuth",
"1.3.6.1.5.5.7.3.3": "codeSigning"
},
CertKeyUsage: {
DIGITAL_SIGNATURE: "digitalSignature",
KEY_ENCIPHERMENT: "keyEncipherment",
KEY_CERT_SIGN: "keyCertSign",
NON_REPUDIATION: "nonRepudiation",
DATA_ENCIPHERMENT: "dataEncipherment",
KEY_AGREEMENT: "keyAgreement",
CRL_SIGN: "cRLSign",
ENCIPHER_ONLY: "encipherOnly",
DECIPHER_ONLY: "decipherOnly"
},
CertExtendedKeyUsage: {
CLIENT_AUTH: "clientAuth",
SERVER_AUTH: "serverAuth",
CODE_SIGNING: "codeSigning"
}
}));
vi.mock("@app/services/certificate-common/certificate-utils", () => ({
mapEnumsForValidation: vi.fn((req: any) => req)
}));
vi.mock("../../ee/services/certificate-est/certificate-est-fns", () => ({
convertRawCertsToPkcs7: vi.fn(() => "mocked-pkcs7-response")
}));
describe("CertificateEstV3Service Security Fix", () => {
let service: TCertificateEstV3ServiceFactory;
const mockInternalCertificateAuthorityService = {
signCertFromCa: vi.fn()
};
const mockCertificateTemplateV2Service = {
validateCertificateRequest: vi.fn()
};
const mockCertificateAuthorityDAL = {
findById: vi.fn(),
findByIdWithAssociatedCa: vi.fn()
};
const mockCertificateAuthorityCertDAL = {
find: vi.fn(),
findById: vi.fn()
};
const mockProjectDAL = {
findOne: vi.fn(),
updateById: vi.fn(),
transaction: vi.fn()
};
const mockKmsService = {
decryptWithKmsKey: vi.fn(),
generateKmsKey: vi.fn()
};
const mockLicenseService = {
getPlan: vi.fn()
};
const mockCertificateProfileDAL = {
findByIdWithConfigs: vi.fn()
};
const mockEstEnrollmentConfigDAL = {
findById: vi.fn()
};
const mockProfile = {
id: "profile-123",
projectId: "project-123",
caId: "ca-123",
certificateTemplateId: "template-v2-123",
enrollmentType: EnrollmentType.EST,
estConfigId: "est-config-123"
};
const mockEstConfig = {
id: "est-config-123",
disableBootstrapCaValidation: true
};
const mockProject = {
id: "project-123",
orgId: "org-123"
};
const mockPlan = {
pkiEst: true
};
beforeEach(async () => {
const { Pkcs10CertificateRequest, GeneralNames } = await import("@peculiar/x509");
service = certificateEstV3ServiceFactory({
internalCertificateAuthorityService: mockInternalCertificateAuthorityService,
certificateTemplateV2Service: mockCertificateTemplateV2Service,
certificateAuthorityDAL: mockCertificateAuthorityDAL,
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
projectDAL: mockProjectDAL,
kmsService: mockKmsService,
licenseService: mockLicenseService,
certificateProfileDAL: mockCertificateProfileDAL,
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL
});
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
mockEstEnrollmentConfigDAL.findById.mockResolvedValue(mockEstConfig);
mockProjectDAL.findOne.mockResolvedValue(mockProject);
mockLicenseService.getPlan.mockResolvedValue(mockPlan);
// Set up the default CSR parsing behavior
(Pkcs10CertificateRequest as any).mockImplementation((csr: string) => {
const parsed = JSON.parse(csr);
const mockExtensions: any[] = [];
if (parsed.sans && parsed.sans.length > 0) {
mockExtensions.push({ type: "2.5.29.17", value: "mock-san-value" });
}
return {
subject: parsed.subject,
extensions: mockExtensions,
getExtension: vi.fn((oid: string) => {
if (oid === "2.5.29.15" && parsed.keyUsages && parsed.keyUsages.length > 0) {
// Calculate usages as bitwise OR of key usage flags
let usages = 0;
parsed.keyUsages.forEach((usage: string) => {
switch (usage) {
case "digital_signature":
usages |= 1; // KeyUsageFlags.digitalSignature
break;
case "key_encipherment":
usages |= 4; // KeyUsageFlags.keyEncipherment
break;
case "key_cert_sign":
usages |= 32; // KeyUsageFlags.keyCertSign
break;
default:
break;
}
});
return { usages };
}
if (oid === "2.5.29.37" && parsed.extendedKeyUsages && parsed.extendedKeyUsages.length > 0) {
const ekuOids = parsed.extendedKeyUsages.map((eku: string) => {
switch (eku) {
case "client_auth":
return "1.3.6.1.5.5.7.3.2";
case "server_auth":
return "1.3.6.1.5.5.7.3.1";
case "code_signing":
return "1.3.6.1.5.5.7.3.3";
default:
return "1.3.6.1.5.5.7.3.1";
}
});
return { usages: ekuOids };
}
return undefined;
})
};
});
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
}));
});
afterEach(() => {
vi.clearAllMocks();
});
const createMockCSR = (
options: {
subject?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
sans?: Array<{ type: string; value: string }>;
} = {}
) => {
const {
subject = "CN=test.example.com,O=Test Org,C=US",
keyUsages = [],
extendedKeyUsages = [],
sans = []
} = options;
return JSON.stringify({
subject,
keyUsages,
extendedKeyUsages,
sans
});
};
describe("CSR Extraction and Template Validation", () => {
it("should extract subject attributes from CSR", async () => {
const csr = createMockCSR({
subject: "CN=test.example.com,O=Test Organization,OU=IT Department,L=San Francisco,ST=California,C=US"
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
commonName: "test.example.com",
organization: "Test Organization",
organizationUnit: "IT Department",
locality: "San Francisco",
state: "California",
country: "US"
})
);
});
it("should extract key usages from CSR", async () => {
const csr = createMockCSR({
keyUsages: ["digital_signature", "key_encipherment"]
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
keyUsages: expect.arrayContaining(["digital_signature", "key_encipherment"])
})
);
});
it("should extract extended key usages from CSR", async () => {
const csr = createMockCSR({
extendedKeyUsages: ["client_auth", "server_auth"]
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
extendedKeyUsages: expect.arrayContaining(["client_auth", "server_auth"])
})
);
});
it("should extract Subject Alternative Names from CSR", async () => {
const { GeneralNames } = await import("@peculiar/x509");
const csr = createMockCSR({
sans: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
});
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
}));
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
subjectAlternativeNames: expect.arrayContaining([
expect.objectContaining({
type: "dns_name",
value: "test.example.com"
}),
expect.objectContaining({
type: "ip_address",
value: "192.168.1.1"
})
])
})
);
});
});
describe("Template Validation Enforcement", () => {
const basicCSR = createMockCSR();
it("should enforce template validation and reject invalid requests", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Common name 'test.example.com' is not allowed", "Key usage 'digital_signature' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
});
it("should allow valid requests that pass template validation", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.any(Object)
);
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
isInternal: true,
caId: "ca-123",
csr: basicCSR,
isFromProfile: true
});
});
it("should validate template for both simpleEnrollByProfile and simpleReenrollByProfile", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["SAN value 'evil.com' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalled();
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
});
});
describe("Policy Bypass Prevention", () => {
const maliciousCSR = createMockCSR({
subject: "CN=evil.com,O=Evil Corp,C=XX",
keyUsages: ["key_cert_sign"],
sans: [
{ type: "dns", value: "*.example.com" },
{ type: "ip", value: "127.0.0.1" }
]
});
it("should block attempts to bypass subject attribute policies", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Organization 'Evil Corp' is denied", "Country 'XX' is not allowed"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
commonName: "evil.com",
organization: "Evil Corp",
country: "XX"
})
);
});
it("should block attempts to bypass key usage policies", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Key usage 'key_cert_sign' is denied - certificate authority privileges not allowed"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
keyUsages: expect.arrayContaining(["key_cert_sign"])
})
);
});
it("should block attempts to bypass SAN policies", async () => {
const { GeneralNames } = await import("@peculiar/x509");
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "*.example.com" },
{ type: "ip", value: "127.0.0.1" }
]
}));
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["SAN value '*.example.com' matches denied wildcard pattern", "SAN value '127.0.0.1' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
});
});
describe("Error Handling", () => {
const basicCSR = createMockCSR();
it("should handle profile not found", async () => {
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(null);
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "nonexistent",
sslClientCert: ""
})
).rejects.toThrow(NotFoundError);
});
it("should handle non-EST enrollment type", async () => {
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue({
...mockProfile,
enrollmentType: EnrollmentType.API
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
});
it("should handle template validation service errors", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockRejectedValue(
new Error("Template validation service unavailable")
);
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow("Template validation service unavailable");
});
});
describe("Integration with existing flow", () => {
const basicCSR = createMockCSR();
beforeEach(() => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
});
it("should call internal CA service with correct parameters after validation", async () => {
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
isInternal: true,
caId: "ca-123",
isFromProfile: true,
csr: basicCSR
});
});
it("should use profile's CA ID instead of template ID to avoid v1/v2 mismatch", async () => {
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
// Verify it uses caId from profile, not certificateTemplateId
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
expect.objectContaining({
caId: "ca-123"
})
);
// Verify it does NOT pass certificateTemplateId to avoid v1/v2 confusion
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
expect.not.objectContaining({
certificateTemplateId: expect.anything()
})
);
});
});
});
@@ -0,0 +1,408 @@
import * as x509 from "@peculiar/x509";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import {
CertExtendedKeyUsageOIDToName,
CertKeyUsage,
mapLegacyAltNameType,
TAltNameMapping,
TAltNameType
} from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import {
getCaCertChain,
getCaCertChains,
parseDistinguishedName
} from "@app/services/certificate-authority/certificate-authority-fns";
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import {
mapLegacyExtendedKeyUsageToStandard,
mapLegacyKeyUsageToStandard
} from "@app/services/certificate-common/certificate-constants";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { convertRawCertsToPkcs7 } from "../../ee/services/certificate-est/certificate-est-fns";
import { TLicenseServiceFactory } from "../../ee/services/license/license-service";
type TCertificateEstV3ServiceFactoryDep = {
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
estEnrollmentConfigDAL: Pick<TEstEnrollmentConfigDALFactory, "findById">;
};
export type TCertificateEstV3ServiceFactory = ReturnType<typeof certificateEstV3ServiceFactory>;
export const certificateEstV3ServiceFactory = ({
internalCertificateAuthorityService,
certificateTemplateV2Service,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService,
certificateProfileDAL,
estEnrollmentConfigDAL
}: TCertificateEstV3ServiceFactoryDep) => {
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const subject = parseDistinguishedName(csrObj.subject);
const certificateRequest: TCertificateRequest = {
commonName: subject.commonName,
organization: subject.organization,
organizationUnit: subject.ou,
locality: subject.locality,
state: subject.province,
country: subject.country
};
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
if (csrKeyUsageExtension) {
const csrKeyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
);
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
}
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
if (csrExtendedKeyUsageExtension) {
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
);
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
}
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
const altNamesArray: TAltNameMapping[] = sanNames.items
.filter(
(value) =>
value.type === TAltNameType.EMAIL ||
value.type === TAltNameType.DNS ||
value.type === TAltNameType.IP ||
value.type === TAltNameType.URL
)
.map((name): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
}
return altNameType;
});
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
type: mapLegacyAltNameType(altName.type),
value: altName.value
}));
}
return certificateRequest;
};
const simpleEnrollByProfile = async ({
csr,
profileId,
sslClientCert
}: {
csr: string;
profileId: string;
sslClientCert: string;
}) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.disableBootstrapCaValidation) {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: profile.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = estConfig.encryptedCaChain
? (
await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
})
).toString()
: "";
const caCerts = extractX509CertFromChain(decryptedCaChain)?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
}
const certificateRequest = extractCertificateRequestFromCSR(csr);
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
});
}
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
caId: profile.caId,
csr,
isFromProfile: true
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleReenrollByProfile = async ({
csr,
profileId,
sslClientCert
}: {
csr: string;
profileId: string;
sslClientCert: string;
}) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
const caCertChains = await getCaCertChains({
caId: profile.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const certificateRequest = extractCertificateRequestFromCSR(csr);
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
});
}
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
caId: profile.caId,
csr,
isFromProfile: true
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const getCaCertsByProfile = async ({ profileId }: { profileId: string }) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan."
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca?.internalCa?.id) {
throw new NotFoundError({
message: `Internal Certificate Authority with ID '${profile.caId}' not found`
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
let certificates: x509.X509Certificate[] = [];
if (caCertChain && caCertChain.trim()) {
try {
certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
} catch (error) {
certificates = [];
}
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnrollByProfile,
simpleReenrollByProfile,
getCaCertsByProfile
};
};
@@ -0,0 +1,552 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
EnrollmentType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
} from "./certificate-profile-types";
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
export const certificateProfileDALFactory = (db: TDbClient) => {
const certificateProfileOrm = ormify(db, TableName.PkiCertificateProfile);
const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise<TCertificateProfile> => {
try {
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): Promise<TCertificateProfile> => {
try {
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ id })
.update(data)
.returning("*")) as [TCertificateProfile];
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Update certificate profile" });
}
};
const deleteById = async (id: string, tx?: Knex): Promise<TCertificateProfile> => {
try {
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): Promise<TCertificateProfile | undefined> => {
try {
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): Promise<TCertificateProfileWithConfigs | undefined> => {
try {
const query = (tx || db)(TableName.PkiCertificateProfile)
.leftJoin(
TableName.CertificateAuthority,
`${TableName.PkiCertificateProfile}.caId`,
`${TableName.CertificateAuthority}.id`
)
.leftJoin(
TableName.PkiCertificateTemplateV2,
`${TableName.PkiCertificateProfile}.certificateTemplateId`,
`${TableName.PkiCertificateTemplateV2}.id`
)
.leftJoin(
TableName.PkiEstEnrollmentConfig,
`${TableName.PkiCertificateProfile}.estConfigId`,
`${TableName.PkiEstEnrollmentConfig}.id`
)
.leftJoin(
TableName.PkiApiEnrollmentConfig,
`${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.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();
const result = await query;
if (!result) return undefined;
const estConfig =
result.estConfigId && result.estConfigHashedPassphrase
? ({
id: result.estConfigId,
disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation,
passphrase: result.estConfigHashedPassphrase,
caChain: result.estConfigEncryptedCaChain ? result.estConfigEncryptedCaChain.toString("utf8") : ""
} 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
): Promise<TCertificateProfile | undefined> => {
try {
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" });
}
};
const findByProjectId = async (
projectId: string,
options: {
offset?: number;
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
} = {},
tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
try {
const {
offset = 0,
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 7
} = options;
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
`${TableName.PkiCertificateProfile}.projectId`,
projectId
);
if (search) {
baseQuery = baseQuery.where((builder) => {
void builder.where((qb) => {
void qb
.whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`)
.orWhereILike(`${TableName.PkiCertificateProfile}.description`, `%${search}%`);
});
});
}
if (enrollmentType) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType);
}
if (caId) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
}
let query = baseQuery
.leftJoin(
TableName.PkiEstEnrollmentConfig,
`${TableName.PkiCertificateProfile}.estConfigId`,
`${TableName.PkiEstEnrollmentConfig}.id`
)
.leftJoin(
TableName.PkiApiEnrollmentConfig,
`${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id`
)
.select(selectAllTableCols(TableName.PkiCertificateProfile))
.select(
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays")
);
if (includeMetrics) {
query = query.leftJoin(
TableName.Certificate,
`${TableName.PkiCertificateProfile}.id`,
`${TableName.Certificate}.profileId`
);
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
query = query
.select(
selectAllTableCols(TableName.PkiCertificateProfile),
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays"),
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',
[expiringDate]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
[now]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.groupBy(
`${TableName.PkiCertificateProfile}.id`,
`${TableName.PkiEstEnrollmentConfig}.id`,
`${TableName.PkiApiEnrollmentConfig}.id`
);
}
const results = (await query
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset)
.limit(limit)) as Record<string, unknown>[];
return results.map((result: Record<string, unknown>) => {
const estConfig =
result.estId && result.estHashedPassphrase
? {
id: result.estId as string,
disableBootstrapCaValidation: !!result.estDisableBootstrapCaValidation,
passphrase: result.estConfigHashedPassphrase,
caChain: result.estEncryptedCaChain ? (result.estEncryptedCaChain as Buffer).toString("utf8") : ""
}
: undefined;
const apiConfig = result.apiId
? {
id: result.apiId as string,
autoRenew: !!result.apiAutoRenew,
autoRenewDays: (result.apiAutoRenewDays as number) || undefined
}
: undefined;
const baseProfile = {
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
};
if (includeMetrics) {
return {
...baseProfile,
total_certificates: result.total_certificates,
active_certificates: result.active_certificates,
expired_certificates: result.expired_certificates,
expiring_certificates: result.expiring_certificates,
revoked_certificates: result.revoked_certificates
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
}
return baseProfile as TCertificateProfileWithConfigs;
});
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
}
};
const countByProjectId = async (
projectId: string,
options: {
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
} = {},
tx?: Knex
): Promise<number> => {
try {
const { search, enrollmentType, caId } = options;
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.where((qb) => {
void qb.whereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`);
});
});
}
if (enrollmentType) {
query = query.where({ enrollmentType });
}
if (caId) {
query = query.where({ caId });
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count certificate profiles by project id" });
}
};
const findByNameAndProjectId = async (
name: string,
projectId: string,
tx?: Knex
): Promise<TCertificateProfile | undefined> => {
try {
const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ slug: name, projectId })
.first()) as TCertificateProfile | undefined;
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
}
};
const getCertificatesByProfile = async (
profileId: string,
options: {
offset?: number;
limit?: number;
status?: "active" | "expired" | "revoked";
search?: string;
} = {},
tx?: Knex
): Promise<TCertificateProfileCertificate[]> => {
try {
const { offset = 0, limit = 20, status, search } = options;
const now = new Date();
let query = (tx || db)(TableName.Certificate).where("profileId", profileId);
if (search) {
query = query.where((builder) => {
void builder.where((qb) => {
void qb.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`);
});
});
}
if (status) {
switch (status) {
case "active":
query = query.where("notAfter", ">", now).whereNull("revokedAt");
break;
case "expired":
query = query.where("notAfter", "<=", now).whereNull("revokedAt");
break;
case "revoked":
query = query.whereNotNull("revokedAt");
break;
default:
break;
}
}
const certificates = await query
.select((tx || db).ref("id").withSchema(TableName.Certificate))
.select((tx || db).ref("serialNumber").withSchema(TableName.Certificate))
.select((tx || db).ref("cn").withSchema(TableName.Certificate))
.select((tx || db).ref("status").withSchema(TableName.Certificate))
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
.select((tx || db).ref("revokedAt").withSchema(TableName.Certificate))
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
.orderBy("createdAt", "desc")
.offset(offset)
.limit(limit);
return certificates.map((cert) => ({
...cert,
revokedAt: cert.revokedAt ?? null
}));
} catch (error) {
throw new DatabaseError({ error, name: "Get certificates by profile" });
}
};
const getProfileMetrics = async (
profileId: string,
expiringDays: number = 7,
tx?: Knex
): Promise<TCertificateProfileMetrics> => {
try {
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
const metrics = await (tx || db)(TableName.Certificate)
.where("profileId", profileId)
.select(
db.raw("COUNT(*) as total_certificates"),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
expiringDate
]),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
db.raw(
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.first();
return {
profileId,
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
};
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
}
};
const isProfileInUse = async (profileId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
return parseInt((doc as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Check if certificate profile is in use" });
}
};
return {
...certificateProfileOrm,
create,
updateById,
deleteById,
findById,
findByIdWithConfigs,
findBySlugAndProjectId,
findByProjectId,
countByProjectId,
findByNameAndProjectId,
getCertificatesByProfile,
getProfileMetrics,
isProfileInUse
};
};
@@ -0,0 +1,134 @@
import RE2 from "re2";
import { z } from "zod";
import { EnrollmentType } from "./certificate-profile-types";
export const createCertificateProfileSchema = z
.object({
projectId: z.string().uuid("Project ID must be valid"),
caId: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1),
encryptedCaChain: z.string()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
}
);
export const updateCertificateProfileSchema = z
.object({
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
.optional(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1),
encryptedCaChain: z.string()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
}
return true;
},
{
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
}
);
export const getCertificateProfileByIdSchema = z.object({
id: z.string().uuid()
});
export const getCertificateProfileBySlugSchema = z.object({
projectId: z.string().uuid("Project ID must be valid"),
slug: z.string().min(1)
});
export const listCertificateProfilesSchema = z.object({
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(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
caId: z.string().uuid().optional()
});
export const deleteCertificateProfileSchema = z.object({
id: z.string().uuid()
});
export const listCertificatesByProfileSchema = z.object({
profileId: z.string().uuid(),
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()
});
export const getCertificateProfileMetricsSchema = z.object({
profileId: z.string().uuid(),
expiringDays: z.coerce.number().min(1).max(365).default(30)
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,824 @@
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateTemplateV2DALFactory } from "../certificate-template-v2/certificate-template-v2-dal";
import { TApiEnrollmentConfigDALFactory } from "../enrollment-config/api-enrollment-config-dal";
import { TApiConfigData, TEstConfigData } from "../enrollment-config/enrollment-config-types";
import { TEstEnrollmentConfigDALFactory } from "../enrollment-config/est-enrollment-config-dal";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
import { TCertificateProfileDALFactory } from "./certificate-profile-dal";
import {
EnrollmentType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
} from "./certificate-profile-types";
const validateAndEncryptPemCaChain = async (
caChain: string,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey">,
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">
) => {
try {
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
if (!certificates || certificates.length === 0) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
if (!(await isCertChainValid(certificates))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob } = await kmsEncryptor({
plainText: Buffer.from(caChain)
});
return { encryptedCaChain: cipherTextBlob };
} catch (error) {
throw new BadRequestError({ message: `Failed to process certificate chain: ${(error as Error).message}` });
}
};
const decryptCaChain = async (
encryptedCaChain: Buffer,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey">,
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">
): Promise<string> => {
try {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = await kmsDecryptor({
cipherTextBlob: encryptedCaChain
});
return decryptedCaChain.toString();
} catch (error) {
throw new BadRequestError({ message: `Failed to decrypt certificate chain: ${(error as Error).message}` });
}
};
export type TCertificateProfileCreateData = Omit<TCertificateProfileInsert, "estConfigId" | "apiConfigId"> & {
estConfig?: TEstConfigData;
apiConfig?: TApiConfigData;
};
type TCertificateProfileServiceFactoryDep = {
certificateProfileDAL: TCertificateProfileDALFactory;
certificateTemplateV2DAL: TCertificateTemplateV2DALFactory;
apiEnrollmentConfigDAL: TApiEnrollmentConfigDALFactory;
estEnrollmentConfigDAL: TEstEnrollmentConfigDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
};
export type TCertificateProfileServiceFactory = ReturnType<typeof certificateProfileServiceFactory>;
const convertDalToService = (dalResult: Record<string, unknown>): TCertificateProfile => {
return {
...dalResult,
enrollmentType: dalResult.enrollmentType as EnrollmentType
} as TCertificateProfile;
};
export const certificateProfileServiceFactory = ({
certificateProfileDAL,
certificateTemplateV2DAL,
apiEnrollmentConfigDAL,
estEnrollmentConfigDAL,
permissionService,
kmsService,
projectDAL
}: TCertificateProfileServiceFactoryDep) => {
const createProfile = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
data: Omit<TCertificateProfileCreateData, "projectId">;
}): Promise<TCertificateProfile> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionSub.CertificateProfiles
);
// Validate that certificate template exists and belongs to the same project
if (data.certificateTemplateId) {
const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
if (template.projectId !== projectId) {
throw new ForbiddenRequestError({
message: "Certificate template must belong to the same project"
});
}
}
// Check for slug uniqueness within project
const existingSlugProfile = await certificateProfileDAL.findBySlugAndProjectId(data.slug, projectId);
if (existingSlugProfile) {
throw new ForbiddenRequestError({
message: "Certificate profile with this name already exists in project"
});
}
// Validate enrollment configuration requirements
if (data.enrollmentType === EnrollmentType.EST && !data.estConfig) {
throw new ForbiddenRequestError({
message: "EST enrollment requires EST configuration"
});
}
if (data.enrollmentType === EnrollmentType.API && !data.apiConfig) {
throw new ForbiddenRequestError({
message: "API enrollment requires API configuration"
});
}
// Create enrollment configs and profile
const profile = await certificateProfileDAL.transaction(async (tx) => {
let estConfigId: string | null = null;
let apiConfigId: string | null = null;
if (data.enrollmentType === EnrollmentType.EST && data.estConfig) {
const appCfg = getConfig();
// Hash the passphrase
const hashedPassphrase = await crypto.hashing().createHash(data.estConfig.passphrase, appCfg.SALT_ROUNDS);
let encryptedCaChainBuffer: Buffer | null = null;
if (!data.estConfig.disableBootstrapCaValidation && data.estConfig.caChain) {
const { encryptedCaChain } = await validateAndEncryptPemCaChain(
data.estConfig.caChain,
projectId,
kmsService,
projectDAL
);
encryptedCaChainBuffer = encryptedCaChain;
}
const estConfig = await estEnrollmentConfigDAL.create(
{
disableBootstrapCaValidation: data.estConfig.disableBootstrapCaValidation,
hashedPassphrase,
encryptedCaChain: encryptedCaChainBuffer
},
tx
);
estConfigId = estConfig.id;
} else if (data.enrollmentType === EnrollmentType.API && data.apiConfig) {
const apiConfig = await apiEnrollmentConfigDAL.create(
{
autoRenew: data.apiConfig.autoRenew,
autoRenewDays: data.apiConfig.autoRenewDays
},
tx
);
apiConfigId = apiConfig.id;
}
// Create the profile with the created config IDs
const { estConfig, apiConfig, ...profileData } = data;
const profileResult = await certificateProfileDAL.create(
{
...profileData,
projectId,
estConfigId,
apiConfigId
},
tx
);
return profileResult;
});
return convertDalToService(profile);
};
const updateProfile = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
data: TCertificateProfileUpdate;
}): Promise<TCertificateProfile> => {
const existingProfile = await certificateProfileDAL.findById(profileId);
if (!existingProfile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: existingProfile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionSub.CertificateProfiles
);
if (data.certificateTemplateId) {
const template = await certificateTemplateV2DAL.findById(data.certificateTemplateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
if (template.projectId !== existingProfile.projectId) {
throw new ForbiddenRequestError({
message: "Certificate template must belong to the same project"
});
}
}
if (data.slug && data.slug !== existingProfile.slug) {
const conflictingProfile = await certificateProfileDAL.findBySlugAndProjectId(
data.slug,
existingProfile.projectId
);
if (conflictingProfile && conflictingProfile.id !== profileId) {
throw new ForbiddenRequestError({
message: "Certificate profile with this name already exists in project"
});
}
}
const { estConfig, apiConfig, ...profileUpdateData } = data;
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
if (estConfig && existingProfile.estConfigId) {
const updateData: {
disableBootstrapCaValidation: boolean;
hashedPassphrase?: string;
encryptedCaChain?: Buffer;
} = {
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation ?? false
};
if (estConfig.passphrase) {
updateData.hashedPassphrase = await crypto
.hashing()
.createHash(estConfig.passphrase, getConfig().SALT_ROUNDS);
}
if (estConfig.caChain) {
const { encryptedCaChain } = await validateAndEncryptPemCaChain(
estConfig.caChain,
existingProfile.projectId,
kmsService,
projectDAL
);
updateData.encryptedCaChain = encryptedCaChain;
}
await estEnrollmentConfigDAL.updateById(existingProfile.estConfigId, updateData, tx);
}
if (apiConfig && existingProfile.apiConfigId) {
await apiEnrollmentConfigDAL.updateById(
existingProfile.apiConfigId,
{
autoRenew: apiConfig.autoRenew,
autoRenewDays: apiConfig.autoRenewDays
},
tx
);
}
const profileResult = await certificateProfileDAL.updateById(profileId, profileUpdateData, tx);
return profileResult;
});
return convertDalToService(updatedProfile);
};
const getProfileById = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId,
includeMetrics = false,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
const profile = await certificateProfileDAL.findById(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
);
const converted = convertDalToService(profile);
if (includeMetrics) {
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
return {
...converted,
metrics
};
}
return converted;
};
const getProfileByIdWithConfigs = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
}): Promise<TCertificateProfileWithConfigs> => {
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.estConfig && profile.estConfig.caChain) {
try {
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId!);
if (estConfig && estConfig.encryptedCaChain) {
const decryptedCaChain = await decryptCaChain(
estConfig.encryptedCaChain,
profile.projectId,
kmsService,
projectDAL
);
profile.estConfig.caChain = decryptedCaChain;
} else {
profile.estConfig.caChain = "";
}
} catch (error) {
profile.estConfig.caChain = "";
}
}
return {
...profile,
enrollmentType: profile.enrollmentType as EnrollmentType
};
};
const getProfileBySlug = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
slug
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
slug: string;
}): Promise<TCertificateProfile> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
const profile = await certificateProfileDAL.findBySlugAndProjectId(slug, projectId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
return convertDalToService(profile);
};
const listProfiles = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
offset = 0,
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
offset?: number;
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
}): Promise<{
profiles: (TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics })[];
totalCount: number;
}> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionSub.CertificateProfiles
);
const profiles = await certificateProfileDAL.findByProjectId(projectId, {
offset,
limit,
search,
enrollmentType,
caId,
includeMetrics,
expiringDays
});
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
search,
enrollmentType,
caId
});
const convertedProfiles = await Promise.all(
profiles.map(async (profile) => {
const profileWithConfigs = profile as TCertificateProfileWithConfigs;
let decryptedEstConfig = profileWithConfigs.estConfig;
if (decryptedEstConfig && profileWithConfigs.estConfigId) {
try {
const estConfig = await estEnrollmentConfigDAL.findById(profileWithConfigs.estConfigId);
if (estConfig && estConfig.encryptedCaChain) {
const decryptedCaChain = await decryptCaChain(
estConfig.encryptedCaChain,
projectId,
kmsService,
projectDAL
);
decryptedEstConfig = {
...decryptedEstConfig,
caChain: decryptedCaChain
};
} else if (decryptedEstConfig) {
decryptedEstConfig = {
...decryptedEstConfig,
caChain: ""
};
}
} catch (error) {
if (decryptedEstConfig) {
decryptedEstConfig = {
...decryptedEstConfig,
caChain: ""
};
}
}
}
const converted = convertDalToService(profileWithConfigs);
let result: TCertificateProfileWithConfigs & { metrics?: TCertificateProfileMetrics } = {
...converted,
estConfig: decryptedEstConfig,
apiConfig: profileWithConfigs.apiConfig
};
if (includeMetrics) {
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
result = {
...result,
metrics: {
profileId: converted.id,
totalCertificates: parseInt(String(profileWithMetrics.total_certificates || 0), 10),
activeCertificates: parseInt(String(profileWithMetrics.active_certificates || 0), 10),
expiredCertificates: parseInt(String(profileWithMetrics.expired_certificates || 0), 10),
expiringCertificates: parseInt(String(profileWithMetrics.expiring_certificates || 0), 10),
revokedCertificates: parseInt(String(profileWithMetrics.revoked_certificates || 0), 10)
}
};
}
return result;
})
);
return {
profiles: convertedProfiles,
totalCount
};
};
const deleteProfile = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
}): Promise<TCertificateProfile> => {
const profile = await certificateProfileDAL.findById(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.Delete,
ProjectPermissionSub.CertificateProfiles
);
const deletedProfile = await certificateProfileDAL.deleteById(profileId);
if (!deletedProfile) {
throw new NotFoundError({ message: "Failed to delete certificate profile" });
}
return convertDalToService(deletedProfile);
};
const getProfileCertificates = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId,
offset = 0,
limit = 20,
status,
search
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
offset?: number;
limit?: number;
status?: "active" | "expired" | "revoked";
search?: string;
}): Promise<TCertificateProfileCertificate[]> => {
const profile = await certificateProfileDAL.findById(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
);
const certificates = await certificateProfileDAL.getCertificatesByProfile(profileId, {
offset,
limit,
status,
search
});
return certificates;
};
const getProfileMetrics = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
profileId,
expiringDays = 30
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
profileId: string;
expiringDays?: number;
}): Promise<TCertificateProfileMetrics> => {
const profile = await certificateProfileDAL.findById(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
);
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
return metrics;
};
const getEstConfigurationByProfile = async (
params:
| {
profileId: string;
isInternal: true;
}
| {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
profileId: string;
isInternal?: false;
}
) => {
const { profileId, isInternal = false } = params;
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (!isInternal) {
const { actor, actorId, actorAuthMethod, actorOrgId } = params as {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
};
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.estConfig) {
throw new NotFoundError({ message: "EST configuration not found for this profile" });
}
return {
orgId: profile.projectId,
isEnabled: true,
caChain: profile.estConfig.caChain,
disableBootstrapCertValidation: profile.estConfig.disableBootstrapCaValidation,
hashedPassphrase: profile.estConfig.passphrase
};
};
return {
createProfile,
updateProfile,
getProfileById,
getProfileByIdWithConfigs,
getProfileBySlug,
listProfiles,
deleteProfile,
getProfileCertificates,
getProfileMetrics,
getEstConfigurationByProfile
};
};
@@ -0,0 +1,86 @@
import {
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate
} from "@app/db/schemas/pki-certificate-profiles";
export enum EnrollmentType {
API = "api",
EST = "est"
}
export type TCertificateProfile = Omit<TPkiCertificateProfiles, "enrollmentType"> & {
enrollmentType: EnrollmentType;
};
export type TCertificateProfileInsert = Omit<TPkiCertificateProfilesInsert, "enrollmentType"> & {
enrollmentType: EnrollmentType;
};
export type TCertificateProfileUpdate = Omit<TPkiCertificateProfilesUpdate, "enrollmentType"> & {
enrollmentType?: EnrollmentType;
estConfig?: {
disableBootstrapCaValidation?: boolean;
passphrase?: string;
caChain?: string;
};
apiConfig?: {
autoRenew?: boolean;
autoRenewDays?: number;
};
};
export type TCertificateProfileWithConfigs = TCertificateProfile & {
certificateAuthority?: {
id: string;
projectId: string;
status: string;
name: string;
};
certificateTemplate?: {
id: string;
projectId: string;
name: string;
description?: string;
};
estConfig?: {
id: string;
disableBootstrapCaValidation: boolean;
passphrase: string;
caChain: string;
};
apiConfig?: {
id: string;
autoRenew: boolean;
autoRenewDays?: number;
};
metrics?: TCertificateProfileMetrics;
};
export interface TCertificateProfileMetrics {
profileId: string;
totalCertificates: number;
activeCertificates: number;
expiredCertificates: number;
expiringCertificates: number;
revokedCertificates: number;
}
export interface TCertificateProfileCertificate {
id: string;
serialNumber: string;
cn: string;
status: string;
notBefore: Date;
notAfter: Date;
revokedAt: Date | null;
createdAt: Date;
}
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
total_certificates?: string;
active_certificates?: string;
expired_certificates?: string;
expiring_certificates?: string;
revoked_certificates?: string;
};
@@ -0,0 +1,244 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TPkiCertificateTemplatesV2Insert } from "@app/db/schemas/pki-certificate-templates-v2";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import {
TCertificateTemplateV2,
TCertificateTemplateV2Insert,
TCertificateTemplateV2Update
} from "./certificate-template-v2-types";
export type TCertificateTemplateV2DALFactory = ReturnType<typeof certificateTemplateV2DALFactory>;
interface CountResult {
count: string;
}
export const certificateTemplateV2DALFactory = (db: TDbClient) => {
const certificateTemplateV2Orm = ormify(db, TableName.PkiCertificateTemplateV2);
const serializeJsonFields = (data: TCertificateTemplateV2Insert | TCertificateTemplateV2Update) => {
const serialized = { ...data } as Record<string, unknown>;
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
jsonFields.forEach((field) => {
const value = serialized[field];
if (value !== undefined && typeof value !== "string") {
serialized[field] = JSON.stringify(value);
}
});
return serialized;
};
const parseJsonFields = (raw: Record<string, unknown>): TCertificateTemplateV2 => {
const jsonFields = ["subject", "sans", "keyUsages", "extendedKeyUsages", "algorithms", "validity"];
const parsed = { ...raw } as Record<string, unknown>;
jsonFields.forEach((field) => {
const value = raw[field];
if (value !== null && value !== undefined) {
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;
}
});
return parsed as TCertificateTemplateV2;
};
const create = async (data: TCertificateTemplateV2Insert, tx?: Knex) => {
try {
const serializedData = serializeJsonFields(data);
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");
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Create certificate template v2" });
}
};
const updateById = async (id: string, data: TCertificateTemplateV2Update, tx?: Knex) => {
try {
const serializedData = serializeJsonFields(data);
const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ id })
.update(serializedData)
.returning("*")) as Record<string, unknown>[];
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Update certificate template v2" });
}
};
const deleteById = async (id: string, tx?: Knex) => {
try {
const [certificateTemplateV2] = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ id })
.del()
.returning("*")) as Record<string, unknown>[];
return certificateTemplateV2;
} catch (error) {
throw new DatabaseError({ error, name: "Delete certificate template v2" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2).where({ id }).first()) as
| Record<string, unknown>
| undefined;
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate template v2 by id" });
}
};
const findByProjectId = async (
projectId: string,
options: {
offset?: number;
limit?: number;
search?: string;
} = {},
tx?: Knex
) => {
try {
const { offset = 0, limit = 20, search } = options;
let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
const certificateTemplatesV2 = await query.orderBy("createdAt", "desc").offset(offset).limit(limit);
return certificateTemplatesV2.map((template: Record<string, unknown>) => parseJsonFields(template));
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate templates v2 by project id" });
}
};
const countByProjectId = async (
projectId: string,
options: {
search?: string;
} = {},
tx?: Knex
) => {
try {
const { search } = options;
let query = (tx || db)(TableName.PkiCertificateTemplateV2).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
});
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count certificate templates v2 by project id" });
}
};
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
try {
const certificateTemplateV2 = (await (tx || db)(TableName.PkiCertificateTemplateV2)
.where({ name, projectId })
.first()) as Record<string, unknown> | undefined;
if (!certificateTemplateV2) {
return null;
}
return parseJsonFields(certificateTemplateV2);
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" });
}
};
const isTemplateInUse = async (templateId: string, tx?: Knex) => {
try {
const profileCount = await (tx || db)(TableName.PkiCertificateProfile)
.where({ certificateTemplateId: templateId })
.count("*")
.first();
const profileUsage = parseInt((profileCount as unknown as CountResult).count || "0", 10) > 0;
const certCount = await (tx || db)(TableName.Certificate)
.where({ certificateTemplateId: templateId })
.count("*")
.first();
const certUsage = parseInt((certCount as unknown as CountResult).count || "0", 10) > 0;
return profileUsage || certUsage;
} catch (error) {
throw new DatabaseError({ error, name: "Check if certificate template v2 is in use" });
}
};
const getProfilesUsingTemplate = async (templateId: string, tx?: Knex) => {
try {
const profiles = await (tx || db)(TableName.PkiCertificateProfile)
.select("id", "slug", "description")
.where({ certificateTemplateId: templateId });
return profiles as Array<{ id: string; slug: string; description?: string }>;
} catch (error) {
throw new DatabaseError({ error, name: "Get profiles using certificate template v2" });
}
};
return {
...certificateTemplateV2Orm,
create,
updateById,
deleteById,
findById,
findByProjectId,
countByProjectId,
findByNameAndProjectId,
isTemplateInUse,
getProfilesUsingTemplate
};
};
@@ -0,0 +1,181 @@
import RE2 from "re2";
import { z } from "zod";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
const attributeTypeSchema = z.nativeEnum(CertSubjectAttributeType);
const sanTypeSchema = z.nativeEnum(CertSubjectAlternativeNameType);
const templateV2SubjectSchema = z
.object({
type: attributeTypeSchema,
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Subject attribute must have at least one allowed, required, or denied value"
}
);
const templateV2KeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Key usages must have at least one allowed, required, or denied value"
}
);
const templateV2ExtendedKeyUsagesSchema = z
.object({
allowed: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
required: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional(),
denied: z.array(z.nativeEnum(CertExtendedKeyUsageType)).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "Extended key usages must have at least one allowed, required, or denied value"
}
);
const templateV2SanSchema = z
.object({
type: sanTypeSchema,
allowed: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
required: z.array(z.string().trim().min(1, "Value cannot be empty")).optional(),
denied: z.array(z.string().trim().min(1, "Value cannot be empty")).optional()
})
.refine(
(data) => {
if (!data.allowed && !data.required && !data.denied) {
return false;
}
return true;
},
{
message: "SAN must have at least one allowed, required, or denied value"
}
);
const templateV2ValiditySchema = z.object({
max: z
.string()
.regex(new RE2("^\\d+[dhmy]$"), {
message: "Max validity must be in format like '365d', '12m', '1y', or '24h'"
})
.optional()
});
const templateV2AlgorithmsSchema = z.object({
signature: z
.array(z.string().trim().min(1, "Algorithm cannot be empty"))
.min(1, "At least one signature algorithm must be provided")
.optional(),
keyAlgorithm: z
.array(z.string().trim().min(1, "Algorithm cannot be empty"))
.min(1, "At least one key algorithm must be provided")
.optional()
});
export const certificateTemplateV2ResponseSchema = z.object({
id: z.string().uuid(),
projectId: z.string().uuid("Project ID must be valid"),
name: z
.string()
.trim()
.min(1, "Template name is required")
.max(255, "Template name must be less than 255 characters")
.regex(new RE2("^[a-zA-Z0-9-_]+$"), "Template name must contain only letters, numbers, hyphens, and underscores"),
description: z.string().trim().max(1000, "Description must be less than 1000 characters").nullable().optional(),
subject: z.array(templateV2SubjectSchema).optional(),
sans: z.array(templateV2SanSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
extendedKeyUsages: templateV2ExtendedKeyUsagesSchema.optional(),
algorithms: templateV2AlgorithmsSchema.optional(),
validity: templateV2ValiditySchema.optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export const certificateRequestSchema = z.object({
commonName: z
.string()
.trim()
.min(1, "Common name cannot be empty")
.max(64, "Common name must be less than 64 characters")
.optional(),
organization: z
.string()
.trim()
.min(1, "Organization cannot be empty")
.max(64, "Organization must be less than 64 characters")
.optional(),
country: z
.string()
.trim()
.min(2, "Country code must be 2 characters")
.max(2, "Country code must be 2 characters")
.optional(),
keyUsages: z.array(z.nativeEnum(CertKeyUsageType)).min(1, "At least one key usage must be provided").optional(),
extendedKeyUsages: z
.array(z.nativeEnum(CertExtendedKeyUsageType))
.min(1, "At least one extended key usage must be provided")
.optional(),
subjectAlternativeNames: z
.array(
z.object({
type: sanTypeSchema,
value: z
.string()
.trim()
.min(1, "SAN value cannot be empty")
.max(255, "SAN value must be less than 255 characters")
})
)
.min(1, "At least one SAN must be provided")
.optional(),
validity: z
.object({
ttl: z
.string()
.trim()
.min(1, "TTL cannot be empty")
.regex(new RE2("^\\d+[dhmy]$"), "TTL must be in format like '365d', '12m', '1y', or '24h'")
})
.optional(),
signatureAlgorithm: z.string().trim().min(1, "Signature algorithm cannot be empty").optional(),
keyAlgorithm: z.string().trim().min(1, "Key algorithm cannot be empty").optional()
});
export const validateCertificateRequestSchema = z.object({
templateId: z.string().uuid(),
request: certificateRequestSchema
});
@@ -0,0 +1,959 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { CertSubjectAttributeType } from "../certificate-common/certificate-constants";
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
import {
TCertificateRequest,
TCertificateTemplateV2,
TCertificateTemplateV2Insert,
TCertificateTemplateV2Update,
TTemplateValidationResult
} from "./certificate-template-v2-types";
type TCertificateTemplateV2ServiceFactoryDep = {
certificateTemplateV2DAL: TCertificateTemplateV2DALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export const certificateTemplateV2ServiceFactory = ({
certificateTemplateV2DAL,
permissionService
}: TCertificateTemplateV2ServiceFactoryDep) => {
const consolidateAttributeArray = <
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
>(
attributes: T[]
): T[] => {
const consolidated = new Map<string, T>();
attributes.forEach((attr) => {
const existing = consolidated.get(attr.type);
if (existing) {
throw new ForbiddenRequestError({
message: `Duplicate attribute type '${attr.type}' found in request. Each attribute type must appear only once.`
});
} else {
consolidated.set(attr.type, attr);
}
});
return Array.from(consolidated.values());
};
const parseTTL = (ttl: string): number => {
const regex = new RE2("^(\\d+)([dmyh])$");
const match = regex.exec(ttl);
if (!match) {
throw new Error(`Invalid TTL format: ${ttl}`);
}
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case "h":
return value * 60 * 60 * 1000;
case "d":
return value * 24 * 60 * 60 * 1000;
case "m":
return value * 30 * 24 * 60 * 60 * 1000;
case "y":
return value * 365 * 24 * 60 * 60 * 1000;
default:
throw new Error(`Unsupported TTL unit: ${unit}`);
}
};
const validateSubjectAttributePolicy = (
subject: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
) => {
if (!subject || subject.length === 0) return;
// Validate each subject attribute policy
for (const attr of subject) {
// Ensure at least one field is provided
if (!attr.allowed && !attr.required && !attr.denied) {
throw new ForbiddenRequestError({
message: `Subject attribute type '${attr.type}' must have at least one allowed, required, or denied value`
});
}
// Check for duplicate values within arrays
const arrays = [
{ name: "allowed", values: attr.allowed },
{ name: "required", values: attr.required },
{ name: "denied", values: attr.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} list for subject attribute type '${attr.type}'`
});
}
}
}
}
};
const validateSanPolicy = (
sans: Array<{ type: string; allowed?: string[]; required?: string[]; denied?: string[] }>
) => {
if (!sans || sans.length === 0) return;
// Validate each SAN policy
for (const san of sans) {
if (!san.allowed && !san.required && !san.denied) {
throw new ForbiddenRequestError({
message: `SAN type '${san.type}' must have at least one allowed, required, or denied value`
});
}
const arrays = [
{ name: "allowed", values: san.allowed },
{ name: "required", values: san.required },
{ name: "denied", values: san.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} list for SAN type '${san.type}'`
});
}
}
}
}
};
const generateTemplateSlug = (baseName?: string): string => {
if (baseName) {
return slugify(baseName);
}
return slugify(alphaNumericNanoId(12));
};
const ensureUniqueSlug = async (projectId: string, desiredSlug: string, templateId?: string): Promise<string> => {
const existingTemplate = await certificateTemplateV2DAL.findByNameAndProjectId(desiredSlug, projectId);
if (!existingTemplate || (templateId && existingTemplate.id === templateId)) {
return desiredSlug;
}
const alternativeSlug = `${desiredSlug}-${alphaNumericNanoId(8)}`;
const existingAlternative = await certificateTemplateV2DAL.findByNameAndProjectId(alternativeSlug, projectId);
if (!existingAlternative) {
return alternativeSlug;
}
const randomSlug = slugify(alphaNumericNanoId(12));
return randomSlug;
};
const isWildcardPattern = (value: string): boolean => {
return value.includes("*");
};
const createWildcardRegex = (pattern: string): RegExp => {
const wildcardRegex = new RE2(/\*/g);
const withPlaceholder = pattern.replace(wildcardRegex, "__WILDCARD__");
const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g);
const escaped = withPlaceholder.replace(escapeRegex, "\\$&");
const placeholderRegex = new RE2(/__WILDCARD__/g);
const regexPattern = escaped.replace(placeholderRegex, ".*");
return new RE2(`^${regexPattern}$`);
};
const mapTemplateSignatureAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"SHA256-RSA": "RSA-SHA256",
"SHA384-RSA": "RSA-SHA384",
"SHA512-RSA": "RSA-SHA512",
"SHA256-ECDSA": "ECDSA-SHA256",
"SHA384-ECDSA": "ECDSA-SHA384",
"SHA512-ECDSA": "ECDSA-SHA512"
};
return mapping[templateFormat] || templateFormat;
};
const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string => {
const mapping: Record<string, string> = {
"RSA-2048": "RSA_2048",
"RSA-3072": "RSA_3072",
"RSA-4096": "RSA_4096",
"ECDSA-P256": "EC_prime256v1",
"ECDSA-P384": "EC_secp384r1",
"ECDSA-P521": "EC_secp521r1"
};
return mapping[templateFormat] || templateFormat;
};
const validateKeyUsagePolicy = (keyUsages: { allowed?: string[]; required?: string[]; denied?: string[] }) => {
if (!keyUsages) return;
if (!keyUsages.allowed && !keyUsages.required && !keyUsages.denied) {
throw new ForbiddenRequestError({
message: "Key usages must have at least one allowed, required, or denied value"
});
}
const arrays = [
{ name: "allowed", values: keyUsages.allowed },
{ name: "required", values: keyUsages.required },
{ name: "denied", values: keyUsages.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} key usages list`
});
}
}
}
};
const validateExtendedKeyUsagePolicy = (extendedKeyUsages: {
allowed?: string[];
required?: string[];
denied?: string[];
}) => {
if (!extendedKeyUsages) return;
if (!extendedKeyUsages.allowed && !extendedKeyUsages.required && !extendedKeyUsages.denied) {
throw new ForbiddenRequestError({
message: "Extended key usages must have at least one allowed, required, or denied value"
});
}
const arrays = [
{ name: "allowed", values: extendedKeyUsages.allowed },
{ name: "required", values: extendedKeyUsages.required },
{ name: "denied", values: extendedKeyUsages.denied }
];
for (const { name, values } of arrays) {
if (values && values.length > 0) {
const uniqueValues = new Set(values);
if (uniqueValues.size !== values.length) {
throw new ForbiddenRequestError({
message: `Duplicate values found in ${name} extended key usages list`
});
}
}
}
};
const validateValueAgainstConstraints = (
value: string,
allowedValues: string[],
fieldName: string
): { isValid: boolean; error?: string } => {
if (!allowedValues || allowedValues.length === 0) {
return { isValid: true };
}
const hasWildcards = allowedValues.some(isWildcardPattern);
for (const allowedValue of allowedValues) {
if (isWildcardPattern(allowedValue)) {
try {
const regex = createWildcardRegex(allowedValue);
if (regex.test(value)) {
return { isValid: true };
}
} catch (error) {
if (allowedValue === value) {
return { isValid: true };
}
}
} else if (allowedValue === value) {
return { isValid: true };
}
}
if (hasWildcards) {
return {
isValid: false,
error: `${fieldName} value '${value}' does not match allowed patterns: ${allowedValues.join(", ")}`
};
}
return {
isValid: false,
error: `${fieldName} value '${value}' is not in allowed values list`
};
};
const validateRequestAgainstPolicy = (
template: TCertificateTemplateV2,
request: TCertificateRequest
): TTemplateValidationResult => {
const errors: string[] = [];
const warnings: string[] = [];
// Validate subject attributes
const subjectPolicies = template.subject;
const requestAttributes = new Map<string, string>();
if (request.commonName) requestAttributes.set(CertSubjectAttributeType.COMMON_NAME, request.commonName);
if (request.organization) {
requestAttributes.set(CertSubjectAttributeType.ORGANIZATION, request.organization);
}
if (request.country) requestAttributes.set(CertSubjectAttributeType.COUNTRY, request.country);
if (subjectPolicies && subjectPolicies.length > 0) {
for (const attrPolicy of subjectPolicies) {
const requestValue = requestAttributes.get(attrPolicy.type);
if (attrPolicy.required && attrPolicy.required.length > 0) {
if (!requestValue) {
errors.push(`Missing required ${attrPolicy.type} attribute`);
} else {
// Validate that the request value matches the required pattern
const hasMatchingRequired = attrPolicy.required.some((requiredValue) => {
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
return validation.isValid;
});
if (!hasMatchingRequired) {
errors.push(
`${attrPolicy.type} value '${requestValue}' does not match any required patterns: ${attrPolicy.required.join(", ")}`
);
}
}
}
if (requestValue) {
let isValueDenied = false;
if (attrPolicy.denied && attrPolicy.denied.length > 0) {
const validation = validateValueAgainstConstraints(requestValue, attrPolicy.denied, attrPolicy.type);
if (validation.isValid) {
errors.push(`${attrPolicy.type} value '${requestValue}' is denied by template policy`);
isValueDenied = true;
}
}
if (!isValueDenied && attrPolicy.allowed && attrPolicy.allowed.length > 0) {
let satisfiesRequired = false;
if (attrPolicy.required && attrPolicy.required.length > 0) {
satisfiesRequired = attrPolicy.required.some((requiredValue) => {
const validation = validateValueAgainstConstraints(requestValue, [requiredValue], attrPolicy.type);
return validation.isValid;
});
}
if (!satisfiesRequired) {
const allowedValidation = validateValueAgainstConstraints(
requestValue,
attrPolicy.allowed,
attrPolicy.type
);
if (!allowedValidation.isValid && allowedValidation.error) {
errors.push(allowedValidation.error);
}
}
}
}
}
// Check if any request attributes are not covered by template policies
for (const [attrType] of requestAttributes) {
const hasPolicy = subjectPolicies.some((policy) => policy.type === attrType);
if (!hasPolicy) {
errors.push(`${attrType} is not allowed by template policy (not defined in template)`);
}
}
} else if (requestAttributes.size > 0) {
// No subject policies defined but request has subject attributes - deny all
for (const [attrType] of requestAttributes) {
errors.push(`${attrType} is not allowed by template policy (no subject policies defined)`);
}
}
// Validate Subject Alternative Names
const sansPolicies = template.sans;
if (sansPolicies && sansPolicies.length > 0) {
const requestSansByType = new Map<string, string[]>();
// Group request SANs by type
if (request.subjectAlternativeNames) {
for (const san of request.subjectAlternativeNames) {
if (!requestSansByType.has(san.type)) {
requestSansByType.set(san.type, []);
}
requestSansByType.get(san.type)!.push(san.value);
}
}
// Validate each SAN policy
for (const sanPolicy of sansPolicies) {
const requestSans = requestSansByType.get(sanPolicy.type) || [];
// Check REQUIRED values - at least one SAN must match each required pattern
if (sanPolicy.required && sanPolicy.required.length > 0) {
for (const requiredValue of sanPolicy.required) {
const hasMatchingRequiredSan = requestSans.some((sanValue) => {
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
return validation.isValid;
});
if (!hasMatchingRequiredSan) {
errors.push(`Required ${sanPolicy.type} SAN matching pattern '${requiredValue}' not found in request`);
}
}
}
// Check DENIED values - no SAN should match denied patterns
if (sanPolicy.denied && sanPolicy.denied.length > 0) {
for (const sanValue of requestSans) {
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.denied, `${sanPolicy.type} SAN`);
if (validation.isValid) {
errors.push(`${sanPolicy.type} SAN matching denied pattern '${sanValue}' found in request`);
}
}
}
// Check ALLOWED values - if present, all SANs must match at least one allowed pattern
if (sanPolicy.allowed && sanPolicy.allowed.length > 0 && requestSans.length > 0) {
for (const sanValue of requestSans) {
let satisfiesRequired = false;
if (sanPolicy.required && sanPolicy.required.length > 0) {
satisfiesRequired = sanPolicy.required.some((requiredValue) => {
const validation = validateValueAgainstConstraints(sanValue, [requiredValue], `${sanPolicy.type} SAN`);
return validation.isValid;
});
}
if (!satisfiesRequired) {
const validation = validateValueAgainstConstraints(sanValue, sanPolicy.allowed, `${sanPolicy.type} SAN`);
if (!validation.isValid && validation.error) {
errors.push(validation.error);
}
}
}
}
}
// Check if any request SANs are for types not covered by template policies
for (const [requestSanType] of requestSansByType) {
const hasPolicy = sansPolicies.some((policy) => policy.type === requestSanType);
if (!hasPolicy) {
errors.push(`${requestSanType} SAN is not allowed by template policy (not defined in template)`);
}
}
} else if (request.subjectAlternativeNames && request.subjectAlternativeNames.length > 0) {
// No SAN policies defined but request has SANs - deny all
for (const san of request.subjectAlternativeNames) {
errors.push(`${san.type} SAN is not allowed by template policy (no SAN policies defined)`);
}
}
// Validate key usages
const keyUsagePolicy = template.keyUsages;
if (keyUsagePolicy) {
// Check REQUIRED key usages - must have all required usages
if (keyUsagePolicy.required && keyUsagePolicy.required.length > 0) {
const missingRequired = keyUsagePolicy.required.filter((usage) => !request.keyUsages?.includes(usage));
if (missingRequired.length > 0) {
errors.push(`Missing required key usages: ${missingRequired.join(", ")}`);
}
}
// Check DENIED key usages - must not have any denied usages
if (request.keyUsages && keyUsagePolicy.denied && keyUsagePolicy.denied.length > 0) {
const deniedUsages = request.keyUsages.filter((usage) => keyUsagePolicy?.denied?.includes(usage));
if (deniedUsages.length > 0) {
errors.push(`Denied key usages found in request: ${deniedUsages.join(", ")}`);
}
}
// Check ALLOWED key usages - if present, all usages must be in allowed list
if (request.keyUsages && keyUsagePolicy && keyUsagePolicy.allowed && keyUsagePolicy.allowed.length > 0) {
const allAllowedUsages = [...(keyUsagePolicy.required || []), ...(keyUsagePolicy.allowed || [])];
const invalidUsages = request.keyUsages.filter((usage) => !allAllowedUsages.includes(usage));
if (invalidUsages.length > 0) {
errors.push(`Invalid key usages: ${invalidUsages.join(", ")}`);
}
}
} else if (request.keyUsages && request.keyUsages.length > 0) {
errors.push(`Key usages are not allowed by template policy (not defined in template)`);
}
// Validate extended key usages
const extendedKeyUsagePolicy = template.extendedKeyUsages;
if (extendedKeyUsagePolicy) {
// Check REQUIRED extended key usages - must have all required usages
if (extendedKeyUsagePolicy.required && extendedKeyUsagePolicy.required.length > 0) {
const missingRequired = extendedKeyUsagePolicy.required.filter(
(usage) => !request.extendedKeyUsages?.includes(usage)
);
if (missingRequired.length > 0) {
errors.push(`Missing required extended key usages: ${missingRequired.join(", ")}`);
}
}
// Check DENIED extended key usages - must not have any denied usages
if (request.extendedKeyUsages && extendedKeyUsagePolicy.denied && extendedKeyUsagePolicy.denied.length > 0) {
const deniedUsages = request.extendedKeyUsages.filter((usage) =>
extendedKeyUsagePolicy?.denied?.includes(usage)
);
if (deniedUsages.length > 0) {
errors.push(`Denied extended key usages found in request: ${deniedUsages.join(", ")}`);
}
}
// Check ALLOWED extended key usages - if present, all usages must be in allowed list
if (
request.extendedKeyUsages &&
extendedKeyUsagePolicy &&
extendedKeyUsagePolicy.allowed &&
extendedKeyUsagePolicy.allowed.length > 0
) {
const allAllowedExtendedUsages = [
...(extendedKeyUsagePolicy.required || []),
...(extendedKeyUsagePolicy.allowed || [])
];
const invalidExtendedUsages = request.extendedKeyUsages.filter(
(usage) => !allAllowedExtendedUsages.includes(usage)
);
if (invalidExtendedUsages.length > 0) {
errors.push(`Invalid extended key usages: ${invalidExtendedUsages.join(", ")}`);
}
}
} else if (request.extendedKeyUsages && request.extendedKeyUsages.length > 0) {
errors.push(`Extended key usages are not allowed by template policy (not defined in template)`);
}
// Validate algorithms with new structure
if (request.signatureAlgorithm) {
if (template.algorithms?.signature && template.algorithms.signature.length > 0) {
const mappedTemplateAlgorithms = template.algorithms.signature.map(mapTemplateSignatureAlgorithmToApi);
if (!mappedTemplateAlgorithms.includes(request.signatureAlgorithm)) {
errors.push(`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy`);
}
} else if (!template.algorithms?.signature) {
errors.push(
`Signature algorithm '${request.signatureAlgorithm}' is not allowed by template policy (not defined in template)`
);
}
}
if (request.keyAlgorithm) {
if (template.algorithms?.keyAlgorithm && template.algorithms.keyAlgorithm.length > 0) {
const mappedTemplateKeyTypes = template.algorithms.keyAlgorithm.map(mapTemplateKeyAlgorithmToApi);
if (!mappedTemplateKeyTypes.includes(request.keyAlgorithm)) {
errors.push(`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy`);
}
} else if (!template.algorithms?.keyAlgorithm) {
errors.push(
`Key algorithm '${request.keyAlgorithm}' is not allowed by template policy (not defined in template)`
);
}
}
// Validate validity with new structure
if (request.validity?.ttl && (request.notBefore || request.notAfter)) {
errors.push(
"Cannot specify both TTL and notBefore/notAfter. Use either TTL for duration-based validity or notBefore/notAfter for explicit date range."
);
}
if (request.notBefore && request.notAfter && request.notBefore >= request.notAfter) {
errors.push("notBefore must be earlier than notAfter");
}
// Validate TTL against template validity constraints
if (request.validity?.ttl && template.validity) {
const requestDurationMs = parseTTL(request.validity.ttl);
// Check maximum duration using max field
if (template.validity.max) {
const maxDurationMs = parseTTL(template.validity.max);
if (requestDurationMs > maxDurationMs) {
errors.push("Requested validity period exceeds maximum allowed duration");
}
}
}
// Validate explicit date range against max duration
if ((request.notBefore || request.notAfter) && template.validity?.max) {
const notBefore = request.notBefore || new Date();
const { notAfter } = request;
if (notAfter && notBefore && notAfter instanceof Date && notBefore instanceof Date) {
const requestDuration = notAfter.getTime() - notBefore.getTime();
const maxDurationMs = parseTTL(template.validity.max);
if (requestDuration > maxDurationMs) {
errors.push(
`Requested validity period (notBefore to notAfter) exceeds maximum allowed duration of ${template.validity.max}`
);
}
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
};
const createTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
data: Omit<TCertificateTemplateV2Insert, "projectId">;
}): Promise<TCertificateTemplateV2> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionSub.CertificateTemplates
);
if (!data) {
throw new Error("Template data is required");
}
const consolidatedData = {
...data,
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
};
if (consolidatedData.subject) {
validateSubjectAttributePolicy(consolidatedData.subject);
}
if (consolidatedData.sans) {
validateSanPolicy(consolidatedData.sans);
}
if (consolidatedData.keyUsages) {
validateKeyUsagePolicy(consolidatedData.keyUsages);
}
if (consolidatedData.extendedKeyUsages) {
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
}
// Generate slug from name and ensure it's unique within project
if (!data.name) {
throw new ForbiddenRequestError({ message: "Template name is required" });
}
const slug = generateTemplateSlug(data.name);
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
const template = await certificateTemplateV2DAL.create({
...consolidatedData,
name: uniqueSlug,
projectId
});
return template;
};
const updateTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId,
data
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
data: TCertificateTemplateV2Update;
}): Promise<TCertificateTemplateV2> => {
const existingTemplate = await certificateTemplateV2DAL.findById(templateId);
if (!existingTemplate) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: existingTemplate.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionSub.CertificateTemplates
);
const consolidatedData = {
...data,
subject: data.subject ? consolidateAttributeArray(data.subject) : undefined,
sans: data.sans ? consolidateAttributeArray(data.sans) : undefined
};
if (consolidatedData.subject) {
validateSubjectAttributePolicy(consolidatedData.subject);
}
if (consolidatedData.sans) {
validateSanPolicy(consolidatedData.sans);
}
if (consolidatedData.keyUsages) {
validateKeyUsagePolicy(consolidatedData.keyUsages);
}
if (consolidatedData.extendedKeyUsages) {
validateExtendedKeyUsagePolicy(consolidatedData.extendedKeyUsages);
}
const updateData = { ...consolidatedData };
if (data.name && typeof data.name === "string") {
const newSlug = generateTemplateSlug(data.name);
if (newSlug !== existingTemplate.name) {
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, newSlug, templateId);
updateData.name = uniqueSlug;
}
}
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, updateData);
if (!updatedTemplate) {
throw new NotFoundError({ message: "Failed to update certificate template" });
}
return updatedTemplate;
};
const getTemplateV2ById = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
}): Promise<TCertificateTemplateV2> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
return template;
};
const getTemplateV2BySlug = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
slug
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
slug: string;
}): Promise<TCertificateTemplateV2> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const template = await certificateTemplateV2DAL.findByNameAndProjectId(slug, projectId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
return template;
};
const listTemplatesV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId,
offset = 0,
limit = 20,
search
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
projectId: string;
offset?: number;
limit?: number;
search?: string;
}): Promise<{
templates: TCertificateTemplateV2[];
totalCount: number;
}> => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Read,
ProjectPermissionSub.CertificateTemplates
);
const templates = await certificateTemplateV2DAL.findByProjectId(projectId, {
offset,
limit,
search
});
const totalCount = await certificateTemplateV2DAL.countByProjectId(projectId, { search });
return {
templates,
totalCount
};
};
const deleteTemplateV2 = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId
}: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
templateId: string;
}): Promise<TCertificateTemplateV2> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: template.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionSub.CertificateTemplates
);
const isInUse = await certificateTemplateV2DAL.isTemplateInUse(templateId);
if (isInUse) {
const profilesUsingTemplate = await certificateTemplateV2DAL.getProfilesUsingTemplate(templateId);
const profileNames = profilesUsingTemplate
.map((profile: { slug?: string; id: string }) => profile.slug || profile.id)
.join(", ");
throw new ForbiddenRequestError({
message:
profilesUsingTemplate.length > 0
? `Cannot delete template '${template.name}' as it is currently in use by the following certificate profiles: ${profileNames}. Please remove this template from these profiles before deleting it.`
: `Cannot delete template '${template.name}' as it is currently in use by one or more certificates. Please ensure no certificates are using this template before deleting it.`
});
}
const deletedTemplate = await certificateTemplateV2DAL.deleteById(templateId);
if (!deletedTemplate) {
throw new NotFoundError({ message: "Failed to delete certificate template" });
}
return deletedTemplate as TCertificateTemplateV2;
};
const validateCertificateRequest = async (
templateId: string,
request: TCertificateRequest
): Promise<TTemplateValidationResult> => {
const template = await certificateTemplateV2DAL.findById(templateId);
if (!template) {
throw new NotFoundError({ message: "Certificate template not found" });
}
return validateRequestAgainstPolicy(template, request);
};
return {
createTemplateV2,
updateTemplateV2,
getTemplateV2ById,
getTemplateV2BySlug,
listTemplatesV2,
deleteTemplateV2,
validateCertificateRequest
};
};
export type TCertificateTemplateV2ServiceFactory = ReturnType<typeof certificateTemplateV2ServiceFactory>;
@@ -0,0 +1,98 @@
import {
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert
} from "@app/db/schemas/pki-certificate-templates-v2";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType,
CertSubjectAttributeType
} from "@app/services/certificate-common/certificate-constants";
export interface TTemplateV2Policy {
subject?: Array<{
type: CertSubjectAttributeType;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
sans?: Array<{
type: CertSubjectAlternativeNameType;
allowed?: string[];
required?: string[];
denied?: string[];
}>;
keyUsages?: {
allowed?: CertKeyUsageType[];
required?: CertKeyUsageType[];
denied?: CertKeyUsageType[];
};
extendedKeyUsages?: {
allowed?: CertExtendedKeyUsageType[];
required?: CertExtendedKeyUsageType[];
denied?: CertExtendedKeyUsageType[];
};
algorithms?: {
signature?: string[];
keyAlgorithm?: string[];
};
validity?: {
max?: string;
};
}
export type TCertificateTemplateV2 = TPkiCertificateTemplatesV2 & {
subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
algorithms?: TTemplateV2Policy["algorithms"];
validity?: TTemplateV2Policy["validity"];
};
export type TCertificateTemplateV2Insert = TPkiCertificateTemplatesV2Insert & {
subject?: TTemplateV2Policy["subject"];
sans?: TTemplateV2Policy["sans"];
keyUsages?: TTemplateV2Policy["keyUsages"];
extendedKeyUsages?: TTemplateV2Policy["extendedKeyUsages"];
algorithms?: TTemplateV2Policy["algorithms"];
validity?: TTemplateV2Policy["validity"];
};
export type TCertificateTemplateV2Update = Partial<
Pick<
TCertificateTemplateV2,
"name" | "description" | "subject" | "sans" | "keyUsages" | "extendedKeyUsages" | "algorithms" | "validity"
>
>;
export interface TCertificateRequest {
commonName?: string;
organization?: string;
organizationUnit?: string;
locality?: string;
state?: string;
country?: string;
email?: string;
streetAddress?: string;
postalCode?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
subjectAlternativeNames?: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
validity?: {
ttl: string;
};
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
}
export interface TTemplateValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
@@ -420,7 +420,7 @@ export const certificateTemplateServiceFactory = ({
}; };
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => { const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
const { certificateTemplateId } = dto; const { certificateTemplateId, isInternal } = dto;
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId); const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) { if (!certTemplate) {
@@ -429,7 +429,7 @@ export const certificateTemplateServiceFactory = ({
}); });
} }
if (!dto.isInternal) { if (!isInternal) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: dto.actor, actor: dto.actor,
actorId: dto.actorId, actorId: dto.actorId,
@@ -440,7 +440,7 @@ export const certificateTemplateServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionPkiTemplateActions.Read,
subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name }) subject(ProjectPermissionSub.CertificateTemplates, { name: certTemplate.name })
); );
} }
@@ -3,13 +3,16 @@ import z from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
export const validateTemplateRegexField = z export const createTemplateFieldValidator = (options?: {
.string() minLength?: number;
.min(1) maxLength?: number;
.max(100) allowedCharacters?: CharacterType[];
.refine( customMessage?: string;
(val) => }) => {
characterValidator([ const {
minLength = 1,
maxLength = 100,
allowedCharacters = [
CharacterType.AlphaNumeric, CharacterType.AlphaNumeric,
CharacterType.Spaces, // (space) CharacterType.Spaces, // (space)
CharacterType.Asterisk, // * CharacterType.Asterisk, // *
@@ -17,12 +20,23 @@ export const validateTemplateRegexField = z
CharacterType.Hyphen, // - CharacterType.Hyphen, // -
CharacterType.Period, // . CharacterType.Period, // .
CharacterType.Backslash // \ CharacterType.Backslash // \
])(val), ],
{ customMessage = "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed." } = options || {};
}
) return (
z
.string()
.min(minLength)
.max(maxLength)
.refine((val) => characterValidator(allowedCharacters)(val), {
message: customMessage
})
// we ensure that the inputted pattern is computationally safe by limiting star height to 1 // we ensure that the inputted pattern is computationally safe by limiting star height to 1
.refine((v) => safe(v), { .refine((v) => safe(v), {
message: "Unsafe REGEX pattern" message: "Unsafe REGEX pattern"
}); })
);
};
export const validateTemplateRegexField = createTemplateFieldValidator();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,487 @@
import { ForbiddenError } from "@casl/ability";
import { randomUUID } from "crypto";
import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionCertificateProfileActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import {
CertificateOrderStatus,
CertKeyAlgorithm,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import {
TCertificateAuthorityDALFactory,
TCertificateAuthorityWithAssociatedCa
} from "@app/services/certificate-authority/certificate-authority-dal";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { CertSubjectAlternativeNameType } from "../certificate-common/certificate-constants";
import {
bufferToString,
buildCertificateSubjectFromTemplate,
buildSubjectAlternativeNamesFromTemplate,
convertExtendedKeyUsageArrayToLegacy,
convertKeyUsageArrayToLegacy,
mapEnumsForValidation,
normalizeDateForApi
} from "../certificate-common/certificate-utils";
import {
TCertificateFromProfileResponse,
TCertificateOrderResponse,
TIssueCertificateFromProfileDTO,
TOrderCertificateFromProfileDTO,
TSignCertificateFromProfileDTO
} from "./certificate-v3-types";
type TCertificateV3ServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "updateById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
certificateTemplateV2Service: Pick<
TCertificateTemplateV2ServiceFactory,
"validateCertificateRequest" | "getTemplateV2ById"
>;
internalCaService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa" | "issueCertFromCa">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TCertificateV3ServiceFactory = ReturnType<typeof certificateV3ServiceFactory>;
const validateProfileAndPermissions = async (
profileId: string,
actor: ActorType,
actorId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string,
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">,
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">,
requiredEnrollmentType: EnrollmentType
) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== requiredEnrollmentType) {
throw new ForbiddenRequestError({
message: `Profile is not configured for ${requiredEnrollmentType} enrollment`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: profile.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateProfileActions.IssueCert,
ProjectPermissionSub.CertificateProfiles
);
return profile;
};
const validateCaSupport = (ca: TCertificateAuthorityWithAssociatedCa, operation: string) => {
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
if (caType !== CaType.INTERNAL) {
throw new BadRequestError({ message: `Only internal CAs support ${operation}` });
}
return caType;
};
const validateAlgorithmCompatibility = (
ca: TCertificateAuthorityWithAssociatedCa,
template: {
algorithms?: {
signature?: string[];
};
}
) => {
if (!template.algorithms?.signature || template.algorithms.signature.length === 0) {
return;
}
const caKeyAlgorithm = ca.internalCa?.keyAlgorithm;
if (!caKeyAlgorithm) {
throw new BadRequestError({ message: "CA key algorithm not found" });
}
const compatibleAlgorithms =
template.algorithms?.signature?.filter((sigAlg: string) => {
const parts = sigAlg.split("-");
if (parts.length === 0) {
return false;
}
const keyType = parts[parts.length - 1];
if (caKeyAlgorithm.startsWith("RSA")) {
return keyType === "RSA";
}
if (caKeyAlgorithm.startsWith("EC")) {
return keyType === "ECDSA";
}
return false;
}) || [];
if (compatibleAlgorithms.length === 0) {
throw new BadRequestError({
message: `Template signature algorithms (${template.algorithms?.signature?.join(", ") || "none"}) are not compatible with CA key algorithm (${caKeyAlgorithm})`
});
}
};
const extractCertificateFromBuffer = (certData: Buffer | { rawData: Buffer } | string): string => {
if (typeof certData === "string") return certData;
if (Buffer.isBuffer(certData)) return bufferToString(certData);
if (certData && typeof certData === "object" && "rawData" in certData && Buffer.isBuffer(certData.rawData)) {
return bufferToString(certData.rawData);
}
return bufferToString(certData as unknown as Buffer);
};
export const certificateV3ServiceFactory = ({
certificateDAL,
certificateAuthorityDAL,
certificateProfileDAL,
certificateTemplateV2Service,
internalCaService,
permissionService
}: TCertificateV3ServiceFactoryDep) => {
const issueCertificateFromProfile = async ({
profileId,
certificateRequest,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TIssueCertificateFromProfileDTO): Promise<TCertificateFromProfileResponse> => {
const profile = await validateProfileAndPermissions(
profileId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
certificateProfileDAL,
permissionService,
EnrollmentType.API
);
if (certificateRequest.commonName && Array.isArray(certificateRequest.commonName)) {
throw new BadRequestError({
message: "Common Name must be a single value, not an array"
});
}
const mappedCertificateRequest = mapEnumsForValidation({
...certificateRequest,
subjectAlternativeNames: certificateRequest.altNames
});
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId
});
if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
validateCaSupport(ca, "direct certificate issuance");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate issuance" });
}
validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm = certificateRequest.signatureAlgorithm as CertSignatureAlgorithm | undefined;
const effectiveKeyAlgorithm = certificateRequest.keyAlgorithm as CertKeyAlgorithm | undefined;
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
throw new BadRequestError({
message: "Key algorithm is required by template policy but not provided in request"
});
}
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
throw new BadRequestError({
message: "Signature algorithm is required by template policy but not provided in request"
});
}
const certificateSubject = buildCertificateSubjectFromTemplate(certificateRequest, template.subject);
const subjectAlternativeNames = buildSubjectAlternativeNamesFromTemplate(
{ subjectAlternativeNames: certificateRequest.altNames },
template.sans
);
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber } =
await internalCaService.issueCertFromCa({
caId: ca.id,
friendlyName: certificateSubject.common_name || "Certificate",
commonName: certificateSubject.common_name || "",
altNames: subjectAlternativeNames,
ttl: certificateRequest.validity.ttl,
keyUsages: convertKeyUsageArrayToLegacy(certificateRequest.keyUsages) || [],
extendedKeyUsages: convertExtendedKeyUsageArrayToLegacy(certificateRequest.extendedKeyUsages) || [],
notBefore: normalizeDateForApi(certificateRequest.notBefore),
notAfter: normalizeDateForApi(certificateRequest.notAfter),
signatureAlgorithm: effectiveSignatureAlgorithm,
keyAlgorithm: effectiveKeyAlgorithm,
actor,
actorId,
actorAuthMethod,
actorOrgId,
isFromProfile: true
});
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!cert) {
throw new NotFoundError({ message: "Certificate was issued but could not be found in database" });
}
await certificateDAL.updateById(cert.id, { profileId });
return {
certificate: bufferToString(certificate),
issuingCaCertificate: bufferToString(issuingCaCertificate),
certificateChain: bufferToString(certificateChain),
privateKey: bufferToString(privateKey),
serialNumber,
certificateId: cert.id,
projectId: profile.projectId,
profileName: profile.slug
};
};
const signCertificateFromProfile = async ({
profileId,
csr,
validity,
notBefore,
notAfter,
signatureAlgorithm,
keyAlgorithm,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TSignCertificateFromProfileDTO): Promise<Omit<TCertificateFromProfileResponse, "privateKey">> => {
const profile = await validateProfileAndPermissions(
profileId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
certificateProfileDAL,
permissionService,
EnrollmentType.API
);
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
validateCaSupport(ca, "CSR signing");
if (!actorAuthMethod) {
throw new BadRequestError({ message: "Authentication method is required for certificate signing" });
}
const template = await certificateTemplateV2Service.getTemplateV2ById({
actor,
actorId,
actorAuthMethod,
actorOrgId,
templateId: profile.certificateTemplateId
});
if (!template) {
throw new NotFoundError({ message: "Certificate template not found for this profile" });
}
validateAlgorithmCompatibility(ca, template);
const effectiveSignatureAlgorithm = signatureAlgorithm;
const effectiveKeyAlgorithm = keyAlgorithm;
if (template.algorithms?.keyAlgorithm && !effectiveKeyAlgorithm) {
throw new BadRequestError({
message: "Key algorithm is required by template policy but not provided in request"
});
}
if (template.algorithms?.signature && !effectiveSignatureAlgorithm) {
throw new BadRequestError({
message: "Signature algorithm is required by template policy but not provided in request"
});
}
const { certificate, certificateChain, issuingCaCertificate, serialNumber } =
await internalCaService.signCertFromCa({
isInternal: true,
caId: ca.id,
csr,
ttl: validity.ttl,
altNames: undefined,
notBefore: normalizeDateForApi(notBefore),
notAfter: normalizeDateForApi(notAfter),
signatureAlgorithm: effectiveSignatureAlgorithm,
keyAlgorithm: effectiveKeyAlgorithm,
isFromProfile: true
});
const cert = await certificateDAL.findOne({ serialNumber, caId: ca.id });
if (!cert) {
throw new NotFoundError({ message: "Certificate was signed but could not be found in database" });
}
await certificateDAL.updateById(cert.id, { profileId });
const certificateString = extractCertificateFromBuffer(certificate as unknown as Buffer);
const certificateChainString = extractCertificateFromBuffer(certificateChain as unknown as Buffer);
return {
certificate: certificateString,
issuingCaCertificate: extractCertificateFromBuffer(issuingCaCertificate as unknown as Buffer),
certificateChain: certificateChainString,
serialNumber,
certificateId: cert.id,
projectId: profile.projectId,
profileName: profile.slug
};
};
const orderCertificateFromProfile = async ({
profileId,
certificateOrder,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TOrderCertificateFromProfileDTO): Promise<TCertificateOrderResponse> => {
const profile = await validateProfileAndPermissions(
profileId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
certificateProfileDAL,
permissionService,
EnrollmentType.API
);
const certificateRequest = {
commonName: certificateOrder.commonName,
keyUsages: certificateOrder.keyUsages,
extendedKeyUsages: certificateOrder.extendedKeyUsages,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type === "dns" ? CertSubjectAlternativeNameType.DNS_NAME : CertSubjectAlternativeNameType.IP_ADDRESS,
value: san.value
})),
validity: certificateOrder.validity,
notBefore: certificateOrder.notBefore,
notAfter: certificateOrder.notAfter,
signatureAlgorithm: certificateOrder.signatureAlgorithm,
keyAlgorithm: certificateOrder.keyAlgorithm
};
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate order validation failed: ${validationResult.errors.join(", ")}`
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca) {
throw new NotFoundError({ message: "Certificate Authority not found" });
}
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
if (caType === CaType.INTERNAL) {
const certificateResult = await issueCertificateFromProfile({
profileId,
certificateRequest,
actor,
actorId,
actorAuthMethod,
actorOrgId
});
const orderId = randomUUID();
return {
orderId,
status: CertificateOrderStatus.VALID,
subjectAlternativeNames: certificateOrder.altNames.map((san) => ({
type: san.type,
value: san.value,
status: CertificateOrderStatus.VALID
})),
authorizations: [],
finalize: `/api/v3/certificates/orders/${orderId}/completed`,
certificate: certificateResult.certificate,
projectId: certificateResult.projectId,
profileName: certificateResult.profileName
};
}
if (caType === CaType.ACME) {
throw new BadRequestError({
message: "ACME certificate ordering via profiles is not yet implemented."
});
}
throw new BadRequestError({
message: `Certificate ordering is not supported for CA type: ${caType}`
});
};
return {
issueCertificateFromProfile,
signCertificateFromProfile,
orderCertificateFromProfile
};
};
@@ -0,0 +1,99 @@
import { TProjectPermission } from "@app/lib/types";
import { ACMESANType, CertificateOrderStatus } from "../certificate/certificate-types";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSubjectAlternativeNameType
} from "../certificate-common/certificate-constants";
export type TIssueCertificateFromProfileDTO = {
profileId: string;
certificateRequest: {
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
altNames?: Array<{
type: CertSubjectAlternativeNameType;
value: string;
}>;
validity: {
ttl: string;
};
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
};
} & Omit<TProjectPermission, "projectId">;
export type TSignCertificateFromProfileDTO = {
profileId: string;
csr: string;
validity: {
ttl: string;
};
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
} & Omit<TProjectPermission, "projectId">;
export type TOrderCertificateFromProfileDTO = {
profileId: string;
certificateOrder: {
altNames: Array<{
type: ACMESANType;
value: string;
}>;
validity: {
ttl: string;
};
commonName?: string;
keyUsages?: CertKeyUsageType[];
extendedKeyUsages?: CertExtendedKeyUsageType[];
notBefore?: Date;
notAfter?: Date;
signatureAlgorithm?: string;
keyAlgorithm?: string;
};
} & Omit<TProjectPermission, "projectId">;
export type TCertificateFromProfileResponse = {
certificate: string;
issuingCaCertificate: string;
certificateChain: string;
privateKey?: string;
serialNumber: string;
certificateId: string;
projectId: string;
profileName: string;
};
export type TCertificateOrderResponse = {
orderId: string;
status: CertificateOrderStatus;
subjectAlternativeNames: Array<{
type: ACMESANType;
value: string;
status: CertificateOrderStatus;
}>;
authorizations: Array<{
identifier: {
type: ACMESANType;
value: string;
};
status: CertificateOrderStatus;
expires?: string;
challenges: Array<{
type: string;
status: CertificateOrderStatus;
url: string;
token: string;
}>;
}>;
finalize: string;
certificate?: string;
projectId: string;
profileName: string;
};
@@ -8,14 +8,26 @@ import { TCertificateSecretDALFactory } from "./certificate-secret-dal";
export enum CertStatus { export enum CertStatus {
ACTIVE = "active", ACTIVE = "active",
EXPIRED = "expired",
REVOKED = "revoked" REVOKED = "revoked"
} }
export enum CertKeyAlgorithm { export enum CertKeyAlgorithm {
RSA_2048 = "RSA_2048", RSA_2048 = "RSA_2048",
RSA_3072 = "RSA_3072",
RSA_4096 = "RSA_4096", RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1", ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1" ECDSA_P384 = "EC_secp384r1",
ECDSA_P521 = "EC_secp521r1"
}
export enum CertSignatureAlgorithm {
RSA_SHA256 = "RSA-SHA256",
RSA_SHA384 = "RSA-SHA384",
RSA_SHA512 = "RSA-SHA512",
ECDSA_SHA256 = "ECDSA-SHA256",
ECDSA_SHA384 = "ECDSA-SHA384",
ECDSA_SHA512 = "ECDSA-SHA512"
} }
export enum CertKeyUsage { export enum CertKeyUsage {
@@ -39,6 +51,11 @@ export enum CertExtendedKeyUsage {
OCSP_SIGNING = "ocspSigning" OCSP_SIGNING = "ocspSigning"
} }
export enum CertSignatureType {
RSA = "RSA",
ECDSA = "ECDSA"
}
export const CertExtendedKeyUsageOIDToName: Record<string, CertExtendedKeyUsage> = { export const CertExtendedKeyUsageOIDToName: Record<string, CertExtendedKeyUsage> = {
[x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH, [x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH,
[x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH, [x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH,
@@ -105,13 +122,48 @@ export type TGetCertificateCredentialsDTO = {
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
export enum CertSubjectAlternativeNameType {
DNS_NAME = "dns_name",
IP_ADDRESS = "ip_address",
EMAIL = "email",
URI = "uri"
}
export enum TAltNameType { export enum TAltNameType {
EMAIL = "email", EMAIL = "email",
DNS = "dns", DNS = "dns",
IP = "ip", IP = "ip",
URL = "url" URL = "url"
} }
export const mapLegacyAltNameType = (legacyType: TAltNameType): CertSubjectAlternativeNameType => {
switch (legacyType) {
case TAltNameType.EMAIL:
return CertSubjectAlternativeNameType.EMAIL;
case TAltNameType.DNS:
return CertSubjectAlternativeNameType.DNS_NAME;
case TAltNameType.IP:
return CertSubjectAlternativeNameType.IP_ADDRESS;
case TAltNameType.URL:
return CertSubjectAlternativeNameType.URI;
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unknown legacy alt name type: ${legacyType}`);
}
};
export type TAltNameMapping = { export type TAltNameMapping = {
type: TAltNameType; type: TAltNameType;
value: string; value: string;
}; };
export enum ACMESANType {
DNS = "dns",
IP = "ip"
}
export enum CertificateOrderStatus {
PENDING = "pending",
PROCESSING = "processing",
VALID = "valid",
INVALID = "invalid"
}
@@ -0,0 +1,118 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { TApiEnrollmentConfigInsert, TApiEnrollmentConfigUpdate } from "./enrollment-config-types";
export type TApiEnrollmentConfigDALFactory = ReturnType<typeof apiEnrollmentConfigDALFactory>;
export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
const apiEnrollmentConfigOrm = ormify(db, TableName.PkiApiEnrollmentConfig);
const create = async (data: TApiEnrollmentConfigInsert, tx?: Knex) => {
try {
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).insert(data).returning("*");
return apiConfig;
} catch (error) {
throw new DatabaseError({ error, name: "Create API enrollment config" });
}
};
const updateById = async (id: string, data: TApiEnrollmentConfigUpdate, tx?: Knex) => {
try {
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).update(data).returning("*");
return apiConfig;
} catch (error) {
throw new DatabaseError({ error, name: "Update API enrollment config" });
}
};
const deleteById = async (id: string, tx?: Knex) => {
try {
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).del().returning("*");
return apiConfig;
} catch (error) {
throw new DatabaseError({ error, name: "Delete API enrollment config" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const apiConfig = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).first();
return apiConfig;
} catch (error) {
throw new DatabaseError({ error, name: "Find API enrollment config by id" });
}
};
const findProfilesForAutoRenewal = async (renewalThresholdDays: number = 30, projectId?: string, tx?: Knex) => {
try {
let query = (tx || db)(TableName.PkiCertificateProfile)
.join(
TableName.PkiApiEnrollmentConfig,
`${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id`
)
.where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true);
if (projectId) {
query = query.where(`${TableName.PkiCertificateProfile}.projectId`, projectId);
}
const profiles = await query
.where((qb) => {
void qb
.whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`)
.orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays);
})
.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 as Array<{ id: string; name: string; projectId: string; autoRenewDays?: number }>;
} catch (error) {
throw new DatabaseError({ error, name: "Find profiles for auto renewal" });
}
};
const isConfigInUse = async (configId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.PkiCertificateProfile).where({ apiConfigId: configId }).count("*").first();
if (!doc || typeof doc !== "object") {
return 0;
}
const countValue = (doc as Record<string, unknown>).count;
if (typeof countValue === "number") {
return countValue;
}
if (typeof countValue === "string") {
const parsed = parseInt(countValue, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
return 0;
} catch (error) {
throw new DatabaseError({ error, name: "Check if API enrollment config is in use" });
}
};
return {
...apiEnrollmentConfigOrm,
create,
updateById,
deleteById,
findById,
findProfilesForAutoRenewal,
isConfigInUse
};
};
@@ -0,0 +1,29 @@
import {
TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate
} from "@app/db/schemas/pki-api-enrollment-configs";
import {
TPkiEstEnrollmentConfigs,
TPkiEstEnrollmentConfigsInsert,
TPkiEstEnrollmentConfigsUpdate
} from "@app/db/schemas/pki-est-enrollment-configs";
export type TEstEnrollmentConfig = TPkiEstEnrollmentConfigs;
export type TEstEnrollmentConfigInsert = TPkiEstEnrollmentConfigsInsert;
export type TEstEnrollmentConfigUpdate = TPkiEstEnrollmentConfigsUpdate;
export type TApiEnrollmentConfig = TPkiApiEnrollmentConfigs;
export type TApiEnrollmentConfigInsert = TPkiApiEnrollmentConfigsInsert;
export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate;
export interface TEstConfigData {
disableBootstrapCaValidation: boolean;
passphrase: string;
caChain?: string;
}
export interface TApiConfigData {
autoRenew: boolean;
autoRenewDays?: number;
}
@@ -0,0 +1,61 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { TEstEnrollmentConfigInsert, TEstEnrollmentConfigUpdate } from "./enrollment-config-types";
export type TEstEnrollmentConfigDALFactory = ReturnType<typeof estEnrollmentConfigDALFactory>;
export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
const estEnrollmentConfigOrm = ormify(db, TableName.PkiEstEnrollmentConfig);
const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => {
try {
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) {
throw new DatabaseError({ error, name: "Create EST enrollment config" });
}
};
const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => {
try {
const result = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*");
const [estConfig] = result;
if (!estConfig) {
return null;
}
return estConfig;
} catch (error) {
throw new DatabaseError({ error, name: "Update EST enrollment config" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first();
return estConfig || null;
} catch (error) {
throw new DatabaseError({ error, name: "Find EST enrollment config by id" });
}
};
return {
...estEnrollmentConfigOrm,
create,
updateById,
findById
};
};
@@ -1,9 +1,33 @@
import { OrgMembershipRole } from "@app/db/schemas"; import { OrgMembershipRole } from "@app/db/schemas";
import {
AuditLogInfo,
EventType,
SecretApprovalEvent,
TAuditLogServiceFactory
} from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection/app-connection-enums";
import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
import {
getHCVaultAuthMounts,
getHCVaultKubernetesAuthRoles,
getHCVaultSecretsForPath,
HCVaultAuthType,
listHCVaultMounts,
listHCVaultPolicies,
listHCVaultSecretPaths,
THCVaultConnection
} from "../app-connection/hc-vault";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { SecretProtectionType } from "../secret/secret-types";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { import {
decryptEnvKeyDataFn, decryptEnvKeyDataFn,
@@ -15,16 +39,29 @@ import { TExternalMigrationQueueFactory } from "./external-migration-queue";
import { import {
ExternalMigrationProviders, ExternalMigrationProviders,
ExternalPlatforms, ExternalPlatforms,
TCreateVaultExternalMigrationDTO,
TDeleteVaultExternalMigrationDTO,
THasCustomVaultMigrationDTO, THasCustomVaultMigrationDTO,
TImportEnvKeyDataDTO, TImportEnvKeyDataDTO,
TImportVaultDataDTO TImportVaultDataDTO,
TUpdateVaultExternalMigrationDTO,
VaultImportStatus
} from "./external-migration-types"; } from "./external-migration-types";
import { TVaultExternalMigrationConfigDALFactory } from "./vault-external-migration-config-dal";
type TExternalMigrationServiceFactoryDep = { type TExternalMigrationServiceFactoryDep = {
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
secretService: TSecretServiceFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
externalMigrationQueue: TExternalMigrationQueueFactory; externalMigrationQueue: TExternalMigrationQueueFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
vaultExternalMigrationConfigDAL: Pick<
TVaultExternalMigrationConfigDALFactory,
"create" | "findOne" | "transaction" | "find" | "updateById" | "deleteById" | "findById"
>;
userDAL: Pick<TUserDALFactory, "findById">; userDAL: Pick<TUserDALFactory, "findById">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>; export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
@@ -33,7 +70,12 @@ export const externalMigrationServiceFactory = ({
permissionService, permissionService,
externalMigrationQueue, externalMigrationQueue,
userDAL, userDAL,
gatewayService gatewayService,
secretService,
auditLogService,
appConnectionService,
vaultExternalMigrationConfigDAL,
kmsService
}: TExternalMigrationServiceFactoryDep) => { }: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({ const importEnvKeyData = async ({
decryptionKey, decryptionKey,
@@ -171,9 +213,554 @@ export const externalMigrationServiceFactory = ({
return actorOrgId in vaultMigrationTransformMappings; return actorOrgId in vaultMigrationTransformMappings;
}; };
const validateVaultExternalMigrationConnection = async ({
connection,
namespace
}: {
connection: THCVaultConnection;
namespace: string;
}) => {
// Allow root namespace access when no namespace is configured on the connection
const isRootAccess = namespace === "root" || namespace === "/";
const hasNoNamespace = connection.credentials.namespace === undefined;
if (hasNoNamespace && isRootAccess) {
// Skip validation for root access with no configured namespace
} else if (connection.credentials.namespace !== namespace) {
throw new BadRequestError({ message: "Namespace value does not match the namespace of the connection" });
}
try {
await listHCVaultPolicies(namespace, connection, gatewayService);
await getHCVaultAuthMounts(namespace, HCVaultAuthType.Kubernetes, connection, gatewayService);
const mounts = await listHCVaultMounts(connection, gatewayService);
const sampleKvMount = mounts.find((mount) => mount.type === "kv");
if (sampleKvMount) {
await listHCVaultSecretPaths(namespace, connection, gatewayService, sampleKvMount.path);
}
} catch (error) {
throw new BadRequestError({
message: `Failed to establish namespace confiugration. ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" });
}
const connection = await appConnectionService.connectAppConnectionById<THCVaultConnection>(
AppConnection.HCVault,
connectionId,
actor
);
await validateVaultExternalMigrationConnection({
connection,
namespace
});
try {
const config = await vaultExternalMigrationConfigDAL.create({
namespace,
connectionId,
orgId: actor.orgId
});
return config;
} catch (error) {
if (
error instanceof DatabaseError &&
(error.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
) {
throw new BadRequestError({
message: `Vault external migration already exists for this namespace`
});
}
throw error;
}
};
const updateVaultExternalMigration = async ({
id,
namespace,
connectionId,
actor
}: TUpdateVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" });
}
if (connectionId) {
const connection = await appConnectionService.connectAppConnectionById<THCVaultConnection>(
AppConnection.HCVault,
connectionId,
actor
);
await validateVaultExternalMigrationConnection({
connection,
namespace
});
}
const config = await vaultExternalMigrationConfigDAL.updateById(id, {
namespace,
connectionId
});
return config;
};
const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" });
}
const configs = await vaultExternalMigrationConfigDAL.find({
orgId: actor.orgId
});
return configs;
};
const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" });
}
// Get all configured namespaces for this org
const vaultConfigs = await vaultExternalMigrationConfigDAL.find({
orgId: actor.orgId
});
// Return the configured namespaces as an array of objects with id and name
// where both id and name are the namespace path
const namespaces = vaultConfigs.map((config) => ({
id: config.namespace,
name: config.namespace
}));
return namespaces;
};
const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault policies" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const policies = await listHCVaultPolicies(namespace, connection, gatewayService);
return policies;
};
const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
return mounts;
};
const getVaultSecretPaths = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const secretPaths = await listHCVaultSecretPaths(namespace, connection, gatewayService, mountPath);
return secretPaths;
};
const importVaultSecrets = async ({
actor,
projectId,
environment,
secretPath,
vaultNamespace,
vaultSecretPath,
auditLogInfo
}: {
actor: OrgServiceActor;
projectId: string;
environment: string;
secretPath: string;
vaultNamespace: string;
vaultSecretPath: string;
auditLogInfo: AuditLogInfo;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace: vaultNamespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const vaultSecrets = await getHCVaultSecretsForPath(vaultNamespace, vaultSecretPath, connection, gatewayService);
try {
const secretOperation = await secretService.createManySecretsRaw({
actorId: actor.id,
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
secretPath,
environment,
projectId,
secrets: Object.entries(vaultSecrets).map(([secretKey, secretValue]) => ({
secretKey,
secretValue
}))
});
if (secretOperation.type === SecretProtectionType.Approval) {
await auditLogService.createAuditLog({
projectId,
...auditLogInfo,
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: secretOperation.approval.committerUserId,
secretApprovalRequestId: secretOperation.approval.id,
secretApprovalRequestSlug: secretOperation.approval.slug,
secretPath,
environment,
secrets: Object.entries(vaultSecrets).map(([secretKey]) => ({
secretKey
})),
eventType: SecretApprovalEvent.CreateMany
}
}
});
return { status: VaultImportStatus.ApprovalRequired };
}
return { status: VaultImportStatus.Imported };
} catch (error) {
throw new BadRequestError({
message: `Failed to import Vault secrets. ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" });
}
const config = await vaultExternalMigrationConfigDAL.findById(id);
if (!config) {
throw new NotFoundError({ message: "Vault migration config not found" });
}
if (config.orgId !== actor.orgId) {
throw new ForbiddenRequestError({ message: "Config does not belong to this organization" });
}
const deletedConfig = await vaultExternalMigrationConfigDAL.deleteById(id);
return deletedConfig;
};
const getVaultAuthMounts = async ({
actor,
namespace,
authType
}: {
actor: OrgServiceActor;
namespace: string;
authType?: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault auth mounts" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const authMounts = await getHCVaultAuthMounts(namespace, authType as HCVaultAuthType, connection, gatewayService);
return authMounts;
};
const getVaultKubernetesAuthRoles = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
// Get roles for the specified mount path only
const roles = await getHCVaultKubernetesAuthRoles(namespace, mountPath, connection, gatewayService);
return roles;
};
return { return {
importEnvKeyData, importEnvKeyData,
importVaultData, importVaultData,
hasCustomVaultMigration hasCustomVaultMigration,
createVaultExternalMigration,
getVaultExternalMigrationConfigs,
updateVaultExternalMigration,
deleteVaultExternalMigration,
getVaultNamespaces,
getVaultPolicies,
getVaultMounts,
getVaultAuthMounts,
getVaultSecretPaths,
importVaultSecrets,
getVaultKubernetesAuthRoles
}; };
}; };

Some files were not shown because too many files have changed in this diff Show More