mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 05:29:25 +00:00
Merge branch 'main' into ENG-3955
This commit is contained in:
@@ -40,7 +40,7 @@ describe("Secret Folder Router", async () => {
|
||||
{ name: "folder1", path: "/" }, // one in root
|
||||
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
|
||||
{ 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 }) => {
|
||||
const createdFolder = await createFolder({ path, name });
|
||||
// check for default environments
|
||||
@@ -57,7 +57,7 @@ describe("Secret Folder Router", async () => {
|
||||
{
|
||||
path: "/",
|
||||
expected: {
|
||||
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
|
||||
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
|
||||
length: 3
|
||||
}
|
||||
},
|
||||
@@ -162,4 +162,25 @@ describe("Secret Folder Router", async () => {
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ describe("Secret Folder Router", async () => {
|
||||
{ name: "folder1", path: "/" }, // one in root
|
||||
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
|
||||
{ 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 }) => {
|
||||
const createdFolder = await createFolder({ path, name });
|
||||
// check for default environments
|
||||
@@ -58,7 +58,7 @@ describe("Secret Folder Router", async () => {
|
||||
{
|
||||
path: "/",
|
||||
expected: {
|
||||
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
|
||||
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
|
||||
length: 3
|
||||
}
|
||||
},
|
||||
@@ -163,4 +163,26 @@ describe("Secret Folder Router", async () => {
|
||||
expect(updatedFolderList).toHaveProperty("folders");
|
||||
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
+12
@@ -48,6 +48,7 @@ import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh
|
||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||
import { TSshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service";
|
||||
import { TSubOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service";
|
||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types";
|
||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||
import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service";
|
||||
@@ -61,7 +62,11 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/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 { 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 { TConvertorServiceFactory } from "@app/services/convertor/convertor-service";
|
||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
@@ -178,6 +183,8 @@ declare module "fastify" {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
orgId: string;
|
||||
parentOrgId: string;
|
||||
rootOrgId: string;
|
||||
};
|
||||
rateLimits: RateLimitConfiguration;
|
||||
// passport data
|
||||
@@ -262,7 +269,10 @@ declare module "fastify" {
|
||||
auditLog: TAuditLogServiceFactory;
|
||||
auditLogStream: TAuditLogStreamServiceFactory;
|
||||
certificate: TCertificateServiceFactory;
|
||||
certificateV3: TCertificateV3ServiceFactory;
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
|
||||
certificateProfile: TCertificateProfileServiceFactory;
|
||||
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||
sshHost: TSshHostServiceFactory;
|
||||
@@ -270,6 +280,7 @@ declare module "fastify" {
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
certificateEstV3: TCertificateEstV3ServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||
pkiSync: TPkiSyncServiceFactory;
|
||||
@@ -327,6 +338,7 @@ declare module "fastify" {
|
||||
additionalPrivilege: TAdditionalPrivilegeServiceFactory;
|
||||
role: TRoleServiceFactory;
|
||||
convertor: TConvertorServiceFactory;
|
||||
subOrganization: TSubOrgServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+32
@@ -266,12 +266,24 @@ import {
|
||||
TPkiAlerts,
|
||||
TPkiAlertsInsert,
|
||||
TPkiAlertsUpdate,
|
||||
TPkiApiEnrollmentConfigs,
|
||||
TPkiApiEnrollmentConfigsInsert,
|
||||
TPkiApiEnrollmentConfigsUpdate,
|
||||
TPkiCertificateProfiles,
|
||||
TPkiCertificateProfilesInsert,
|
||||
TPkiCertificateProfilesUpdate,
|
||||
TPkiCertificateTemplatesV2,
|
||||
TPkiCertificateTemplatesV2Insert,
|
||||
TPkiCertificateTemplatesV2Update,
|
||||
TPkiCollectionItems,
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate,
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate,
|
||||
TPkiEstEnrollmentConfigs,
|
||||
TPkiEstEnrollmentConfigsInsert,
|
||||
TPkiEstEnrollmentConfigsUpdate,
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate,
|
||||
@@ -674,6 +686,26 @@ declare module "knex/types/tables" {
|
||||
TCertificateTemplatesInsert,
|
||||
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<
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
|
||||
@@ -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,66 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
if (!hasParentOrgId) {
|
||||
await knex.schema.alterTable(TableName.Organization, async (t) => {
|
||||
// the one just above the chain
|
||||
t.uuid("parentOrgId");
|
||||
t.foreign("parentOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
// this would root organization containing various informations like billing etc
|
||||
t.uuid("rootOrgId");
|
||||
t.foreign("rootOrgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
|
||||
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
||||
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
||||
});
|
||||
|
||||
// had to switch to raw for null not distinct
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (!hasIdentityOrgCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(
|
||||
`
|
||||
UPDATE ?? AS identity
|
||||
SET "orgId" = membership."scopeOrgId"
|
||||
FROM ?? AS membership
|
||||
WHERE
|
||||
membership."actorIdentityId" = identity."id"
|
||||
AND membership."scope" = ?
|
||||
`,
|
||||
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
const hasRootOrgId = await knex.schema.hasColumn(TableName.Organization, "rootOrgId");
|
||||
if (hasParentOrgId || hasRootOrgId) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (hasParentOrgId) t.dropColumn("parentOrgId");
|
||||
if (hasRootOrgId) t.dropColumn("rootOrgId");
|
||||
});
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.dropColumn("orgId");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export const CertificatesSchema = z.object({
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
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>;
|
||||
|
||||
@@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({
|
||||
authMethod: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
hasDeleteProtection: z.boolean().default(false)
|
||||
hasDeleteProtection: z.boolean().default(false),
|
||||
orgId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TIdentities = z.infer<typeof IdentitiesSchema>;
|
||||
|
||||
@@ -92,8 +92,12 @@ export * from "./pam-folders";
|
||||
export * from "./pam-resources";
|
||||
export * from "./pam-sessions";
|
||||
export * from "./pki-alerts";
|
||||
export * from "./pki-api-enrollment-configs";
|
||||
export * from "./pki-certificate-profiles";
|
||||
export * from "./pki-certificate-templates-v2";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./pki-est-enrollment-configs";
|
||||
export * from "./pki-subscribers";
|
||||
export * from "./pki-syncs";
|
||||
export * from "./project-bots";
|
||||
|
||||
@@ -23,6 +23,10 @@ export enum TableName {
|
||||
CertificateBody = "certificate_bodies",
|
||||
CertificateSecret = "certificate_secrets",
|
||||
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",
|
||||
PkiAlert = "pki_alerts",
|
||||
PkiCollection = "pki_collections",
|
||||
@@ -312,6 +316,12 @@ export enum ActionProjectType {
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
export enum OrganizationActionScope {
|
||||
ChildOrganization = "child-organization-only",
|
||||
ParentOrganization = "parent-organization-only",
|
||||
Any = "any"
|
||||
}
|
||||
|
||||
export enum TemporaryPermissionMode {
|
||||
Relative = "relative"
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ export const OrganizationsSchema = z.object({
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||
parentOrgId: z.string().uuid().nullable().optional(),
|
||||
rootOrgId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -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>
|
||||
>;
|
||||
@@ -24,7 +24,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
// @ts-ignore
|
||||
id: seedData1.machineIdentity.id,
|
||||
name: seedData1.machineIdentity.name,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
orgId: seedData1.organization.id
|
||||
}
|
||||
]);
|
||||
const identityUa = await knex(TableName.IdentityUniversalAuth)
|
||||
|
||||
@@ -8,6 +8,35 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
||||
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
|
||||
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
@@ -59,11 +88,29 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
return;
|
||||
}
|
||||
|
||||
const certificateTemplateId = urlFragments.slice(-2)[0];
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
const identifier = urlFragments.slice(-2)[0];
|
||||
|
||||
const identifierType = await getIdentifierType(identifier);
|
||||
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) {
|
||||
throw new BadRequestError({
|
||||
@@ -95,14 +142,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simpleenroll",
|
||||
url: "/:identifier/simpleenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
identifier: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
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-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({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
certificateTemplateId: identifier,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
@@ -122,14 +183,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simplereenroll",
|
||||
url: "/:identifier/simplereenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
identifier: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
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-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({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
certificateTemplateId: identifier,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
@@ -149,13 +224,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/cacerts",
|
||||
url: "/:identifier/cacerts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
identifier: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
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-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({
|
||||
certificateTemplateId: req.params.certificateTemplateId
|
||||
certificateTemplateId: identifier
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,12 +48,14 @@ import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||
import { registerSshHostGroupRouter } from "./ssh-host-group-router";
|
||||
import { registerSshHostRouter } from "./ssh-host-router";
|
||||
import { registerSubOrgRouter } from "./sub-org-router";
|
||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||
|
||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||
await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" });
|
||||
await server.register(registerLicenseRouter, { prefix: "/organizations" });
|
||||
|
||||
// depreciated in favour of infisical workspace
|
||||
|
||||
@@ -58,7 +58,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||
const plan = await server.services.license.getOrgPlan({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorOrgId: req.permission.rootOrgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.params.organizationId,
|
||||
refreshCache: req.query.refreshCache
|
||||
|
||||
@@ -3,12 +3,35 @@ import { z } from "zod";
|
||||
|
||||
import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionSchema, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const INVALID_SUBORG_PERMISSIONS = [
|
||||
OrgPermissionSubjects.Sso,
|
||||
OrgPermissionSubjects.Ldap,
|
||||
OrgPermissionSubjects.Scim,
|
||||
OrgPermissionSubjects.GithubOrgSync,
|
||||
OrgPermissionSubjects.GithubOrgSyncManual,
|
||||
OrgPermissionSubjects.Billing,
|
||||
OrgPermissionSubjects.SubOrganization
|
||||
];
|
||||
|
||||
const validateSubOrganizationSubjects = (permissions: unknown) => {
|
||||
const invalidPermissionSubjects = (permissions as { subject: OrgPermissionSubjects }[])
|
||||
.filter((el) => INVALID_SUBORG_PERMISSIONS.includes(el.subject))
|
||||
.map((el) => el.subject);
|
||||
if (invalidPermissionSubjects.length) {
|
||||
const deduplication = Array.from(new Set(invalidPermissionSubjects));
|
||||
throw new BadRequestError({
|
||||
message: `Suborganization contains invalid permission subjects: ${deduplication.join(",")}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@@ -37,6 +60,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId;
|
||||
if (isSubOrganization) {
|
||||
validateSubOrganizationSubjects(req.body.permissions);
|
||||
}
|
||||
|
||||
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
|
||||
const role = await server.services.role.createRole({
|
||||
permission: req.permission,
|
||||
@@ -133,6 +161,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const isSubOrganization = req.permission.rootOrgId !== req.permission.orgId;
|
||||
if (isSubOrganization && req.body.permissions) {
|
||||
validateSubOrganizationSubjects(req.body.permissions);
|
||||
}
|
||||
|
||||
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
|
||||
const role = await server.services.role.updateRole({
|
||||
permission: req.permission,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, SUB_ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const sanitizedSubOrganizationSchema = OrganizationsSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
parentOrgId: true
|
||||
});
|
||||
|
||||
export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SubOrganizations],
|
||||
description: "Create a sub organization",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
body: z.object({
|
||||
name: slugSchema().describe(SUB_ORGANIZATIONS.CREATE.name)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: sanitizedSubOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { organization } = await server.services.subOrganization.createSubOrg({
|
||||
name: req.body.name,
|
||||
permissionActor: req.permission
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_SUB_ORGANIZATION,
|
||||
metadata: {
|
||||
name: req.body.name,
|
||||
organizationId: organization.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SubOrganizations],
|
||||
description: "List of sub organizations",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
limit: z.coerce.number().min(1).max(1000).default(25).describe(SUB_ORGANIZATIONS.LIST.limit),
|
||||
offset: z.coerce.number().min(0).default(0).describe(SUB_ORGANIZATIONS.LIST.offset),
|
||||
isAccessible: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((value) => value === "true")
|
||||
.describe(SUB_ORGANIZATIONS.LIST.isAccessible)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organizations: sanitizedSubOrganizationSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { organizations } = await server.services.subOrganization.listSubOrgs({
|
||||
permissionActor: req.permission,
|
||||
data: {
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
isAccessible: req.query.isAccessible
|
||||
}
|
||||
});
|
||||
|
||||
return { organizations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:subOrgId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SubOrganizations],
|
||||
description: "Update a sub organization",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
subOrgId: z.string().trim().describe(SUB_ORGANIZATIONS.UPDATE.subOrgId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema().describe(SUB_ORGANIZATIONS.UPDATE.name)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: sanitizedSubOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { organization } = await server.services.subOrganization.updateSubOrg({
|
||||
subOrgId: req.params.subOrgId,
|
||||
name: req.body.name,
|
||||
permissionActor: req.permission
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SUB_ORGANIZATION,
|
||||
metadata: {
|
||||
name: req.body.name,
|
||||
organizationId: organization.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { TAuditLogs } from "@app/db/schemas";
|
||||
import { OrganizationActionScope, TAuditLogs } from "@app/db/schemas";
|
||||
import {
|
||||
decryptLogStream,
|
||||
decryptLogStreamCredentials,
|
||||
@@ -45,13 +45,14 @@ export const auditLogStreamServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
@@ -94,13 +95,14 @@ export const auditLogStreamServiceFactory = ({
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
logStream.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
@@ -160,13 +162,14 @@ export const auditLogStreamServiceFactory = ({
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
logStream.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||
|
||||
@@ -185,14 +188,14 @@ export const auditLogStreamServiceFactory = ({
|
||||
const logStream = await auditLogStreamDAL.findById(logStreamId);
|
||||
|
||||
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
logStream.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
@@ -206,13 +209,14 @@ export const auditLogStreamServiceFactory = ({
|
||||
};
|
||||
|
||||
const list = async (actor: OrgServiceActor) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
@@ -47,13 +47,14 @@ export const auditLogServiceFactory = ({
|
||||
);
|
||||
} else {
|
||||
// Organization-wide logs
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAuditLogsActions.Read,
|
||||
|
||||
@@ -173,6 +173,9 @@ export enum EventType {
|
||||
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
|
||||
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
|
||||
|
||||
CREATE_SUB_ORGANIZATION = "create-sub-organization",
|
||||
UPDATE_SUB_ORGANIZATION = "update-sub-organization",
|
||||
|
||||
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
|
||||
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
|
||||
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
|
||||
@@ -352,9 +355,18 @@ export enum EventType {
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-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_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
@@ -607,6 +619,22 @@ interface GetSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSubOrganizationEvent {
|
||||
type: EventType.CREATE_SUB_ORGANIZATION;
|
||||
metadata: {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSubOrganizationEvent {
|
||||
type: EventType.UPDATE_SUB_ORGANIZATION;
|
||||
metadata: {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
type TSecretMetadata = { key: string; value: string }[];
|
||||
|
||||
interface GetSecretEvent {
|
||||
@@ -2512,46 +2540,6 @@ interface LoadProjectKmsBackupEvent {
|
||||
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 {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
@@ -2598,6 +2586,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 {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
@@ -3863,6 +3983,8 @@ interface PamResourceDeleteEvent {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateSubOrganizationEvent
|
||||
| UpdateSubOrganizationEvent
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
@@ -4051,13 +4173,22 @@ export type Event =
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent
|
||||
| OrgAdminBypassSSOEvent
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| DeleteCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| ListCertificateTemplates
|
||||
| CreateCertificateProfile
|
||||
| UpdateCertificateProfile
|
||||
| DeleteCertificateProfile
|
||||
| GetCertificateProfile
|
||||
| ListCertificateProfiles
|
||||
| IssueCertificateFromProfile
|
||||
| SignCertificateFromProfile
|
||||
| OrderCertificateFromProfile
|
||||
| GetAzureAdCsTemplatesEvent
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import {
|
||||
@@ -134,13 +134,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
isGatewayV1 = false;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
gateway?.orgId ?? gatewayv2?.orgId,
|
||||
orgId: gateway?.orgId || gatewayv2?.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
@@ -297,13 +298,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
isGatewayV1 = false;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: gateway?.orgId || gatewayv2?.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { STSServiceException } from "@aws-sdk/client-sts";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
@@ -51,13 +52,14 @@ export const externalKmsServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@@ -154,13 +156,14 @@ export const externalKmsServiceFactory = ({
|
||||
actorAuthMethod
|
||||
}: TUpdateExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
orgId: kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
|
||||
const plan = await licenseService.getPlan(kmsDoc.orgId);
|
||||
@@ -257,13 +260,14 @@ export const externalKmsServiceFactory = ({
|
||||
|
||||
const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
orgId: kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
@@ -278,13 +282,14 @@ export const externalKmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
|
||||
@@ -294,13 +299,14 @@ export const externalKmsServiceFactory = ({
|
||||
|
||||
const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
orgId: kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
@@ -342,13 +348,14 @@ export const externalKmsServiceFactory = ({
|
||||
name: kmsName
|
||||
}: TGetExternalKmsBySlugDTO) => {
|
||||
const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
orgId: kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
|
||||
@@ -3,7 +3,7 @@ import net from "node:net";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { OrgMembershipRole, TRelays } from "@app/db/schemas";
|
||||
import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
@@ -73,13 +73,14 @@ export const gatewayV2ServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
actorOrgId: orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
@@ -258,13 +259,14 @@ export const gatewayV2ServiceFactory = ({
|
||||
};
|
||||
|
||||
const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
@@ -815,13 +817,14 @@ export const gatewayV2ServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Gateway ${id} not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
gateway.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: gateway.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.DeleteGateways,
|
||||
@@ -845,13 +848,14 @@ export const gatewayV2ServiceFactory = ({
|
||||
};
|
||||
|
||||
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
@@ -68,13 +69,14 @@ export const gatewayServiceFactory = ({
|
||||
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
actorOrgId: orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
@@ -480,13 +482,14 @@ export const gatewayServiceFactory = ({
|
||||
};
|
||||
|
||||
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
@@ -501,13 +504,14 @@ export const gatewayServiceFactory = ({
|
||||
};
|
||||
|
||||
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
@@ -521,13 +525,14 @@ export const gatewayServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.EditGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
@@ -542,13 +547,14 @@ export const gatewayServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.DeleteGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
|
||||
@@ -6,7 +6,7 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql";
|
||||
import { Octokit as OctokitRest } from "@octokit/rest";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { AccessScope, OrgMembershipRole } from "@app/db/schemas";
|
||||
import { AccessScope, OrganizationActionScope, OrgMembershipRole } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -104,13 +104,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
githubOrgAccessToken,
|
||||
isActive
|
||||
}: TCreateGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
@@ -162,13 +163,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
githubOrgAccessToken,
|
||||
isActive
|
||||
}: TUpdateGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
@@ -226,13 +228,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: orgPermission.type,
|
||||
actorId: orgPermission.id,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
@@ -256,13 +259,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: orgPermission.id,
|
||||
actor: orgPermission.type,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
@@ -422,13 +426,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: orgPermission.id,
|
||||
actor: orgPermission.type,
|
||||
orgId: orgPermission.orgId,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
@@ -509,13 +514,14 @@ export const githubOrgSyncServiceFactory = ({
|
||||
};
|
||||
|
||||
const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise<TSyncResult> => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor: orgPermission.type,
|
||||
orgId: orgPermission.orgId,
|
||||
actorId: orgPermission.id,
|
||||
actorAuthMethod: orgPermission.authMethod,
|
||||
actorOrgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { AccessScope, OrgMembershipRole, TRoles } from "@app/db/schemas";
|
||||
import { AccessScope, OrganizationActionScope, OrgMembershipRole, TRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
@@ -73,13 +73,14 @@ export const groupServiceFactory = ({
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@@ -167,13 +168,14 @@ export const groupServiceFactory = ({
|
||||
}: TUpdateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
@@ -270,13 +272,14 @@ export const groupServiceFactory = ({
|
||||
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
@@ -297,17 +300,18 @@ export const groupServiceFactory = ({
|
||||
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findById(id);
|
||||
if (!group) {
|
||||
if (!group || group.orgId !== actorOrgId) {
|
||||
throw new NotFoundError({
|
||||
message: `Cannot find group with ID ${id}`
|
||||
});
|
||||
@@ -330,13 +334,14 @@ export const groupServiceFactory = ({
|
||||
}: TListGroupUsersDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
@@ -365,13 +370,14 @@ export const groupServiceFactory = ({
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
@@ -451,13 +457,14 @@ export const groupServiceFactory = ({
|
||||
}: TRemoveUserFromGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
actorOrgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import {
|
||||
@@ -68,13 +69,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
templateFields: Record<string, unknown>;
|
||||
} & Omit<TOrgPermission, "orgId">) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -113,13 +115,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
orgId: template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -227,13 +230,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
orgId: template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -282,13 +286,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
throw new NotFoundError({ message: "Template not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
template.orgId,
|
||||
orgId: template.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -316,13 +321,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TListIdentityAuthTemplatesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -352,13 +358,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TGetTemplatesByAuthMethodDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -385,13 +392,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TFindTemplateUsagesDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
@@ -415,13 +423,14 @@ export const identityAuthTemplateServiceFactory = ({
|
||||
actorOrgId
|
||||
}: TUnlinkTemplateUsageDTO) => {
|
||||
await $checkPlan(actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
OrgPermissionSubjects.MachineIdentityAuthTemplate
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@@ -46,13 +47,14 @@ export const kmipOperationServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TKmipCreateDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -78,13 +80,14 @@ export const kmipOperationServiceFactory = ({
|
||||
};
|
||||
|
||||
const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -131,13 +134,14 @@ export const kmipOperationServiceFactory = ({
|
||||
};
|
||||
|
||||
const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -189,13 +193,14 @@ export const kmipOperationServiceFactory = ({
|
||||
};
|
||||
|
||||
const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -226,13 +231,14 @@ export const kmipOperationServiceFactory = ({
|
||||
};
|
||||
|
||||
const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -287,13 +293,14 @@ export const kmipOperationServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TKmipGetAttributesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -339,13 +346,14 @@ export const kmipOperationServiceFactory = ({
|
||||
};
|
||||
|
||||
const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
@@ -377,13 +385,14 @@ export const kmipOperationServiceFactory = ({
|
||||
actorOrgId,
|
||||
kmipMetadata
|
||||
}: TKmipRegisterDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
@@ -401,13 +401,14 @@ export const kmipServiceFactory = ({
|
||||
};
|
||||
|
||||
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
@@ -566,7 +567,14 @@ export const kmipServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => {
|
||||
await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId);
|
||||
await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId: actorOrgId
|
||||
@@ -759,13 +767,14 @@ export const kmipServiceFactory = ({
|
||||
keyAlgorithm,
|
||||
hostnamesOrIps
|
||||
}: TRegisterServerDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { AccessScope, OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app/db/schemas";
|
||||
import {
|
||||
AccessScope,
|
||||
OrganizationActionScope,
|
||||
OrgMembershipStatus,
|
||||
TableName,
|
||||
TLdapConfigsUpdate,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -119,7 +126,14 @@ export const ldapConfigServiceFactory = ({
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TCreateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -238,7 +252,14 @@ export const ldapConfigServiceFactory = ({
|
||||
groupSearchFilter,
|
||||
caCert
|
||||
}: TUpdateLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -316,7 +337,14 @@ export const ldapConfigServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetLdapCfgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
return getLdapCfg({
|
||||
orgId
|
||||
@@ -649,7 +677,14 @@ export const ldapConfigServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetLdapGroupMapsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const ldapConfig = await ldapConfigDAL.findOne({
|
||||
@@ -678,7 +713,14 @@ export const ldapConfigServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateLdapGroupMapDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -732,7 +774,14 @@ export const ldapConfigServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TDeleteLdapGroupMapDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -771,7 +820,14 @@ export const ldapConfigServiceFactory = ({
|
||||
caCert,
|
||||
url
|
||||
}: TTestLdapConnectionDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
|
||||
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseCertificateSyncs: false,
|
||||
enterpriseAppConnections: true,
|
||||
machineIdentityAuthTemplates: false
|
||||
machineIdentityAuthTemplates: false,
|
||||
pkiLegacyTemplates: false
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.Membership)
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
|
||||
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
|
||||
.andWhere((bd) => {
|
||||
if (orgId) {
|
||||
@@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.whereNull(`${TableName.Organization}.rootOrgId`)
|
||||
.count();
|
||||
return Number(doc?.[0]?.count ?? 0);
|
||||
} catch (error) {
|
||||
@@ -25,10 +27,31 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const countOfOrgIdentities = async (orgId: string | null, tx?: Knex) => {
|
||||
try {
|
||||
// count org identities
|
||||
const identityDoc = await (tx || db.replicaNode())(TableName.Identity)
|
||||
.join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`)
|
||||
.where((bd) => {
|
||||
if (orgId) {
|
||||
void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId);
|
||||
}
|
||||
})
|
||||
.count();
|
||||
|
||||
const identityCount = Number(identityDoc?.[0].count);
|
||||
|
||||
return identityCount;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count of Org Users + Identities" });
|
||||
}
|
||||
};
|
||||
|
||||
const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => {
|
||||
try {
|
||||
// count org users
|
||||
const userDoc = await (tx || db.replicaNode())(TableName.Membership)
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
|
||||
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
|
||||
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||
.andWhere((bd) => {
|
||||
@@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.Users}.isGhost`, false)
|
||||
.whereNull(`${TableName.Organization}.rootOrgId`)
|
||||
.count();
|
||||
|
||||
const userCount = Number(userDoc?.[0].count);
|
||||
|
||||
// count org identities
|
||||
const identityDoc = await (tx || db.replicaNode())(TableName.Membership)
|
||||
.where({ scope: AccessScope.Organization })
|
||||
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
|
||||
const identityDoc = await (tx || db.replicaNode())(TableName.Identity)
|
||||
.join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`)
|
||||
.where((bd) => {
|
||||
if (orgId) {
|
||||
void bd.where(`${TableName.Membership}.scopeOrgId`, orgId);
|
||||
void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId);
|
||||
}
|
||||
})
|
||||
.count();
|
||||
@@ -61,5 +84,5 @@ export const licenseDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { countOfOrgMembers, countOrgUsersAndIdentities };
|
||||
return { countOfOrgMembers, countOrgUsersAndIdentities, countOfOrgIdentities };
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
subOrganization: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
auditLogs: false,
|
||||
@@ -67,6 +68,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
machineIdentityAuthTemplates: false,
|
||||
pkiLegacyTemplates: false,
|
||||
pam: false
|
||||
});
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import { AxiosError } from "axios";
|
||||
import { CronJob } from "cron";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
@@ -45,11 +45,10 @@ import {
|
||||
} from "./license-types";
|
||||
|
||||
type TLicenseServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findRootOrgDetails" | "countAllOrgMembers" | "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseDAL: TLicenseDALFactory;
|
||||
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
};
|
||||
|
||||
@@ -66,7 +65,6 @@ export const licenseServiceFactory = ({
|
||||
permissionService,
|
||||
licenseDAL,
|
||||
keyStore,
|
||||
identityOrgMembershipDAL,
|
||||
projectDAL
|
||||
}: TLicenseServiceFactoryDep) => {
|
||||
let isValidLicense = false;
|
||||
@@ -199,19 +197,21 @@ export const licenseServiceFactory = ({
|
||||
return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
}
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
const org = await orgDAL.findRootOrgDetails(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
const rootOrgId = org.id;
|
||||
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`
|
||||
);
|
||||
const workspacesUsed = await projectDAL.countOfOrgProjects(orgId);
|
||||
const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId);
|
||||
currentPlan.workspacesUsed = workspacesUsed;
|
||||
|
||||
const membersUsed = await licenseDAL.countOfOrgMembers(orgId);
|
||||
const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId);
|
||||
currentPlan.membersUsed = membersUsed;
|
||||
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
|
||||
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId);
|
||||
currentPlan.identitiesUsed = identityUsed;
|
||||
|
||||
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
|
||||
@@ -284,19 +284,20 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
const org = await orgDAL.findRootOrgDetails(orgId, tx);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
|
||||
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
|
||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
|
||||
const rootOrgId = org.id;
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx);
|
||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx);
|
||||
if (org?.customerId) {
|
||||
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
||||
quantity,
|
||||
quantityIdentities
|
||||
});
|
||||
}
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId));
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
||||
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
||||
@@ -307,7 +308,7 @@ export const licenseServiceFactory = ({
|
||||
usedIdentitySeats
|
||||
});
|
||||
}
|
||||
await refreshPlan(orgId);
|
||||
await refreshPlan(rootOrgId);
|
||||
};
|
||||
|
||||
// below all are api calls
|
||||
@@ -319,7 +320,14 @@ export const licenseServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
billingCycle
|
||||
}: TOrgPlansTableDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
@@ -336,7 +344,14 @@ export const licenseServiceFactory = ({
|
||||
projectId,
|
||||
refreshCache
|
||||
}: TOrgPlanDTO) => {
|
||||
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
if (refreshCache) {
|
||||
await refreshPlan(orgId);
|
||||
}
|
||||
@@ -352,13 +367,20 @@ export const licenseServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
success_url
|
||||
}: TStartOrgTrialDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -384,13 +406,20 @@ export const licenseServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateOrgPortalSession) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: "Organization not found"
|
||||
@@ -433,10 +462,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -502,7 +538,7 @@ export const licenseServiceFactory = ({
|
||||
const getUsageMetrics = async (orgId: string) => {
|
||||
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
|
||||
orgDAL.countAllOrgMembers(orgId),
|
||||
identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }),
|
||||
licenseDAL.countOfOrgIdentities(orgId),
|
||||
projectDAL.countOfOrgProjects(orgId)
|
||||
]);
|
||||
|
||||
@@ -516,10 +552,17 @@ export const licenseServiceFactory = ({
|
||||
|
||||
// returns org current plan feature table
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -553,10 +596,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -578,13 +628,20 @@ export const licenseServiceFactory = ({
|
||||
name,
|
||||
email
|
||||
}: TUpdateOrgBillingDetailsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -601,10 +658,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -628,13 +692,20 @@ export const licenseServiceFactory = ({
|
||||
success_url,
|
||||
cancel_url
|
||||
}: TAddOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -660,13 +731,20 @@ export const licenseServiceFactory = ({
|
||||
orgId,
|
||||
pmtMethodId
|
||||
}: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -692,10 +770,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -710,13 +795,20 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const addOrgTaxId = async ({ actorId, actor, actorAuthMethod, actorOrgId, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -734,13 +826,20 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionBillingActions.ManageBilling,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -754,10 +853,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -771,10 +877,17 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
|
||||
|
||||
const organization = await orgDAL.findOrgById(orgId);
|
||||
const organization = await orgDAL.findById(orgId);
|
||||
if (!organization) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
@@ -819,7 +932,6 @@ export const licenseServiceFactory = ({
|
||||
getLicenseId,
|
||||
invalidateGetPlan,
|
||||
updateSubscriptionOrgMemberCount,
|
||||
refreshPlan,
|
||||
getOrgPlan,
|
||||
getOrgPlansTableByBillCycle,
|
||||
startOrgTrial,
|
||||
|
||||
@@ -33,6 +33,7 @@ export type TFeatureSet = {
|
||||
membersUsed: number;
|
||||
identityLimit: null;
|
||||
identitiesUsed: number;
|
||||
subOrganization: false;
|
||||
environmentLimit: null;
|
||||
environmentsUsed: 0;
|
||||
secretVersioning: true;
|
||||
@@ -78,6 +79,7 @@ export type TFeatureSet = {
|
||||
enterpriseCertificateSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
machineIdentityAuthTemplates: false;
|
||||
pkiLegacyTemplates: false;
|
||||
fips: false;
|
||||
eventSubscriptions: false;
|
||||
pam: false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
||||
|
||||
import { AccessScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
||||
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
@@ -118,13 +118,14 @@ export const oidcConfigServiceFactory = ({
|
||||
}
|
||||
|
||||
if (dto.type === "external") {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
dto.organizationId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: dto.actorId,
|
||||
actor: dto.actor,
|
||||
orgId: dto.organizationId,
|
||||
actorOrgId: dto.actorOrgId,
|
||||
actorAuthMethod: dto.actorAuthMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
|
||||
@@ -508,13 +509,14 @@ export const oidcConfigServiceFactory = ({
|
||||
"Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
org.id,
|
||||
actor,
|
||||
orgId: org.id,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
|
||||
if (org.googleSsoAuthEnforced && isActive) {
|
||||
@@ -602,13 +604,14 @@ export const oidcConfigServiceFactory = ({
|
||||
"Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId,
|
||||
org.id,
|
||||
actor,
|
||||
orgId: org.id,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||
|
||||
if (org.googleSsoAuthEnforced && isActive) {
|
||||
@@ -764,7 +767,14 @@ export const oidcConfigServiceFactory = ({
|
||||
};
|
||||
|
||||
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => {
|
||||
await permissionService.getOrgPermission(ActorType.USER, actor.id, orgId, actor.authMethod, actor.orgId);
|
||||
await permissionService.getOrgPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: actor.id,
|
||||
orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
const oidcConfig = await oidcConfigDAL.findOne({
|
||||
orgId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TPamAccounts, TPamResources } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamResources } from "@app/db/schemas";
|
||||
import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory";
|
||||
import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
@@ -459,13 +459,14 @@ export const pamAccountServiceFactory = ({
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: project.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
@@ -102,13 +102,14 @@ export const pamSessionServiceFactory = ({
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: project.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
@@ -142,13 +143,14 @@ export const pamSessionServiceFactory = ({
|
||||
const project = await projectDAL.findById(session.projectId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
project.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: project.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
if (actor.type === ActorType.IDENTITY) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ProjectPermissionAppConnectionActions,
|
||||
ProjectPermissionAuditLogsActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCertificateProfileActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionCommitsActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
@@ -72,8 +73,8 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionPkiTemplateActions.Edit,
|
||||
ProjectPermissionPkiTemplateActions.Create,
|
||||
ProjectPermissionPkiTemplateActions.Delete,
|
||||
ProjectPermissionPkiTemplateActions.IssueCert,
|
||||
ProjectPermissionPkiTemplateActions.ListCerts
|
||||
ProjectPermissionPkiTemplateActions.IssueCert, // deprecated
|
||||
ProjectPermissionPkiTemplateActions.ListCerts // deprecated
|
||||
],
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
@@ -99,6 +100,17 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateProfileActions.Read,
|
||||
ProjectPermissionCertificateProfileActions.Edit,
|
||||
ProjectPermissionCertificateProfileActions.Create,
|
||||
ProjectPermissionCertificateProfileActions.Delete,
|
||||
ProjectPermissionCertificateProfileActions.IssueCert
|
||||
],
|
||||
ProjectPermissionSub.CertificateProfiles
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||
ProjectPermissionSub.Commits
|
||||
@@ -443,6 +455,7 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can(
|
||||
[
|
||||
@@ -454,7 +467,15 @@ const buildMemberPermissionRules = () => {
|
||||
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.PkiCollections);
|
||||
|
||||
@@ -15,6 +15,11 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubOrgActions {
|
||||
Create = "create",
|
||||
DirectAccess = "direct-access"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAppConnectionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -117,7 +122,8 @@ export enum OrgPermissionSubjects {
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
Relay = "relay",
|
||||
SecretShare = "secret-share"
|
||||
SecretShare = "secret-share",
|
||||
SubOrganization = "sub-organization"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@@ -128,6 +134,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||
| [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Member]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Settings]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
@@ -185,6 +192,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.SubOrganization).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionSubOrgActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||
@@ -308,6 +321,10 @@ const buildAdminPermission = () => {
|
||||
// ws permissions
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);
|
||||
|
||||
can(OrgPermissionSubOrgActions.Create, OrgPermissionSubjects.SubOrganization);
|
||||
can(OrgPermissionSubOrgActions.DirectAccess, OrgPermissionSubjects.SubOrganization);
|
||||
|
||||
// role permission
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
|
||||
|
||||
@@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships {
|
||||
orgAuthEnforced?: boolean | null;
|
||||
orgGoogleSsoAuthEnforced?: boolean | null;
|
||||
shouldUseNewPrivilegeSystem?: boolean | null;
|
||||
rootOrgId?: string | null;
|
||||
bypassOrgAuthEnabled?: boolean | null;
|
||||
roles: {
|
||||
id: string;
|
||||
@@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled")
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("rootOrgId").withSchema(TableName.Organization).as("rootOrgId")
|
||||
);
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
@@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
MembershipsSchema.extend({
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
|
||||
rootOrgId: z.string().optional().nullable(),
|
||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||
bypassOrgAuthEnabled: z.boolean()
|
||||
}).parse(el),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ActionProjectType, TMemberships } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope, TMemberships } from "@app/db/schemas";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionSet } from "./org-permission";
|
||||
@@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = {
|
||||
role: string;
|
||||
}[];
|
||||
|
||||
export type TGetUserProjectPermissionArg = {
|
||||
userId: string;
|
||||
projectId: string;
|
||||
authMethod: ActorAuthMethod;
|
||||
actionProjectType: ActionProjectType;
|
||||
userOrgId?: string;
|
||||
};
|
||||
|
||||
export type TGetIdentityProjectPermissionArg = {
|
||||
identityId: string;
|
||||
projectId: string;
|
||||
identityOrgId?: string;
|
||||
actionProjectType: ActionProjectType;
|
||||
};
|
||||
|
||||
export type TGetServiceTokenProjectPermissionArg = {
|
||||
serviceTokenId: string;
|
||||
projectId: string;
|
||||
@@ -54,17 +39,12 @@ export type TGetOrgPermissionArg = {
|
||||
actorId: string;
|
||||
orgId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId?: string;
|
||||
actorOrgId: string;
|
||||
scope: OrganizationActionScope;
|
||||
};
|
||||
|
||||
export type TPermissionServiceFactory = {
|
||||
getOrgPermission: (
|
||||
type: ActorType,
|
||||
id: string,
|
||||
orgId: string,
|
||||
authMethod: ActorAuthMethod,
|
||||
actorOrgId: string | undefined
|
||||
) => Promise<{
|
||||
getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{
|
||||
permission: MongoAbility<OrgPermissionSet, MongoQuery>;
|
||||
memberships: Array<
|
||||
TMemberships & {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Knex } from "knex";
|
||||
import {
|
||||
AccessScope,
|
||||
ActionProjectType,
|
||||
OrganizationActionScope,
|
||||
OrgMembershipRole,
|
||||
ProjectMembershipRole,
|
||||
ServiceTokenScopes
|
||||
@@ -179,14 +180,15 @@ export const permissionServiceFactory = ({
|
||||
// return minTtl;
|
||||
// };
|
||||
|
||||
const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async (
|
||||
type,
|
||||
id,
|
||||
const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
authMethod,
|
||||
actorOrgId
|
||||
) => {
|
||||
if (type !== ActorType.USER && type !== ActorType.IDENTITY) {
|
||||
actorOrgId,
|
||||
scope,
|
||||
actorAuthMethod
|
||||
}) => {
|
||||
if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid actor provided",
|
||||
name: "Get org permission"
|
||||
@@ -202,11 +204,19 @@ export const permissionServiceFactory = ({
|
||||
scope: AccessScope.Organization,
|
||||
orgId
|
||||
},
|
||||
actorId: id,
|
||||
actorType: type
|
||||
actorId,
|
||||
actorType: actor
|
||||
});
|
||||
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" });
|
||||
|
||||
const rootOrgId = permissionData?.[0]?.rootOrgId;
|
||||
const isChild = Boolean(rootOrgId);
|
||||
if (scope === OrganizationActionScope.ParentOrganization && isChild) {
|
||||
throw new ForbiddenRequestError({ message: `Child organization cannot do this operation` });
|
||||
} else if (scope === OrganizationActionScope.ChildOrganization && !isChild) {
|
||||
throw new ForbiddenRequestError({ message: `Parent organization cannot do this operation` });
|
||||
}
|
||||
|
||||
const permissionFromRoles = permissionData.flatMap((membership) => {
|
||||
const activeRoles = membership?.roles
|
||||
.filter(
|
||||
@@ -227,7 +237,7 @@ export const permissionServiceFactory = ({
|
||||
permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role)));
|
||||
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
actorAuthMethod,
|
||||
permissionData?.[0].orgAuthEnforced,
|
||||
Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced),
|
||||
Boolean(permissionData?.[0].bypassOrgAuthEnabled),
|
||||
|
||||
@@ -111,6 +111,14 @@ export enum ProjectPermissionPkiSubscriberActions {
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCertificateProfileActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueCert = "issue-cert"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -249,7 +257,8 @@ export enum ProjectPermissionSub {
|
||||
PamFolders = "pam-folders",
|
||||
PamResources = "pam-resources",
|
||||
PamAccounts = "pam-accounts",
|
||||
PamSessions = "pam-sessions"
|
||||
PamSessions = "pam-sessions",
|
||||
CertificateProfiles = "certificate-profiles"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@@ -438,7 +447,8 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionPamAccountActions,
|
||||
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_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."
|
||||
).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
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
import { ProjectType, TProjectTemplates } from "@app/db/schemas";
|
||||
import { OrganizationActionScope, ProjectType, TProjectTemplates } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
@@ -59,13 +59,14 @@ export const projectTemplateServiceFactory = ({
|
||||
message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
@@ -97,13 +98,14 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectTemplate.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: projectTemplate.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
@@ -125,13 +127,14 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectTemplate.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: projectTemplate.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
@@ -152,13 +155,14 @@ export const projectTemplateServiceFactory = ({
|
||||
message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
@@ -213,13 +217,14 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectTemplate.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: projectTemplate.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||
if (projectTemplate.type !== ProjectType.SecretManager && environments)
|
||||
@@ -272,13 +277,14 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
projectTemplate.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: projectTemplate.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ProjectServiceActor } from "@app/lib/types";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
|
||||
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
|
||||
@@ -31,7 +31,7 @@ export enum InfisicalProjectTemplate {
|
||||
|
||||
export type TProjectTemplateServiceFactory = {
|
||||
listProjectTemplatesByOrg: (
|
||||
actor: OrgServiceActor,
|
||||
actor: ProjectServiceActor,
|
||||
type?: ProjectType
|
||||
) => Promise<
|
||||
(
|
||||
@@ -85,7 +85,7 @@ export type TProjectTemplateServiceFactory = {
|
||||
>;
|
||||
createProjectTemplate: (
|
||||
arg: TCreateProjectTemplateDTO,
|
||||
actor: OrgServiceActor
|
||||
actor: ProjectServiceActor
|
||||
) => Promise<{
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
roles: {
|
||||
@@ -109,7 +109,7 @@ export type TProjectTemplateServiceFactory = {
|
||||
updateProjectTemplateById: (
|
||||
id: string,
|
||||
{ roles, environments, ...params }: TUpdateProjectTemplateDTO,
|
||||
actor: OrgServiceActor
|
||||
actor: ProjectServiceActor
|
||||
) => Promise<{
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
roles: {
|
||||
@@ -132,7 +132,7 @@ export type TProjectTemplateServiceFactory = {
|
||||
}>;
|
||||
deleteProjectTemplateById: (
|
||||
id: string,
|
||||
actor: OrgServiceActor
|
||||
actor: ProjectServiceActor
|
||||
) => Promise<{
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
roles: {
|
||||
@@ -155,7 +155,7 @@ export type TProjectTemplateServiceFactory = {
|
||||
}>;
|
||||
findProjectTemplateById: (
|
||||
id: string,
|
||||
actor: OrgServiceActor
|
||||
actor: ProjectServiceActor
|
||||
) => Promise<{
|
||||
packedRoles: TProjectTemplateRole[];
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
@@ -179,7 +179,7 @@ export type TProjectTemplateServiceFactory = {
|
||||
}>;
|
||||
findProjectTemplateByName: (
|
||||
name: string,
|
||||
actor: OrgServiceActor
|
||||
actor: ProjectServiceActor
|
||||
) => Promise<{
|
||||
packedRoles: TProjectTemplateRole[];
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isIP } from "node:net";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { OrgMembershipRole, TRelays } from "@app/db/schemas";
|
||||
import { OrganizationActionScope, OrgMembershipRole, TRelays } from "@app/db/schemas";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -126,8 +126,8 @@ export const relayServiceFactory = ({
|
||||
|
||||
// generate instance relay CA
|
||||
const instanceRelayCaSerialNumber = createSerialNumber();
|
||||
const instanceRelayCaIssuedAt = new Date();
|
||||
const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const instanceRelayCaIssuedAt = new Date();
|
||||
const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey);
|
||||
const instanceRelayCaCert = await x509.X509CertificateGenerator.create({
|
||||
@@ -972,13 +972,14 @@ export const relayServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityId,
|
||||
orgId,
|
||||
actorAuthMethod!,
|
||||
orgId
|
||||
);
|
||||
actorAuthMethod: actorAuthMethod!,
|
||||
actorOrgId: orgId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionRelayActions.CreateRelays,
|
||||
@@ -1102,13 +1103,14 @@ export const relayServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityId,
|
||||
orgId,
|
||||
actorAuthMethod!,
|
||||
orgId
|
||||
);
|
||||
actorAuthMethod: actorAuthMethod!,
|
||||
actorOrgId: orgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionRelayActions.CreateRelays,
|
||||
OrgPermissionSubjects.Relay
|
||||
@@ -1155,13 +1157,14 @@ export const relayServiceFactory = ({
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod: actorAuthMethod!,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
@@ -1189,13 +1192,14 @@ export const relayServiceFactory = ({
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId: actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import RE2 from "re2";
|
||||
|
||||
import {
|
||||
AccessScope,
|
||||
OrganizationActionScope,
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
TableName,
|
||||
@@ -251,7 +252,14 @@ export const samlConfigServiceFactory = ({
|
||||
authProvider,
|
||||
enableGroupSync
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -317,7 +325,14 @@ export const samlConfigServiceFactory = ({
|
||||
authProvider,
|
||||
enableGroupSync
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.samlSSO)
|
||||
@@ -393,7 +408,7 @@ export const samlConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
} else if (dto.type === "orgSlug") {
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||
const org = await orgDAL.findOne({ slug: dto.orgSlug, rootOrgId: null });
|
||||
if (!org) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization with slug '${dto.orgSlug}' not found`
|
||||
@@ -424,13 +439,14 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
// when dto is type id means it's internally used
|
||||
if (dto.type === "org") {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
samlConfig.orgId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor: dto.actor,
|
||||
actorId: dto.actorId,
|
||||
orgId: samlConfig.orgId,
|
||||
actorAuthMethod: dto.actorAuthMethod,
|
||||
actorOrgId: dto.actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
|
||||
@@ -37,7 +37,7 @@ export type TGetSamlCfgDTO =
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
actorOrgId: string;
|
||||
}
|
||||
| {
|
||||
type: "orgSlug";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { scimPatch } from "scim-patch";
|
||||
|
||||
import {
|
||||
AccessScope,
|
||||
OrganizationActionScope,
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
TableName,
|
||||
@@ -56,6 +57,7 @@ type TScimServiceFactoryDep = {
|
||||
TOrgDALFactory,
|
||||
| "createMembership"
|
||||
| "findById"
|
||||
| "find"
|
||||
| "findMembership"
|
||||
| "findMembershipWithScimFilter"
|
||||
| "deleteMembershipById"
|
||||
@@ -125,7 +127,14 @@ export const scimServiceFactory = ({
|
||||
description,
|
||||
ttlDays
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -160,7 +169,14 @@ export const scimServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
@@ -183,13 +199,14 @@ export const scimServiceFactory = ({
|
||||
let scimToken = await scimDAL.findById(scimTokenId);
|
||||
if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.ParentOrganization,
|
||||
actor,
|
||||
actorId,
|
||||
scimToken.orgId,
|
||||
orgId: scimToken.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||
|
||||
const plan = await licenseService.getPlan(scimToken.orgId);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { WebhookEventMap } from "@octokit/webhooks-types";
|
||||
import { ProbotOctokit } from "probot";
|
||||
|
||||
import { OrganizationActionScope } from "@app/db/schemas";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
@@ -49,7 +50,14 @@ export const secretScanningServiceFactory = ({
|
||||
}: TInstallAppSessionDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
@@ -68,13 +76,14 @@ export const secretScanningServiceFactory = ({
|
||||
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
|
||||
if (!session) throw new NotFoundError({ message: "Session was not found" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
session.orgId,
|
||||
orgId: session.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
|
||||
await gitAppInstallSessionDAL.deleteById(session.id, tx);
|
||||
@@ -117,7 +126,14 @@ export const secretScanningServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetOrgInstallStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const appInstallation = await gitAppOrgDAL.findOne({ orgId });
|
||||
@@ -125,7 +141,14 @@ export const secretScanningServiceFactory = ({
|
||||
};
|
||||
|
||||
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const results = await secretScanningDAL.findByOrgId(orgId, filter);
|
||||
@@ -134,7 +157,14 @@ export const secretScanningServiceFactory = ({
|
||||
};
|
||||
|
||||
const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
|
||||
@@ -150,7 +180,14 @@ export const secretScanningServiceFactory = ({
|
||||
riskId,
|
||||
status
|
||||
}: TUpdateRiskStatusDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const isRiskResolved = Boolean(
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { AccessScope, OrganizationActionScope, OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TMembershipDALFactory } from "@app/services/membership/membership-dal";
|
||||
import { TMembershipRoleDALFactory } from "@app/services/membership/membership-role-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, OrgPermissionSubOrgActions } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { TCreateSubOrgDTO, TListSubOrgDTO, TUpdateSubOrgDTO } from "./sub-org-types";
|
||||
|
||||
type TSubOrgServiceFactoryDep = {
|
||||
orgDAL: Pick<
|
||||
TOrgDALFactory,
|
||||
"findOne" | "create" | "transaction" | "listSubOrganizations" | "updateById" | "findById"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
membershipDAL: Pick<TMembershipDALFactory, "create">;
|
||||
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "create">;
|
||||
};
|
||||
|
||||
export type TSubOrgServiceFactory = ReturnType<typeof subOrgServiceFactory>;
|
||||
|
||||
export const subOrgServiceFactory = ({
|
||||
orgDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
membershipDAL,
|
||||
membershipRoleDAL
|
||||
}: TSubOrgServiceFactoryDep) => {
|
||||
const createSubOrg = async ({ name, permissionActor }: TCreateSubOrgDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: permissionActor.id,
|
||||
actor: permissionActor.type,
|
||||
orgId: permissionActor.orgId,
|
||||
actorOrgId: permissionActor.orgId,
|
||||
actorAuthMethod: permissionActor.authMethod,
|
||||
scope: OrganizationActionScope.ParentOrganization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionSubOrgActions.Create,
|
||||
OrgPermissionSubjects.SubOrganization
|
||||
);
|
||||
|
||||
const orgLicensePlan = await licenseService.getPlan(permissionActor.rootOrgId);
|
||||
if (!orgLicensePlan.subOrganization) {
|
||||
throw new BadRequestError({
|
||||
message: "Sub-organization creation failed. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const existingSubOrg = await orgDAL.findOne({
|
||||
parentOrgId: permissionActor.orgId,
|
||||
name
|
||||
});
|
||||
if (existingSubOrg) {
|
||||
throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` });
|
||||
}
|
||||
|
||||
const organization = await orgDAL.transaction(async (tx) => {
|
||||
const org = await orgDAL.create(
|
||||
{ name, slug: name, rootOrgId: permissionActor.rootOrgId, parentOrgId: permissionActor.orgId },
|
||||
tx
|
||||
);
|
||||
const membership = await membershipDAL.create(
|
||||
{
|
||||
scope: AccessScope.Organization,
|
||||
[permissionActor.type === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"]: permissionActor.id,
|
||||
scopeOrgId: org.id,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
await membershipRoleDAL.create(
|
||||
{
|
||||
membershipId: membership.id,
|
||||
role: OrgMembershipRole.Admin
|
||||
},
|
||||
tx
|
||||
);
|
||||
return org;
|
||||
});
|
||||
|
||||
return {
|
||||
organization
|
||||
};
|
||||
};
|
||||
|
||||
const listSubOrgs = async ({ permissionActor, data }: TListSubOrgDTO) => {
|
||||
await permissionService.getOrgPermission({
|
||||
actorId: permissionActor.id,
|
||||
actor: permissionActor.type,
|
||||
orgId: permissionActor.rootOrgId,
|
||||
actorOrgId: permissionActor.rootOrgId,
|
||||
actorAuthMethod: permissionActor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
const organizations = await orgDAL.listSubOrganizations({
|
||||
actorId: permissionActor.id,
|
||||
actorType: permissionActor.type,
|
||||
orgId: permissionActor.rootOrgId,
|
||||
isAccessible: data?.isAccessible,
|
||||
limit: data?.limit,
|
||||
offset: data?.offset
|
||||
});
|
||||
|
||||
return {
|
||||
organizations
|
||||
};
|
||||
};
|
||||
|
||||
const updateSubOrg = async ({ subOrgId, name, permissionActor }: TUpdateSubOrgDTO) => {
|
||||
const subOrg = await orgDAL.findOne({
|
||||
rootOrgId: permissionActor.rootOrgId,
|
||||
id: subOrgId
|
||||
});
|
||||
if (!subOrg) {
|
||||
throw new BadRequestError({ message: "Sub-organization not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: permissionActor.id,
|
||||
actor: permissionActor.type,
|
||||
orgId: subOrgId,
|
||||
actorOrgId: subOrgId,
|
||||
actorAuthMethod: permissionActor.authMethod,
|
||||
scope: OrganizationActionScope.ChildOrganization
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const existingSubOrg = await orgDAL.findOne({
|
||||
parentOrgId: subOrg.parentOrgId,
|
||||
slug: name
|
||||
});
|
||||
|
||||
if (existingSubOrg && existingSubOrg.id !== subOrgId) {
|
||||
throw new BadRequestError({ message: `Sub-organization with name ${name} already exists` });
|
||||
}
|
||||
|
||||
const organization = await orgDAL.updateById(subOrgId, { name, slug: name });
|
||||
|
||||
return {
|
||||
organization
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createSubOrg,
|
||||
listSubOrgs,
|
||||
updateSubOrg
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
export type TCreateSubOrgDTO = {
|
||||
name: string;
|
||||
permissionActor: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TListSubOrgDTO = {
|
||||
permissionActor: OrgServiceActor;
|
||||
data: Partial<{
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
isAccessible?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TUpdateSubOrgDTO = {
|
||||
subOrgId: string;
|
||||
name: string;
|
||||
permissionActor: OrgServiceActor;
|
||||
};
|
||||
@@ -33,6 +33,7 @@ export enum ApiDocsTags {
|
||||
LdapAuth = "LDAP Auth",
|
||||
Groups = "Groups",
|
||||
Organizations = "Organizations",
|
||||
SubOrganizations = "Sub Organizations",
|
||||
Projects = "Projects",
|
||||
ProjectUsers = "Project Users",
|
||||
ProjectGroups = "Project Groups",
|
||||
@@ -57,6 +58,7 @@ export enum ApiDocsTags {
|
||||
PkiCertificateAuthorities = "PKI Certificate Authorities",
|
||||
PkiCertificates = "PKI Certificates",
|
||||
PkiCertificateTemplates = "PKI Certificate Templates",
|
||||
PkiCertificateProfiles = "PKI Certificate Profiles",
|
||||
PkiCertificateCollections = "PKI Certificate Collections",
|
||||
PkiAlerting = "PKI Alerting",
|
||||
PkiSubscribers = "PKI Subscribers",
|
||||
@@ -716,6 +718,21 @@ export const ORGANIZATIONS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const SUB_ORGANIZATIONS = {
|
||||
CREATE: {
|
||||
name: "The name of the sub organization to create."
|
||||
},
|
||||
UPDATE: {
|
||||
name: "The name of the sub organization to update.",
|
||||
subOrgId: "The id of the sub organization to update."
|
||||
},
|
||||
LIST: {
|
||||
limit: "The number of sub organizations to return.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th sub organization.",
|
||||
isAccessible: "Filter to only return sub organizations that the actor has access to."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const PROJECTS = {
|
||||
CREATE: {
|
||||
organizationSlug: "The slug of the organization to create the project in.",
|
||||
|
||||
@@ -5,7 +5,7 @@ export type TGenericPermission = {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string | undefined;
|
||||
actorOrgId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,6 +78,15 @@ export type OrgServiceActor = {
|
||||
id: string;
|
||||
authMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
};
|
||||
|
||||
export type ProjectServiceActor = {
|
||||
type: ActorType;
|
||||
id: string;
|
||||
authMethod: ActorAuthMethod;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export enum QueueWorkerProfile {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@@ -20,6 +21,8 @@ export type TAuthMode =
|
||||
tokenVersionId: string; // the session id of token used
|
||||
user: TUsers;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: AuthMethod;
|
||||
isMfaVerified?: boolean;
|
||||
token: AuthModeJwtTokenPayload;
|
||||
@@ -31,6 +34,8 @@ export type TAuthMode =
|
||||
userId: string;
|
||||
user: TUsers;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
@@ -39,6 +44,8 @@ export type TAuthMode =
|
||||
actor: ActorType.SERVICE;
|
||||
serviceTokenId: string;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
token: string;
|
||||
}
|
||||
@@ -48,6 +55,8 @@ export type TAuthMode =
|
||||
identityId: string;
|
||||
identityName: string;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
isInstanceAdmin?: boolean;
|
||||
token: TIdentityAccessTokenJwtPayload;
|
||||
@@ -57,6 +66,8 @@ export type TAuthMode =
|
||||
actor: ActorType.SCIM_CLIENT;
|
||||
scimTokenId: string;
|
||||
orgId: string;
|
||||
rootOrgId: string;
|
||||
parentOrgId: string;
|
||||
authMethod: null;
|
||||
};
|
||||
|
||||
@@ -136,17 +147,26 @@ export const injectIdentity = fp(
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
|
||||
if (subOrganizationSelector) {
|
||||
await slugSchema().parseAsync(subOrganizationSelector);
|
||||
}
|
||||
|
||||
switch (authMode) {
|
||||
case AuthMode.JWT: {
|
||||
const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
|
||||
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } =
|
||||
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
||||
requestContext.set("orgId", orgId);
|
||||
|
||||
req.auth = {
|
||||
authMode: AuthMode.JWT,
|
||||
user,
|
||||
userId: user.id,
|
||||
tokenVersionId,
|
||||
actor,
|
||||
orgId: orgId as string,
|
||||
orgId,
|
||||
rootOrgId,
|
||||
parentOrgId,
|
||||
authMethod: token.authMethod,
|
||||
isMfaVerified: token.isMfaVerified,
|
||||
token
|
||||
@@ -154,13 +174,19 @@ export const injectIdentity = fp(
|
||||
break;
|
||||
}
|
||||
case AuthMode.IDENTITY_ACCESS_TOKEN: {
|
||||
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
|
||||
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(
|
||||
token,
|
||||
subOrganizationSelector,
|
||||
req.realIp
|
||||
);
|
||||
const serverCfg = await getServerCfg();
|
||||
requestContext.set("orgId", identity.orgId);
|
||||
req.auth = {
|
||||
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||
actor,
|
||||
orgId: identity.orgId,
|
||||
rootOrgId: identity.rootOrgId,
|
||||
parentOrgId: identity.parentOrgId,
|
||||
identityId: identity.identityId,
|
||||
identityName: identity.name,
|
||||
authMethod: null,
|
||||
@@ -190,8 +216,14 @@ export const injectIdentity = fp(
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
|
||||
requestContext.set("orgId", serviceToken.orgId);
|
||||
|
||||
if (subOrganizationSelector)
|
||||
throw new BadRequestError({ message: `Service token doesn't support sub organization selector` });
|
||||
|
||||
req.auth = {
|
||||
orgId: serviceToken.orgId,
|
||||
rootOrgId: serviceToken.rootOrgId,
|
||||
parentOrgId: serviceToken.parentOrgId,
|
||||
authMode: AuthMode.SERVICE_TOKEN as const,
|
||||
serviceToken,
|
||||
serviceTokenId: serviceToken.id,
|
||||
@@ -202,22 +234,27 @@ export const injectIdentity = fp(
|
||||
break;
|
||||
}
|
||||
case AuthMode.API_KEY: {
|
||||
const user = await server.services.apiKey.fnValidateApiKey(token as string);
|
||||
req.auth = {
|
||||
authMode: AuthMode.API_KEY as const,
|
||||
userId: user.id,
|
||||
actor,
|
||||
user,
|
||||
orgId: "API_KEY", // We set the orgId to an arbitrary value, since we can't link an API key to a specific org. We have to deprecate API keys soon!
|
||||
authMethod: null,
|
||||
token: token as string
|
||||
};
|
||||
break;
|
||||
throw new BadRequestError({
|
||||
message: "API key authentication is not supported anymore. Please switch to identity authentication."
|
||||
});
|
||||
}
|
||||
case AuthMode.SCIM_TOKEN: {
|
||||
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
|
||||
requestContext.set("orgId", orgId);
|
||||
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null };
|
||||
|
||||
if (subOrganizationSelector)
|
||||
throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` });
|
||||
|
||||
req.auth = {
|
||||
authMode: AuthMode.SCIM_TOKEN,
|
||||
actor,
|
||||
scimTokenId,
|
||||
orgId,
|
||||
authMethod: null,
|
||||
// scim cannot be done for sub organization
|
||||
rootOrgId: orgId,
|
||||
parentOrgId: orgId
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -14,7 +14,9 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.USER,
|
||||
id: req.auth.userId,
|
||||
orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY"
|
||||
authMethod: req.auth.authMethod // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null
|
||||
authMethod: req.auth.authMethod, // if the req.auth.authMode is AuthMode.API_KEY, the authMethod will be null
|
||||
rootOrgId: req.auth.rootOrgId,
|
||||
parentOrgId: req.auth.parentOrgId
|
||||
};
|
||||
|
||||
logger.info(
|
||||
@@ -25,7 +27,9 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.IDENTITY,
|
||||
id: req.auth.identityId,
|
||||
orgId: req.auth.orgId,
|
||||
authMethod: null
|
||||
authMethod: null,
|
||||
rootOrgId: req.auth.rootOrgId,
|
||||
parentOrgId: req.auth.parentOrgId
|
||||
};
|
||||
|
||||
logger.info(
|
||||
@@ -36,6 +40,8 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.SERVICE,
|
||||
id: req.auth.serviceTokenId,
|
||||
orgId: req.auth.orgId,
|
||||
rootOrgId: req.auth.rootOrgId,
|
||||
parentOrgId: req.auth.parentOrgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
@@ -47,6 +53,8 @@ export const injectPermission = fp(async (server) => {
|
||||
type: ActorType.SCIM_CLIENT,
|
||||
id: req.auth.scimTokenId,
|
||||
orgId: req.auth.orgId,
|
||||
rootOrgId: req.auth.rootOrgId,
|
||||
parentOrgId: req.auth.parentOrgId,
|
||||
authMethod: null
|
||||
};
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-
|
||||
import { sshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
|
||||
import { sshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
|
||||
import { sshHostGroupServiceFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-service";
|
||||
import { subOrgServiceFactory } from "@app/ee/services/sub-org/sub-org-service";
|
||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||
import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal";
|
||||
@@ -167,11 +168,19 @@ import { externalCertificateAuthorityDALFactory } from "@app/services/certificat
|
||||
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 { 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 { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
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 { 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 { 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";
|
||||
@@ -560,11 +569,10 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
licenseDAL,
|
||||
keyStore,
|
||||
identityOrgMembershipDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL });
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL });
|
||||
|
||||
const membershipUserService = membershipUserServiceFactory({
|
||||
licenseService,
|
||||
@@ -584,6 +592,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const membershipIdentityService = membershipIdentityServiceFactory({
|
||||
identityDAL,
|
||||
membershipIdentityDAL,
|
||||
membershipRoleDAL,
|
||||
orgDAL,
|
||||
@@ -904,6 +913,15 @@ export const registerRoutes = async (
|
||||
userGroupMembershipDAL,
|
||||
additionalPrivilegeDAL
|
||||
});
|
||||
|
||||
const subOrgService = subOrgServiceFactory({
|
||||
licenseService,
|
||||
membershipDAL,
|
||||
membershipRoleDAL,
|
||||
orgDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const signupService = authSignupServiceFactory({
|
||||
tokenService,
|
||||
smtpService,
|
||||
@@ -1036,6 +1054,10 @@ export const registerRoutes = async (
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
const certificateTemplateDAL = certificateTemplateDALFactory(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 certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
@@ -1120,6 +1142,21 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateTemplateV2Service = certificateTemplateV2ServiceFactory({
|
||||
certificateTemplateV2DAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateProfileService = certificateProfileServiceFactory({
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2DAL,
|
||||
apiEnrollmentConfigDAL,
|
||||
estEnrollmentConfigDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
pkiAlertDAL,
|
||||
pkiCollectionDAL,
|
||||
@@ -1567,10 +1604,12 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
projectDAL,
|
||||
accessTokenQueue,
|
||||
smtpService
|
||||
smtpService,
|
||||
orgDAL
|
||||
});
|
||||
|
||||
const identityService = identityServiceFactory({
|
||||
additionalPrivilegeDAL,
|
||||
permissionService,
|
||||
identityDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@@ -1601,10 +1640,12 @@ export const registerRoutes = async (
|
||||
identityAccessTokenDAL,
|
||||
accessTokenQueue,
|
||||
identityDAL,
|
||||
membershipIdentityDAL
|
||||
membershipIdentityDAL,
|
||||
orgDAL
|
||||
});
|
||||
|
||||
const identityTokenAuthService = identityTokenAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityTokenAuthDAL,
|
||||
identityAccessTokenDAL,
|
||||
permissionService,
|
||||
@@ -1614,6 +1655,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityUaService = identityUaServiceFactory({
|
||||
identityDAL,
|
||||
permissionService,
|
||||
identityAccessTokenDAL,
|
||||
identityUaClientSecretDAL,
|
||||
@@ -1625,6 +1667,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityKubernetesAuthDAL,
|
||||
identityAccessTokenDAL,
|
||||
permissionService,
|
||||
@@ -1638,6 +1681,7 @@ export const registerRoutes = async (
|
||||
membershipIdentityDAL
|
||||
});
|
||||
const identityGcpAuthService = identityGcpAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityGcpAuthDAL,
|
||||
orgDAL,
|
||||
identityAccessTokenDAL,
|
||||
@@ -1647,6 +1691,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityAccessTokenDAL,
|
||||
orgDAL,
|
||||
identityAliCloudAuthDAL,
|
||||
@@ -1656,6 +1701,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityAccessTokenDAL,
|
||||
identityTlsCertAuthDAL,
|
||||
licenseService,
|
||||
@@ -1665,6 +1711,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityAccessTokenDAL,
|
||||
orgDAL,
|
||||
identityAwsAuthDAL,
|
||||
@@ -1674,6 +1721,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityAzureAuthService = identityAzureAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityAzureAuthDAL,
|
||||
orgDAL,
|
||||
identityAccessTokenDAL,
|
||||
@@ -1683,6 +1731,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityOciAuthService = identityOciAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityAccessTokenDAL,
|
||||
orgDAL,
|
||||
identityOciAuthDAL,
|
||||
@@ -1706,6 +1755,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityOidcAuthDAL,
|
||||
orgDAL,
|
||||
identityAccessTokenDAL,
|
||||
@@ -1716,6 +1766,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const identityJwtAuthService = identityJwtAuthServiceFactory({
|
||||
identityDAL,
|
||||
identityJwtAuthDAL,
|
||||
orgDAL,
|
||||
permissionService,
|
||||
@@ -2086,6 +2137,27 @@ export const registerRoutes = async (
|
||||
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({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
@@ -2248,6 +2320,7 @@ export const registerRoutes = async (
|
||||
groupProject: groupProjectService,
|
||||
permission: permissionService,
|
||||
org: orgService,
|
||||
subOrganization: subOrgService,
|
||||
oidc: oidcService,
|
||||
apiKey: apiKeyService,
|
||||
authToken: tokenService,
|
||||
@@ -2296,6 +2369,8 @@ export const registerRoutes = async (
|
||||
auditLog: auditLogService,
|
||||
auditLogStream: auditLogStreamService,
|
||||
certificate: certificateService,
|
||||
certificateV3: certificateV3Service,
|
||||
certificateEstV3: certificateEstV3Service,
|
||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||
sshCertificateTemplate: sshCertificateTemplateService,
|
||||
sshHost: sshHostService,
|
||||
@@ -2303,6 +2378,8 @@ export const registerRoutes = async (
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
internalCertificateAuthority: internalCertificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateTemplateV2: certificateTemplateV2Service,
|
||||
certificateProfile: certificateProfileService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pit: pitService,
|
||||
|
||||
@@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
decodedToken.userId,
|
||||
decodedToken.organizationId,
|
||||
decodedToken.authMethod,
|
||||
decodedToken.organizationId,
|
||||
decodedToken.organizationId
|
||||
);
|
||||
if (org && org.userTokenExpiration) {
|
||||
|
||||
@@ -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: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -121,7 +122,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
name: certificateTemplate.name,
|
||||
commonName: certificateTemplate.commonName,
|
||||
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,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.name,
|
||||
caId: certificateTemplate.caId,
|
||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
||||
name: certificateTemplate.name,
|
||||
commonName: certificateTemplate.commonName,
|
||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
||||
ttl: certificateTemplate.ttl
|
||||
@@ -230,7 +232,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
event: {
|
||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
||||
metadata: {
|
||||
certificateTemplateId: certificateTemplate.id
|
||||
certificateTemplateId: certificateTemplate.id,
|
||||
name: certificateTemplate.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,12 +73,12 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityAliCloudAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityAliCloudAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -40,12 +40,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityAwsAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityAwsAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_AWS_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -35,12 +35,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityAzureAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityAzureAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_AZURE_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -35,12 +35,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityGcpAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityGcpAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_GCP_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -111,7 +111,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityJwtAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityJwtAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
jwt: req.body.jwt
|
||||
@@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_JWT_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityKubernetesAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityKubernetesAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
jwt: req.body.jwt
|
||||
@@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -162,13 +162,13 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
|
||||
|
||||
const { identityId, user } = req.passportMachineIdentity;
|
||||
|
||||
const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({
|
||||
const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({
|
||||
identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -52,12 +52,12 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityOciAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityOciAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -59,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } =
|
||||
const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } =
|
||||
await server.services.identityOidcAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
jwt: req.body.jwt
|
||||
@@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas";
|
||||
import { ApiDocsTags, PROJECT_IDENTITIES } 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";
|
||||
|
||||
const sanitizedOrgIdentityMembershipSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string(),
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/identity-memberships/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
hide: true,
|
||||
// this is hidden so not updating tags
|
||||
tags: [ApiDocsTags.ProjectIdentities],
|
||||
description: "Create org identity membership",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
roles: z
|
||||
.array(
|
||||
z.union([
|
||||
z.object({
|
||||
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
|
||||
isTemporary: z
|
||||
.literal(false)
|
||||
.default(false)
|
||||
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
|
||||
}),
|
||||
z.object({
|
||||
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
|
||||
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
|
||||
temporaryMode: z
|
||||
.nativeEnum(TemporaryPermissionMode)
|
||||
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
|
||||
temporaryRange: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
||||
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
|
||||
temporaryAccessStartTime: z
|
||||
.string()
|
||||
.datetime()
|
||||
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
|
||||
})
|
||||
])
|
||||
)
|
||||
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
|
||||
.max(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMembership: sanitizedOrgIdentityMembershipSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { membership } = await server.services.membershipIdentity.createMembership({
|
||||
permission: req.permission,
|
||||
scopeData: {
|
||||
scope: AccessScope.Organization,
|
||||
orgId: req.permission.orgId
|
||||
},
|
||||
data: {
|
||||
identityId: req.params.identityId,
|
||||
roles: req.body.roles
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/identity-memberships/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
hide: true,
|
||||
tags: [ApiDocsTags.ProjectIdentities],
|
||||
description: "Delete org identity memberships",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityMembership: sanitizedOrgIdentityMembershipSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { membership } = await server.services.membershipIdentity.deleteMembership({
|
||||
permission: req.permission,
|
||||
scopeData: {
|
||||
scope: AccessScope.Organization,
|
||||
orgId: req.permission.orgId
|
||||
},
|
||||
selector: {
|
||||
identityId: req.params.identityId
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -249,7 +249,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({
|
||||
authMethods: z.array(z.string()),
|
||||
activeLockoutAuthMethods: z.array(z.string())
|
||||
})
|
||||
@@ -393,7 +393,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true, orgId: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
}).array(),
|
||||
|
||||
@@ -64,7 +64,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
|
||||
throw new BadRequestError({ message: "Missing TLS certificate in header" });
|
||||
}
|
||||
|
||||
const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityTlsCertAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityTlsCertAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
clientCertificate: clientCertificate as string
|
||||
@@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -319,7 +319,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityTokenAuth, accessToken, identityAccessToken, identity } =
|
||||
await server.services.identityTokenAuth.createTokenAuthToken({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -52,14 +52,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||
accessToken,
|
||||
identityAccessToken,
|
||||
validClientSecretInfo,
|
||||
identityMembershipOrg,
|
||||
identity,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL
|
||||
} = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg.scopeOrgId,
|
||||
orgId: identity.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH,
|
||||
metadata: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
|
||||
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
|
||||
@@ -32,6 +33,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
|
||||
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
|
||||
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
|
||||
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
|
||||
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
|
||||
import { registerIdentityProjectRouter } from "./identity-project-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
|
||||
@@ -89,6 +91,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
@@ -146,6 +149,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
await pkiRouter.register(registerCertRouter, { prefix: "/certificates" });
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||
|
||||
@@ -2,6 +2,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AccessScope,
|
||||
AuditLogsSchema,
|
||||
GroupsSchema,
|
||||
IncidentContactsSchema,
|
||||
@@ -59,7 +60,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: sanitizedOrganizationSchema
|
||||
organization: sanitizedOrganizationSchema.extend({
|
||||
subOrganization: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -69,6 +77,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.permission.authMethod,
|
||||
req.permission.rootOrgId,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { organization };
|
||||
@@ -467,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
return { groups };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/users/available",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
users: z
|
||||
.object({
|
||||
id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
email: z.string().nullable().optional(),
|
||||
firstName: z.string().nullable().optional(),
|
||||
lastName: z.string().nullable().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { users } = await server.services.membershipUser.listAvailableUsers({
|
||||
permission: req.permission,
|
||||
scopeData: {
|
||||
orgId: req.permission.orgId,
|
||||
scope: AccessScope.Organization
|
||||
},
|
||||
data: {}
|
||||
});
|
||||
|
||||
return { users };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identities/available",
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
identities: z
|
||||
.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
hasDeleteProtection: z.boolean()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
|
||||
permission: req.permission,
|
||||
scopeData: {
|
||||
orgId: req.permission.orgId,
|
||||
scope: AccessScope.Organization
|
||||
},
|
||||
data: {}
|
||||
});
|
||||
|
||||
return { identities };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -60,7 +60,7 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
permissions: true,
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true }).extend({
|
||||
authMethods: z.array(z.string())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertificateTemplatesV2Router } from "./certificate-templates-v2-router";
|
||||
import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
|
||||
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-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(registerPasswordRouter, { prefix: "/password" });
|
||||
|
||||
await server.register(registerCertificateTemplatesV2Router, { prefix: "/certificate-templates" });
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { registerCertificatesRouter } from "./certificates-router";
|
||||
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
|
||||
import { registerExternalMigrationRouter } from "./external-migration-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(registerDeprecatedSecretRouter, { prefix: "/secrets" });
|
||||
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
|
||||
await server.register(registerCertificatesRouter, { prefix: "/certificates" });
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TAppConnections } from "@app/db/schemas";
|
||||
import { ActionProjectType, OrganizationActionScope, TAppConnections } from "@app/db/schemas";
|
||||
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
||||
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
||||
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
||||
@@ -215,13 +215,14 @@ export const appConnectionServiceFactory = ({
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: actor.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
@@ -268,13 +269,14 @@ export const appConnectionServiceFactory = ({
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
@@ -318,13 +320,14 @@ export const appConnectionServiceFactory = ({
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
@@ -342,13 +345,14 @@ export const appConnectionServiceFactory = ({
|
||||
{ method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: actor.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
@@ -477,13 +481,14 @@ export const appConnectionServiceFactory = ({
|
||||
"Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||
);
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
if (appConnection.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
@@ -635,13 +640,14 @@ export const appConnectionServiceFactory = ({
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Delete,
|
||||
@@ -704,13 +710,14 @@ export const appConnectionServiceFactory = ({
|
||||
subject(ProjectPermissionSub.AppConnections, { connectionId })
|
||||
);
|
||||
} else {
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Connect,
|
||||
@@ -747,13 +754,14 @@ export const appConnectionServiceFactory = ({
|
||||
};
|
||||
|
||||
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => {
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: actor.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
let availableProjectConnections: TAppConnections[] = [];
|
||||
|
||||
@@ -805,13 +813,14 @@ export const appConnectionServiceFactory = ({
|
||||
|
||||
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
actorId: actor.id,
|
||||
actor: actor.type,
|
||||
orgId: appConnection.orgId,
|
||||
actorOrgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
scope: OrganizationActionScope.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionAppConnectionActions.Read,
|
||||
|
||||
@@ -3,10 +3,11 @@ import { Knex } from "knex";
|
||||
import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
|
||||
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TTokenDALFactory } from "./auth-token-dal";
|
||||
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
||||
@@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
|
||||
type TAuthTokenServiceFactoryDep = {
|
||||
tokenDAL: TTokenDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
membershipUserDAL: Pick<TMembershipUserDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
@@ -80,7 +82,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
@@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
|
||||
};
|
||||
|
||||
// to parse jwt identity in inject identity plugin
|
||||
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
|
||||
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => {
|
||||
const session = await tokenDAL.findOneTokenSession({
|
||||
id: token.tokenVersionId,
|
||||
userId: token.userId
|
||||
@@ -207,22 +209,56 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
|
||||
const user = await userDAL.findById(session.userId);
|
||||
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
|
||||
|
||||
let orgId = "";
|
||||
let rootOrgId = "";
|
||||
let parentOrgId = "";
|
||||
if (token.organizationId) {
|
||||
const orgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: token.organizationId,
|
||||
scope: AccessScope.Organization
|
||||
});
|
||||
if (subOrganizationSelector) {
|
||||
const subOrganization = await orgDAL.findOne({
|
||||
rootOrgId: token.organizationId,
|
||||
slug: subOrganizationSelector
|
||||
});
|
||||
if (!subOrganization)
|
||||
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
|
||||
|
||||
if (!orgMembership) {
|
||||
throw new ForbiddenRequestError({ message: "User not member of organization" });
|
||||
}
|
||||
if (!orgMembership.isActive) {
|
||||
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
|
||||
const orgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: subOrganization.id,
|
||||
scope: AccessScope.Organization
|
||||
});
|
||||
|
||||
if (!orgMembership) {
|
||||
throw new ForbiddenRequestError({ message: "User not member of organization" });
|
||||
}
|
||||
|
||||
if (!orgMembership.isActive) {
|
||||
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
|
||||
}
|
||||
orgId = subOrganization.id;
|
||||
rootOrgId = token.organizationId;
|
||||
parentOrgId = subOrganization.parentOrgId as string;
|
||||
} else {
|
||||
const orgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
scopeOrgId: token.organizationId,
|
||||
scope: AccessScope.Organization
|
||||
});
|
||||
|
||||
if (!orgMembership) {
|
||||
throw new ForbiddenRequestError({ message: "User not member of organization" });
|
||||
}
|
||||
|
||||
if (!orgMembership.isActive) {
|
||||
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
|
||||
}
|
||||
|
||||
orgId = token.organizationId;
|
||||
rootOrgId = token.organizationId;
|
||||
parentOrgId = token.organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -258,7 +258,13 @@ export const authSignupServiceFactory = ({
|
||||
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||
|
||||
if (organizationId) {
|
||||
const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
|
||||
const org = await orgService.findOrganizationById(
|
||||
user.id,
|
||||
organizationId,
|
||||
authMethod,
|
||||
organizationId,
|
||||
organizationId
|
||||
);
|
||||
if (org && org.userTokenExpiration) {
|
||||
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||
|
||||
@@ -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 { crypto } from "@app/lib/crypto/cryptography";
|
||||
@@ -68,6 +69,13 @@ export const parseDistinguishedName = (dn: string): TDNParts => {
|
||||
|
||||
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
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:
|
||||
return {
|
||||
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]
|
||||
* Note: credentials are returned as crypto.webcrypto.CryptoKey
|
||||
@@ -111,7 +186,8 @@ export const getCaCredentials = async ({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
signatureAlgorithm
|
||||
}: TGetCaCredentialsDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
|
||||
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
|
||||
});
|
||||
|
||||
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 caPrivateKey = await crypto.nativeCrypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick(
|
||||
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId),
|
||||
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
|
||||
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
|
||||
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
|
||||
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId),
|
||||
projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
|
||||
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
|
||||
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
|
||||
});
|
||||
|
||||
+74
-13
@@ -32,6 +32,8 @@ import {
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertSignatureAlgorithm,
|
||||
CertSignatureType,
|
||||
CertStatus,
|
||||
TAltNameMapping
|
||||
} from "../../certificate/certificate-types";
|
||||
@@ -48,7 +50,8 @@ import {
|
||||
getCaCertChains,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
parseDistinguishedName,
|
||||
signatureAlgorithmToAlgCfg
|
||||
} from "../certificate-authority-fns";
|
||||
import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||
@@ -1174,7 +1177,10 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
extendedKeyUsages,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm,
|
||||
isFromProfile
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorityWithAssociatedCa | 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.internalCa.activeCaCertId)
|
||||
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" });
|
||||
}
|
||||
|
||||
@@ -1277,13 +1283,43 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const effectiveKeyAlgorithm =
|
||||
(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({
|
||||
name: `CN=${commonName}`,
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
signingAlgorithm: keyGenAlg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||
@@ -1296,7 +1332,8 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
signatureAlgorithm: signingAlg
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
@@ -1319,7 +1356,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
// handle key usages
|
||||
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
|
||||
if (keyUsages === undefined && !certificateTemplate) {
|
||||
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||
selectedKeyUsages = isFromProfile ? [] : [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||
}
|
||||
|
||||
if (keyUsages === undefined && certificateTemplate) {
|
||||
@@ -1405,7 +1442,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
signingAlgorithm: signingAlg,
|
||||
extensions
|
||||
});
|
||||
|
||||
@@ -1517,7 +1554,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
notBefore,
|
||||
notAfter,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
extendedKeyUsages,
|
||||
signatureAlgorithm,
|
||||
keyAlgorithm
|
||||
} = dto;
|
||||
|
||||
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.internalCa.activeCaCertId)
|
||||
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" });
|
||||
}
|
||||
|
||||
@@ -1622,7 +1661,29 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
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);
|
||||
|
||||
@@ -1671,7 +1732,7 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
if (csrKeyUsageExtension) {
|
||||
selectedKeyUsages = csrKeyUsages;
|
||||
} 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 { TProjectPermission } from "@app/lib/types";
|
||||
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
@@ -131,6 +136,9 @@ export type TIssueCertFromCaDTO = {
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
signatureAlgorithm?: CertSignatureAlgorithm;
|
||||
keyAlgorithm?: CertKeyAlgorithm;
|
||||
isFromProfile?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO =
|
||||
@@ -148,6 +156,9 @@ export type TSignCertFromCaDTO =
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
isFromProfile?: boolean;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
@@ -163,6 +174,9 @@ export type TSignCertFromCaDTO =
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
signatureAlgorithm?: string;
|
||||
keyAlgorithm?: string;
|
||||
isFromProfile?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaCertificateTemplatesDTO = {
|
||||
@@ -184,6 +198,7 @@ export type TGetCaCredentialsDTO = {
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
signatureAlgorithm?: RsaHashedImportParams | EcKeyImportParams;
|
||||
};
|
||||
|
||||
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)
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user