mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-22 20:16:00 +00:00
Merge branch 'heads/main' into daniel/hsm-improvements-prep-work
This commit is contained in:
@@ -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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+8
@@ -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;
|
||||||
|
|||||||
Vendored
+40
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+128
@@ -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: []
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
});
|
});
|
||||||
|
|||||||
+74
-13
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -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
|
||||||
|
});
|
||||||
+1745
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user