Merge branch 'main' into ENG-3955

This commit is contained in:
x032205
2025-10-21 20:30:09 -04:00
438 changed files with 25651 additions and 5604 deletions
@@ -40,7 +40,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root { name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones { name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" }, { name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing { name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => { ])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name }); const createdFolder = await createFolder({ path, name });
// check for default environments // check for default environments
@@ -57,7 +57,7 @@ describe("Secret Folder Router", async () => {
{ {
path: "/", path: "/",
expected: { expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }], folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3 length: 3
} }
}, },
@@ -162,4 +162,25 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0); expect(updatedFolderList.folders.length).toEqual(0);
}); });
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
}); });
@@ -41,7 +41,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root { name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones { name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" }, { name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing { name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => { ])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name }); const createdFolder = await createFolder({ path, name });
// check for default environments // check for default environments
@@ -58,7 +58,7 @@ describe("Secret Folder Router", async () => {
{ {
path: "/", path: "/",
expected: { expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }], folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3 length: 3
} }
}, },
@@ -163,4 +163,26 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders"); expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0); expect(updatedFolderList.folders.length).toEqual(0);
}); });
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v2/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
}); });
+12
View File
@@ -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 { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-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 { 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 { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-types";
import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; 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 { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { TCertificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { TCertificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service"; import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service"; import { TConvertorServiceFactory } from "@app/services/convertor/convertor-service";
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
@@ -178,6 +183,8 @@ declare module "fastify" {
type: ActorType; type: ActorType;
id: string; id: string;
orgId: string; orgId: string;
parentOrgId: string;
rootOrgId: string;
}; };
rateLimits: RateLimitConfiguration; rateLimits: RateLimitConfiguration;
// passport data // passport data
@@ -262,7 +269,10 @@ declare module "fastify" {
auditLog: TAuditLogServiceFactory; auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory; auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory; certificate: TCertificateServiceFactory;
certificateV3: TCertificateV3ServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory;
certificateTemplateV2: TCertificateTemplateV2ServiceFactory;
certificateProfile: TCertificateProfileServiceFactory;
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory; sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
sshCertificateTemplate: TSshCertificateTemplateServiceFactory; sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
sshHost: TSshHostServiceFactory; sshHost: TSshHostServiceFactory;
@@ -270,6 +280,7 @@ declare module "fastify" {
certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory; certificateEst: TCertificateEstServiceFactory;
certificateEstV3: TCertificateEstV3ServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
pkiSubscriber: TPkiSubscriberServiceFactory; pkiSubscriber: TPkiSubscriberServiceFactory;
pkiSync: TPkiSyncServiceFactory; pkiSync: TPkiSyncServiceFactory;
@@ -327,6 +338,7 @@ declare module "fastify" {
additionalPrivilege: TAdditionalPrivilegeServiceFactory; additionalPrivilege: TAdditionalPrivilegeServiceFactory;
role: TRoleServiceFactory; role: TRoleServiceFactory;
convertor: TConvertorServiceFactory; convertor: TConvertorServiceFactory;
subOrganization: TSubOrgServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
+32
View File
@@ -266,12 +266,24 @@ import {
TPkiAlerts, TPkiAlerts,
TPkiAlertsInsert, TPkiAlertsInsert,
TPkiAlertsUpdate, TPkiAlertsUpdate,
TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate,
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate,
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert,
TPkiCertificateTemplatesV2Update,
TPkiCollectionItems, TPkiCollectionItems,
TPkiCollectionItemsInsert, TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate, TPkiCollectionItemsUpdate,
TPkiCollections, TPkiCollections,
TPkiCollectionsInsert, TPkiCollectionsInsert,
TPkiCollectionsUpdate, TPkiCollectionsUpdate,
TPkiEstEnrollmentConfigs,
TPkiEstEnrollmentConfigsInsert,
TPkiEstEnrollmentConfigsUpdate,
TPkiSubscribers, TPkiSubscribers,
TPkiSubscribersInsert, TPkiSubscribersInsert,
TPkiSubscribersUpdate, TPkiSubscribersUpdate,
@@ -674,6 +686,26 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate TCertificateTemplatesUpdate
>; >;
[TableName.PkiCertificateTemplateV2]: KnexOriginal.CompositeTableType<
TPkiCertificateTemplatesV2,
TPkiCertificateTemplatesV2Insert,
TPkiCertificateTemplatesV2Update
>;
[TableName.PkiCertificateProfile]: KnexOriginal.CompositeTableType<
TPkiCertificateProfiles,
TPkiCertificateProfilesInsert,
TPkiCertificateProfilesUpdate
>;
[TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType<
TPkiEstEnrollmentConfigs,
TPkiEstEnrollmentConfigsInsert,
TPkiEstEnrollmentConfigsUpdate
>;
[TableName.PkiApiEnrollmentConfig]: KnexOriginal.CompositeTableType<
TPkiApiEnrollmentConfigs,
TPkiApiEnrollmentConfigsInsert,
TPkiApiEnrollmentConfigsUpdate
>;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs, TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert, TCertificateTemplateEstConfigsInsert,
@@ -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");
});
}
}
+2 -1
View File
@@ -26,7 +26,8 @@ export const CertificatesSchema = z.object({
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(),
projectId: z.string(), projectId: z.string(),
pkiSubscriberId: z.string().uuid().nullable().optional() pkiSubscriberId: z.string().uuid().nullable().optional(),
profileId: z.string().uuid().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;
+2 -1
View File
@@ -13,7 +13,8 @@ export const IdentitiesSchema = z.object({
authMethod: z.string().nullable().optional(), authMethod: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: 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>; export type TIdentities = z.infer<typeof IdentitiesSchema>;
+4
View File
@@ -92,8 +92,12 @@ export * from "./pam-folders";
export * from "./pam-resources"; export * from "./pam-resources";
export * from "./pam-sessions"; export * from "./pam-sessions";
export * from "./pki-alerts"; export * from "./pki-alerts";
export * from "./pki-api-enrollment-configs";
export * from "./pki-certificate-profiles";
export * from "./pki-certificate-templates-v2";
export * from "./pki-collection-items"; export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./pki-est-enrollment-configs";
export * from "./pki-subscribers"; export * from "./pki-subscribers";
export * from "./pki-syncs"; export * from "./pki-syncs";
export * from "./project-bots"; export * from "./project-bots";
+10
View File
@@ -23,6 +23,10 @@ export enum TableName {
CertificateBody = "certificate_bodies", CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets", CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates", CertificateTemplate = "certificate_templates",
PkiCertificateTemplateV2 = "pki_certificate_templates_v2",
PkiCertificateProfile = "pki_certificate_profiles",
PkiEstEnrollmentConfig = "pki_est_enrollment_configs",
PkiApiEnrollmentConfig = "pki_api_enrollment_configs",
PkiSubscriber = "pki_subscribers", PkiSubscriber = "pki_subscribers",
PkiAlert = "pki_alerts", PkiAlert = "pki_alerts",
PkiCollection = "pki_collections", PkiCollection = "pki_collections",
@@ -312,6 +316,12 @@ export enum ActionProjectType {
Any = "any" Any = "any"
} }
export enum OrganizationActionScope {
ChildOrganization = "child-organization-only",
ParentOrganization = "parent-organization-only",
Any = "any"
}
export enum TemporaryPermissionMode { export enum TemporaryPermissionMode {
Relative = "relative" Relative = "relative"
} }
+3 -1
View File
@@ -38,7 +38,9 @@ export const OrganizationsSchema = z.object({
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(), maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
maxSharedSecretViewLimit: z.number().nullable().optional(), maxSharedSecretViewLimit: z.number().nullable().optional(),
googleSsoAuthEnforced: z.boolean().default(false), 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>; 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>
>;
+2 -1
View File
@@ -24,7 +24,8 @@ export async function seed(knex: Knex): Promise<void> {
// @ts-ignore // @ts-ignore
id: seedData1.machineIdentity.id, id: seedData1.machineIdentity.id,
name: seedData1.machineIdentity.name, name: seedData1.machineIdentity.name,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
orgId: seedData1.organization.id
} }
]); ]);
const identityUa = await knex(TableName.IdentityUniversalAuth) const identityUa = await knex(TableName.IdentityUniversalAuth)
@@ -8,6 +8,35 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => { export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
const getIdentifierType = async (identifier: string): Promise<"template" | "profile" | null> => {
try {
// Try to find as profile first using internal access
await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
});
return "profile";
} catch (profileError) {
try {
await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
return "template";
} catch (templateError) {
server.log.debug(
{
identifier,
profileError: profileError instanceof Error ? profileError.message : "Unknown error",
templateError: templateError instanceof Error ? templateError.message : "Unknown error"
},
"EST identifier not found as profile or template"
);
return null;
}
}
};
// add support for CSR bodies // add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => { server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try { try {
@@ -59,11 +88,29 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
return; return;
} }
const certificateTemplateId = urlFragments.slice(-2)[0]; const identifier = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true, const identifierType = await getIdentifierType(identifier);
certificateTemplateId if (!identifierType) {
res.raw.statusCode = 404;
res.raw.setHeader("Content-Type", "text/plain");
res.raw.write("Certificate template or profile not found");
res.raw.flushHeaders();
return;
}
let estConfig;
if (identifierType === "profile") {
estConfig = await server.services.certificateProfile.getEstConfigurationByProfile({
profileId: identifier,
isInternal: true
}); });
} else {
estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId: identifier
});
}
if (!estConfig.isEnabled) { if (!estConfig.isEnabled) {
throw new BadRequestError({ throw new BadRequestError({
@@ -95,14 +142,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "POST", method: "POST",
url: "/:certificateTemplateId/simpleenroll", url: "/:identifier/simpleenroll",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.string().min(1), body: z.string().min(1),
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -112,9 +159,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleEnrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleEnroll({ return server.services.certificateEst.simpleEnroll({
csr: req.body, csr: req.body,
certificateTemplateId: req.params.certificateTemplateId, certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
}); });
} }
@@ -122,14 +183,14 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "POST", method: "POST",
url: "/:certificateTemplateId/simplereenroll", url: "/:identifier/simplereenroll",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.string().min(1), body: z.string().min(1),
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -139,9 +200,23 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.simpleReenrollByProfile({
csr: req.body,
profileId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
return server.services.certificateEst.simpleReenroll({ return server.services.certificateEst.simpleReenroll({
csr: req.body, csr: req.body,
certificateTemplateId: req.params.certificateTemplateId, certificateTemplateId: identifier,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
}); });
} }
@@ -149,13 +224,13 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
server.route({ server.route({
method: "GET", method: "GET",
url: "/:certificateTemplateId/cacerts", url: "/:identifier/cacerts",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
params: z.object({ params: z.object({
certificateTemplateId: z.string().min(1) identifier: z.string().min(1)
}), }),
response: { response: {
200: z.string() 200: z.string()
@@ -165,8 +240,20 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only"); void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64"); void res.header("Content-Transfer-Encoding", "base64");
const { identifier } = req.params;
const identifierType = await getIdentifierType(identifier);
if (!identifierType) {
throw new BadRequestError({ message: "Certificate template or profile not found" });
}
if (identifierType === "profile") {
return server.services.certificateEstV3.getCaCertsByProfile({
profileId: identifier
});
}
return server.services.certificateEst.getCaCerts({ return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId certificateTemplateId: identifier
}); });
} }
}); });
+2
View File
@@ -48,12 +48,14 @@ import { registerSshCertRouter } from "./ssh-certificate-router";
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router"; import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
import { registerSshHostGroupRouter } from "./ssh-host-group-router"; import { registerSshHostGroupRouter } from "./ssh-host-group-router";
import { registerSshHostRouter } from "./ssh-host-router"; import { registerSshHostRouter } from "./ssh-host-router";
import { registerSubOrgRouter } from "./sub-org-router";
import { registerTrustedIpRouter } from "./trusted-ip-router"; import { registerTrustedIpRouter } from "./trusted-ip-router";
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
export const registerV1EERoutes = async (server: FastifyZodProvider) => { export const registerV1EERoutes = async (server: FastifyZodProvider) => {
// org role starts with organization // org role starts with organization
await server.register(registerOrgRoleRouter, { prefix: "/organization" }); await server.register(registerOrgRoleRouter, { prefix: "/organization" });
await server.register(registerSubOrgRouter, { prefix: "/sub-organizations" });
await server.register(registerLicenseRouter, { prefix: "/organizations" }); await server.register(registerLicenseRouter, { prefix: "/organizations" });
// depreciated in favour of infisical workspace // depreciated in favour of infisical workspace
+1 -1
View File
@@ -58,7 +58,7 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
const plan = await server.services.license.getOrgPlan({ const plan = await server.services.license.getOrgPlan({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.rootOrgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId, orgId: req.params.organizationId,
refreshCache: req.query.refreshCache refreshCache: req.query.refreshCache
+34 -1
View File
@@ -3,12 +3,35 @@ import { z } from "zod";
import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { AccessScope, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
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) => { export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
@@ -37,6 +60,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { 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 stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.role.createRole({ const role = await server.services.role.createRole({
permission: req.permission, permission: req.permission,
@@ -133,6 +161,11 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { 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 stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.role.updateRole({ const role = await server.services.role.updateRole({
permission: req.permission, permission: req.permission,
+163
View File
@@ -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 { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { TAuditLogs } from "@app/db/schemas"; import { OrganizationActionScope, TAuditLogs } from "@app/db/schemas";
import { import {
decryptLogStream, decryptLogStream,
decryptLogStreamCredentials, decryptLogStreamCredentials,
@@ -45,13 +45,14 @@ export const auditLogStreamServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, scope: OrganizationActionScope.Any,
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, orgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
@@ -94,13 +95,14 @@ export const auditLogStreamServiceFactory = ({
const logStream = await auditLogStreamDAL.findById(logStreamId); const logStream = await auditLogStreamDAL.findById(logStreamId);
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, scope: OrganizationActionScope.Any,
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, orgId: actor.orgId,
logStream.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
@@ -160,13 +162,14 @@ export const auditLogStreamServiceFactory = ({
const logStream = await auditLogStreamDAL.findById(logStreamId); const logStream = await auditLogStreamDAL.findById(logStreamId);
if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` }); if (!logStream) throw new NotFoundError({ message: `Audit Log Stream with ID '${logStreamId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, scope: OrganizationActionScope.Any,
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, orgId: actor.orgId,
logStream.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
@@ -185,14 +188,14 @@ export const auditLogStreamServiceFactory = ({
const logStream = await auditLogStreamDAL.findById(logStreamId); const logStream = await auditLogStreamDAL.findById(logStreamId);
if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` }); if (!logStream) throw new NotFoundError({ message: `Audit log stream with ID '${logStreamId}' not found` });
const { permission } = await permissionService.getOrgPermission({
const { permission } = await permissionService.getOrgPermission( scope: OrganizationActionScope.Any,
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
logStream.orgId, orgId: actor.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
@@ -206,13 +209,14 @@ export const auditLogStreamServiceFactory = ({
}; };
const list = async (actor: OrgServiceActor) => { const list = async (actor: OrgServiceActor) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, scope: OrganizationActionScope.Any,
actor.id, actor: actor.type,
actor.orgId, actorId: actor.id,
actor.authMethod, orgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); actorOrgId: actor.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context"; 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 { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
@@ -47,13 +47,14 @@ export const auditLogServiceFactory = ({
); );
} else { } else {
// Organization-wide logs // Organization-wide logs
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAuditLogsActions.Read, OrgPermissionAuditLogsActions.Read,
@@ -173,6 +173,9 @@ export enum EventType {
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth", UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-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", ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth", UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth", GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
@@ -352,9 +355,18 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template", GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config", CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
CREATE_CERTIFICATE_PROFILE = "create-certificate-profile",
UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile",
DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile",
GET_CERTIFICATE_PROFILE = "get-certificate-profile",
LIST_CERTIFICATE_PROFILES = "list-certificate-profiles",
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration", ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration", ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -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 }[]; type TSecretMetadata = { key: string; value: string }[];
interface GetSecretEvent { interface GetSecretEvent {
@@ -2512,46 +2540,6 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet metadata: Record<string, string>; // no metadata yet
} }
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface OrgAdminAccessProjectEvent { interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT; type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: { metadata: {
@@ -2598,6 +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 { interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION; type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: { metadata: {
@@ -3863,6 +3983,8 @@ interface PamResourceDeleteEvent {
} }
export type Event = export type Event =
| CreateSubOrganizationEvent
| UpdateSubOrganizationEvent
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
| CreateSecretEvent | CreateSecretEvent
@@ -4051,13 +4173,22 @@ export type Event =
| LoadProjectKmsBackupEvent | LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent | OrgAdminAccessProjectEvent
| OrgAdminBypassSSOEvent | OrgAdminBypassSSOEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| CreateCertificateTemplate
| UpdateCertificateTemplate
| DeleteCertificateTemplate
| GetCertificateTemplate
| ListCertificateTemplates
| CreateCertificateProfile
| UpdateCertificateProfile
| DeleteCertificateProfile
| GetCertificateProfile
| ListCertificateProfiles
| IssueCertificateFromProfile
| SignCertificateFromProfile
| OrderCertificateFromProfile
| GetAzureAdCsTemplatesEvent | GetAzureAdCsTemplatesEvent
| AttemptCreateSlackIntegration | AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration | AttemptReinstallSlackIntegration
@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { import {
@@ -134,13 +134,14 @@ export const dynamicSecretServiceFactory = ({
isGatewayV1 = false; isGatewayV1 = false;
} }
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
gateway?.orgId ?? gatewayv2?.orgId, orgId: gateway?.orgId || gatewayv2?.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(orgPermission).throwUnlessCan( ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways, OrgPermissionGatewayActions.AttachGateways,
@@ -297,13 +298,14 @@ export const dynamicSecretServiceFactory = ({
isGatewayV1 = false; isGatewayV1 = false;
} }
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: gateway?.orgId || gatewayv2?.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(orgPermission).throwUnlessCan( ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways, OrgPermissionGatewayActions.AttachGateways,
@@ -3,6 +3,7 @@ import { STSServiceException } from "@aws-sdk/client-sts";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { OrganizationActionScope } from "@app/db/schemas";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
@@ -51,13 +52,14 @@ export const externalKmsServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
}: TCreateExternalKmsDTO) => { }: TCreateExternalKmsDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -154,13 +156,14 @@ export const externalKmsServiceFactory = ({
actorAuthMethod actorAuthMethod
}: TUpdateExternalKmsDTO) => { }: TUpdateExternalKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId); const kmsDoc = await kmsDAL.findById(kmsId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
kmsDoc.orgId, orgId: kmsDoc.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
const plan = await licenseService.getPlan(kmsDoc.orgId); 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 deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId); const kmsDoc = await kmsDAL.findById(kmsId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
kmsDoc.orgId, orgId: kmsDoc.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
@@ -278,13 +282,14 @@ export const externalKmsServiceFactory = ({
}; };
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => { const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId }); 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 findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId); const kmsDoc = await kmsDAL.findById(kmsId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
kmsDoc.orgId, orgId: kmsDoc.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
@@ -342,13 +348,14 @@ export const externalKmsServiceFactory = ({
name: kmsName name: kmsName
}: TGetExternalKmsBySlugDTO) => { }: TGetExternalKmsBySlugDTO) => {
const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId }); const kmsDoc = await kmsDAL.findOne({ name: kmsName, orgId: actorOrgId });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
kmsDoc.orgId, orgId: kmsDoc.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id }); const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
@@ -3,7 +3,7 @@ import net from "node:net";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; 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 { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { DatabaseErrorCode } from "@app/lib/error-codes"; import { DatabaseErrorCode } from "@app/lib/error-codes";
@@ -73,13 +73,14 @@ export const gatewayV2ServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
ActorType.IDENTITY, scope: OrganizationActionScope.Any,
actor: ActorType.IDENTITY,
actorId, actorId,
orgId, orgId,
actorAuthMethod, actorAuthMethod,
orgId actorOrgId: orgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways, OrgPermissionGatewayActions.CreateGateways,
@@ -258,13 +259,14 @@ export const gatewayV2ServiceFactory = ({
}; };
const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways, OrgPermissionGatewayActions.ListGateways,
@@ -815,13 +817,14 @@ export const gatewayV2ServiceFactory = ({
throw new NotFoundError({ message: `Gateway ${id} not found` }); throw new NotFoundError({ message: `Gateway ${id} not found` });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
gateway.orgId, orgId: gateway.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.DeleteGateways, OrgPermissionGatewayActions.DeleteGateways,
@@ -845,13 +848,14 @@ export const gatewayV2ServiceFactory = ({
}; };
const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways, OrgPermissionGatewayActions.CreateGateways,
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { z } from "zod"; import { z } from "zod";
import { OrganizationActionScope } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
@@ -68,13 +69,14 @@ export const gatewayServiceFactory = ({
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
ActorType.IDENTITY, actor: ActorType.IDENTITY,
actorId, actorId,
orgId, orgId,
actorAuthMethod, actorAuthMethod,
orgId actorOrgId: orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways, OrgPermissionGatewayActions.CreateGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
@@ -480,13 +482,14 @@ export const gatewayServiceFactory = ({
}; };
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways, OrgPermissionGatewayActions.ListGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
@@ -501,13 +504,14 @@ export const gatewayServiceFactory = ({
}; };
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways, OrgPermissionGatewayActions.ListGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
@@ -521,13 +525,14 @@ export const gatewayServiceFactory = ({
}; };
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.EditGateways, OrgPermissionGatewayActions.EditGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
@@ -542,13 +547,14 @@ export const gatewayServiceFactory = ({
}; };
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.DeleteGateways, OrgPermissionGatewayActions.DeleteGateways,
OrgPermissionSubjects.Gateway OrgPermissionSubjects.Gateway
@@ -6,7 +6,7 @@ import { paginateGraphql } from "@octokit/plugin-paginate-graphql";
import { Octokit as OctokitRest } from "@octokit/rest"; import { Octokit as OctokitRest } from "@octokit/rest";
import RE2 from "re2"; 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -104,13 +104,14 @@ export const githubOrgSyncServiceFactory = ({
githubOrgAccessToken, githubOrgAccessToken,
isActive isActive
}: TCreateGithubOrgSyncDTO) => { }: TCreateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, scope: OrganizationActionScope.ParentOrganization,
orgPermission.id, actor: orgPermission.type,
orgPermission.orgId, actorId: orgPermission.id,
orgPermission.authMethod, orgId: orgPermission.orgId,
orgPermission.orgId actorAuthMethod: orgPermission.authMethod,
); actorOrgId: orgPermission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId); const plan = await licenseService.getPlan(orgPermission.orgId);
@@ -162,13 +163,14 @@ export const githubOrgSyncServiceFactory = ({
githubOrgAccessToken, githubOrgAccessToken,
isActive isActive
}: TUpdateGithubOrgSyncDTO) => { }: TUpdateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, scope: OrganizationActionScope.ParentOrganization,
orgPermission.orgId, actorId: orgPermission.id,
orgPermission.authMethod, orgId: orgPermission.orgId,
orgPermission.orgId actorAuthMethod: orgPermission.authMethod,
); actorOrgId: orgPermission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId); const plan = await licenseService.getPlan(orgPermission.orgId);
@@ -226,13 +228,14 @@ export const githubOrgSyncServiceFactory = ({
}; };
const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actor: orgPermission.type,
orgPermission.id, actorId: orgPermission.id,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
@@ -256,13 +259,14 @@ export const githubOrgSyncServiceFactory = ({
}; };
const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => { const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actorId: orgPermission.id,
orgPermission.id, actor: orgPermission.type,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
@@ -422,13 +426,14 @@ export const githubOrgSyncServiceFactory = ({
}; };
const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => { const validateGithubToken = async ({ orgPermission, githubOrgAccessToken }: TValidateGithubTokenDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, actorId: orgPermission.id,
orgPermission.id, actor: orgPermission.type,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorAuthMethod: orgPermission.authMethod,
orgPermission.orgId actorOrgId: orgPermission.orgId,
); scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
@@ -509,13 +514,14 @@ export const githubOrgSyncServiceFactory = ({
}; };
const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise<TSyncResult> => { const syncAllTeams = async ({ orgPermission }: TSyncAllTeamsDTO): Promise<TSyncResult> => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
orgPermission.type, scope: OrganizationActionScope.ParentOrganization,
orgPermission.id, actor: orgPermission.type,
orgPermission.orgId, orgId: orgPermission.orgId,
orgPermission.authMethod, actorId: orgPermission.id,
orgPermission.orgId actorAuthMethod: orgPermission.authMethod,
); actorOrgId: orgPermission.orgId
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit, OrgPermissionActions.Edit,
+31 -24
View File
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify"; 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 { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -73,13 +73,14 @@ export const groupServiceFactory = ({
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -167,13 +168,14 @@ export const groupServiceFactory = ({
}: TUpdateGroupDTO) => { }: TUpdateGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
@@ -270,13 +272,14 @@ export const groupServiceFactory = ({
const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => { const deleteGroup = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -297,17 +300,18 @@ export const groupServiceFactory = ({
const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => { const getGroupById = async ({ id, actor, actorId, actorAuthMethod, actorOrgId }: TGetGroupByIdDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findById(id); const group = await groupDAL.findById(id);
if (!group) { if (!group || group.orgId !== actorOrgId) {
throw new NotFoundError({ throw new NotFoundError({
message: `Cannot find group with ID ${id}` message: `Cannot find group with ID ${id}`
}); });
@@ -330,13 +334,14 @@ export const groupServiceFactory = ({
}: TListGroupUsersDTO) => { }: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
const group = await groupDAL.findOne({ const group = await groupDAL.findOne({
@@ -365,13 +370,14 @@ export const groupServiceFactory = ({
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); 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, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists // check if group with slug exists
@@ -451,13 +457,14 @@ export const groupServiceFactory = ({
}: TRemoveUserFromGroupDTO) => { }: TRemoveUserFromGroupDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
// check if group with slug exists // check if group with slug exists
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { OrganizationActionScope } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { import {
@@ -68,13 +69,14 @@ export const identityAuthTemplateServiceFactory = ({
templateFields: Record<string, unknown>; templateFields: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => { } & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -113,13 +115,14 @@ export const identityAuthTemplateServiceFactory = ({
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
template.orgId, orgId: template.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -227,13 +230,14 @@ export const identityAuthTemplateServiceFactory = ({
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
template.orgId, orgId: template.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -282,13 +286,14 @@ export const identityAuthTemplateServiceFactory = ({
throw new NotFoundError({ message: "Template not found" }); throw new NotFoundError({ message: "Template not found" });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
template.orgId, orgId: template.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -316,13 +321,14 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TListIdentityAuthTemplatesDTO) => { }: TListIdentityAuthTemplatesDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -352,13 +358,14 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TGetTemplatesByAuthMethodDTO) => { }: TGetTemplatesByAuthMethodDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -385,13 +392,14 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TFindTemplateUsagesDTO) => { }: TFindTemplateUsagesDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -415,13 +423,14 @@ export const identityAuthTemplateServiceFactory = ({
actorOrgId actorOrgId
}: TUnlinkTemplateUsageDTO) => { }: TUnlinkTemplateUsageDTO) => {
await $checkPlan(actorOrgId); await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate OrgPermissionSubjects.MachineIdentityAuthTemplate
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { OrganizationActionScope } from "@app/db/schemas";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal"; import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -46,13 +47,14 @@ export const kmipOperationServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TKmipCreateDTO) => { }: TKmipCreateDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
@@ -287,13 +293,14 @@ export const kmipOperationServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TKmipGetAttributesDTO) => { }: TKmipGetAttributesDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); 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 locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
@@ -377,13 +385,14 @@ export const kmipOperationServiceFactory = ({
actorOrgId, actorOrgId,
kmipMetadata kmipMetadata
}: TKmipRegisterDTO) => { }: TKmipRegisterDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
+17 -8
View File
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; 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 { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
@@ -401,13 +401,14 @@ export const kmipServiceFactory = ({
}; };
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => { const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
const kmipConfig = await kmipOrgConfigDAL.findOne({ const kmipConfig = await kmipOrgConfigDAL.findOne({
@@ -566,7 +567,14 @@ export const kmipServiceFactory = ({
}; };
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => { 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({ const kmipConfig = await kmipOrgConfigDAL.findOne({
orgId: actorOrgId orgId: actorOrgId
@@ -759,13 +767,14 @@ export const kmipServiceFactory = ({
keyAlgorithm, keyAlgorithm,
hostnamesOrIps hostnamesOrIps
}: TRegisterServerDTO) => { }: TRegisterServerDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
@@ -1,7 +1,14 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Knex } from "knex"; 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 { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -119,7 +126,14 @@ export const ldapConfigServiceFactory = ({
groupSearchFilter, groupSearchFilter,
caCert caCert
}: TCreateLdapCfgDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -238,7 +252,14 @@ export const ldapConfigServiceFactory = ({
groupSearchFilter, groupSearchFilter,
caCert caCert
}: TUpdateLdapCfgDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -316,7 +337,14 @@ export const ldapConfigServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TGetLdapCfgDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
return getLdapCfg({ return getLdapCfg({
orgId orgId
@@ -649,7 +677,14 @@ export const ldapConfigServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TGetLdapGroupMapsDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
const ldapConfig = await ldapConfigDAL.findOne({ const ldapConfig = await ldapConfigDAL.findOne({
@@ -678,7 +713,14 @@ export const ldapConfigServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TCreateLdapGroupMapDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -732,7 +774,14 @@ export const ldapConfigServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TDeleteLdapGroupMapDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -771,7 +820,14 @@ export const ldapConfigServiceFactory = ({
caCert, caCert,
url url
}: TTestLdapConnectionDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false, enterpriseCertificateSyncs: false,
enterpriseAppConnections: true, enterpriseAppConnections: true,
machineIdentityAuthTemplates: false machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false
}; };
}; };
+28 -5
View File
@@ -10,6 +10,7 @@ export const licenseDALFactory = (db: TDbClient) => {
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => { const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
try { try {
const doc = await (tx || db.replicaNode())(TableName.Membership) const doc = await (tx || db.replicaNode())(TableName.Membership)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
.andWhere((bd) => { .andWhere((bd) => {
if (orgId) { if (orgId) {
@@ -18,6 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => {
}) })
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.isGhost`, false) .where(`${TableName.Users}.isGhost`, false)
.whereNull(`${TableName.Organization}.rootOrgId`)
.count(); .count();
return Number(doc?.[0]?.count ?? 0); return Number(doc?.[0]?.count ?? 0);
} catch (error) { } 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) => { const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => {
try { try {
// count org users // count org users
const userDoc = await (tx || db.replicaNode())(TableName.Membership) const userDoc = await (tx || db.replicaNode())(TableName.Membership)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization }) .where({ status: OrgMembershipStatus.Accepted, scope: AccessScope.Organization })
.whereNotNull(`${TableName.Membership}.actorUserId`) .whereNotNull(`${TableName.Membership}.actorUserId`)
.andWhere((bd) => { .andWhere((bd) => {
@@ -38,17 +61,17 @@ export const licenseDALFactory = (db: TDbClient) => {
}) })
.join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.Membership}.actorUserId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.isGhost`, false) .where(`${TableName.Users}.isGhost`, false)
.whereNull(`${TableName.Organization}.rootOrgId`)
.count(); .count();
const userCount = Number(userDoc?.[0].count); const userCount = Number(userDoc?.[0].count);
// count org identities // count org identities
const identityDoc = await (tx || db.replicaNode())(TableName.Membership) const identityDoc = await (tx || db.replicaNode())(TableName.Identity)
.where({ scope: AccessScope.Organization }) .join(TableName.Organization, `${TableName.Identity}.orgId`, `${TableName.Organization}.id`)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.where((bd) => { .where((bd) => {
if (orgId) { if (orgId) {
void bd.where(`${TableName.Membership}.scopeOrgId`, orgId); void bd.where(`${TableName.Organization}.rootOrgId`, orgId).orWhere(`${TableName.Organization}.id`, orgId);
} }
}) })
.count(); .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, rbac: false,
githubOrgSync: false, githubOrgSync: false,
customRateLimits: false, customRateLimits: false,
subOrganization: false,
customAlerts: false, customAlerts: false,
secretAccessInsights: false, secretAccessInsights: false,
auditLogs: false, auditLogs: false,
@@ -67,6 +68,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
fips: false, fips: false,
eventSubscriptions: false, eventSubscriptions: false,
machineIdentityAuthTemplates: false, machineIdentityAuthTemplates: false,
pkiLegacyTemplates: false,
pam: false pam: false
}); });
@@ -9,12 +9,12 @@ import { AxiosError } from "axios";
import { CronJob } from "cron"; import { CronJob } from "cron";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrganizationActionScope } from "@app/db/schemas";
import { TKeyStoreFactory } from "@app/keystore/keystore"; import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { verifyOfflineLicense } from "@app/lib/crypto"; import { verifyOfflineLicense } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -45,11 +45,10 @@ import {
} from "./license-types"; } from "./license-types";
type TLicenseServiceFactoryDep = { type TLicenseServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">; orgDAL: Pick<TOrgDALFactory, "findRootOrgDetails" | "countAllOrgMembers" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDAL: TLicenseDALFactory; licenseDAL: TLicenseDALFactory;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">; keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
}; };
@@ -66,7 +65,6 @@ export const licenseServiceFactory = ({
permissionService, permissionService,
licenseDAL, licenseDAL,
keyStore, keyStore,
identityOrgMembershipDAL,
projectDAL projectDAL
}: TLicenseServiceFactoryDep) => { }: TLicenseServiceFactoryDep) => {
let isValidLicense = false; let isValidLicense = false;
@@ -199,19 +197,21 @@ export const licenseServiceFactory = ({
return JSON.parse(cachedPlan) as TFeatureSet; 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` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const rootOrgId = org.id;
const { const {
data: { currentPlan } data: { currentPlan }
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan` `/api/license-server/v1/customers/${org.customerId}/cloud-plan`
); );
const workspacesUsed = await projectDAL.countOfOrgProjects(orgId); const workspacesUsed = await projectDAL.countOfOrgProjects(rootOrgId);
currentPlan.workspacesUsed = workspacesUsed; currentPlan.workspacesUsed = workspacesUsed;
const membersUsed = await licenseDAL.countOfOrgMembers(orgId); const membersUsed = await licenseDAL.countOfOrgMembers(rootOrgId);
currentPlan.membersUsed = membersUsed; currentPlan.membersUsed = membersUsed;
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId); const identityUsed = await licenseDAL.countOrgUsersAndIdentities(rootOrgId);
currentPlan.identitiesUsed = identityUsed; currentPlan.identitiesUsed = identityUsed;
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) { if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
@@ -284,19 +284,20 @@ export const licenseServiceFactory = ({
}; };
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
if (instanceType === InstanceType.Cloud) { const org = await orgDAL.findRootOrgDetails(orgId, tx);
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); const rootOrgId = org.id;
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); if (instanceType === InstanceType.Cloud) {
const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx);
if (org?.customerId) { if (org?.customerId) {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity, quantity,
quantityIdentities quantityIdentities
}); });
} }
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) { } else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
@@ -307,7 +308,7 @@ export const licenseServiceFactory = ({
usedIdentitySeats usedIdentitySeats
}); });
} }
await refreshPlan(orgId); await refreshPlan(rootOrgId);
}; };
// below all are api calls // below all are api calls
@@ -319,7 +320,14 @@ export const licenseServiceFactory = ({
actorAuthMethod, actorAuthMethod,
billingCycle billingCycle
}: TOrgPlansTableDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` `/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
@@ -336,7 +344,14 @@ export const licenseServiceFactory = ({
projectId, projectId,
refreshCache refreshCache
}: TOrgPlanDTO) => { }: TOrgPlanDTO) => {
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); await permissionService.getOrgPermission({
actorId,
actor,
orgId,
actorOrgId,
actorAuthMethod,
scope: OrganizationActionScope.ParentOrganization
});
if (refreshCache) { if (refreshCache) {
await refreshPlan(orgId); await refreshPlan(orgId);
} }
@@ -352,13 +367,20 @@ export const licenseServiceFactory = ({
actorAuthMethod, actorAuthMethod,
success_url success_url
}: TStartOrgTrialDTO) => { }: 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -384,13 +406,20 @@ export const licenseServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TCreateOrgPortalSession) => { }: 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: "Organization not found" message: "Organization not found"
@@ -433,10 +462,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -502,7 +538,7 @@ export const licenseServiceFactory = ({
const getUsageMetrics = async (orgId: string) => { const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId), orgDAL.countAllOrgMembers(orgId),
identityOrgMembershipDAL.countAllOrgIdentities({ scopeOrgId: orgId }), licenseDAL.countOfOrgIdentities(orgId),
projectDAL.countOfOrgProjects(orgId) projectDAL.countOfOrgProjects(orgId)
]); ]);
@@ -516,10 +552,17 @@ export const licenseServiceFactory = ({
// returns org current plan feature table // returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -553,10 +596,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -578,13 +628,20 @@ export const licenseServiceFactory = ({
name, name,
email email
}: TUpdateOrgBillingDetailsDTO) => { }: 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -601,10 +658,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPmtMethodsDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -628,13 +692,20 @@ export const licenseServiceFactory = ({
success_url, success_url,
cancel_url cancel_url
}: TAddOrgPmtMethodDTO) => { }: 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -660,13 +731,20 @@ export const licenseServiceFactory = ({
orgId, orgId,
pmtMethodId pmtMethodId
}: TDelOrgPmtMethodDTO) => { }: 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -692,10 +770,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgTaxIds = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgTaxIdDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` 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 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` 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 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( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionBillingActions.ManageBilling, OrgPermissionBillingActions.ManageBilling,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -754,10 +853,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgId, actorAuthMethod, orgId }: TOrgInvoiceDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -771,10 +877,17 @@ export const licenseServiceFactory = ({
}; };
const getOrgLicenses = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgLicensesDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionBillingActions.Read, OrgPermissionSubjects.Billing);
const organization = await orgDAL.findOrgById(orgId); const organization = await orgDAL.findById(orgId);
if (!organization) { if (!organization) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
@@ -819,7 +932,6 @@ export const licenseServiceFactory = ({
getLicenseId, getLicenseId,
invalidateGetPlan, invalidateGetPlan,
updateSubscriptionOrgMemberCount, updateSubscriptionOrgMemberCount,
refreshPlan,
getOrgPlan, getOrgPlan,
getOrgPlansTableByBillCycle, getOrgPlansTableByBillCycle,
startOrgTrial, startOrgTrial,
@@ -33,6 +33,7 @@ export type TFeatureSet = {
membersUsed: number; membersUsed: number;
identityLimit: null; identityLimit: null;
identitiesUsed: number; identitiesUsed: number;
subOrganization: false;
environmentLimit: null; environmentLimit: null;
environmentsUsed: 0; environmentsUsed: 0;
secretVersioning: true; secretVersioning: true;
@@ -78,6 +79,7 @@ export type TFeatureSet = {
enterpriseCertificateSyncs: false; enterpriseCertificateSyncs: false;
enterpriseAppConnections: false; enterpriseAppConnections: false;
machineIdentityAuthTemplates: false; machineIdentityAuthTemplates: false;
pkiLegacyTemplates: false;
fips: false; fips: false;
eventSubscriptions: false; eventSubscriptions: false;
pam: false; pam: false;
@@ -2,7 +2,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; 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 { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@@ -118,13 +118,14 @@ export const oidcConfigServiceFactory = ({
} }
if (dto.type === "external") { if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
dto.actor, actorId: dto.actorId,
dto.actorId, actor: dto.actor,
dto.organizationId, orgId: dto.organizationId,
dto.actorAuthMethod, actorOrgId: dto.actorOrgId,
dto.actorOrgId actorAuthMethod: dto.actorAuthMethod,
); scope: OrganizationActionScope.ParentOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); 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." "Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
}); });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor,
actorId, actorId,
org.id, actor,
orgId: org.id,
actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId scope: OrganizationActionScope.ParentOrganization
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
if (org.googleSsoAuthEnforced && isActive) { 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." "Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
}); });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor,
actorId, actorId,
org.id, actor,
orgId: org.id,
actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId scope: OrganizationActionScope.ParentOrganization
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
if (org.googleSsoAuthEnforced && isActive) { if (org.googleSsoAuthEnforced && isActive) {
@@ -764,7 +767,14 @@ export const oidcConfigServiceFactory = ({
}; };
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => { 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({ const oidcConfig = await oidcConfigDAL.findOne({
orgId, orgId,
@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; 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 { 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 { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -459,13 +459,14 @@ export const pamAccountServiceFactory = ({
const project = await projectDAL.findById(session.projectId); const project = await projectDAL.findById(session.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
project.orgId, orgId: project.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways, OrgPermissionGatewayActions.CreateGateways,
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability"; 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -102,13 +102,14 @@ export const pamSessionServiceFactory = ({
const project = await projectDAL.findById(session.projectId); const project = await projectDAL.findById(session.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
project.orgId, orgId: project.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways, OrgPermissionGatewayActions.CreateGateways,
@@ -142,13 +143,14 @@ export const pamSessionServiceFactory = ({
const project = await projectDAL.findById(session.projectId); const project = await projectDAL.findById(session.projectId);
if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${session.projectId}' not found` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
project.orgId, orgId: project.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
if (actor.type === ActorType.IDENTITY) { if (actor.type === ActorType.IDENTITY) {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -5,6 +5,7 @@ import {
ProjectPermissionAppConnectionActions, ProjectPermissionAppConnectionActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateProfileActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionCommitsActions, ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -72,8 +73,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionPkiTemplateActions.Edit, ProjectPermissionPkiTemplateActions.Edit,
ProjectPermissionPkiTemplateActions.Create, ProjectPermissionPkiTemplateActions.Create,
ProjectPermissionPkiTemplateActions.Delete, ProjectPermissionPkiTemplateActions.Delete,
ProjectPermissionPkiTemplateActions.IssueCert, ProjectPermissionPkiTemplateActions.IssueCert, // deprecated
ProjectPermissionPkiTemplateActions.ListCerts ProjectPermissionPkiTemplateActions.ListCerts // deprecated
], ],
ProjectPermissionSub.CertificateTemplates ProjectPermissionSub.CertificateTemplates
); );
@@ -99,6 +100,17 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete,
ProjectPermissionCertificateProfileActions.IssueCert
],
ProjectPermissionSub.CertificateProfiles
);
can( can(
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback], [ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
ProjectPermissionSub.Commits ProjectPermissionSub.Commits
@@ -443,6 +455,7 @@ const buildMemberPermissionRules = () => {
// double check if all CRUD are needed for CA and Certificates // double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities); can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
can( can(
[ [
@@ -454,7 +467,15 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Certificates ProjectPermissionSub.Certificates
); );
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates); can(
[
ProjectPermissionCertificateProfileActions.Read,
ProjectPermissionCertificateProfileActions.Edit,
ProjectPermissionCertificateProfileActions.Create,
ProjectPermissionCertificateProfileActions.Delete
],
ProjectPermissionSub.CertificateProfiles
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections); can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
@@ -15,6 +15,11 @@ export enum OrgPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum OrgPermissionSubOrgActions {
Create = "create",
DirectAccess = "direct-access"
}
export enum OrgPermissionAppConnectionActions { export enum OrgPermissionAppConnectionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -117,7 +122,8 @@ export enum OrgPermissionSubjects {
Kmip = "kmip", Kmip = "kmip",
Gateway = "gateway", Gateway = "gateway",
Relay = "relay", Relay = "relay",
SecretShare = "secret-share" SecretShare = "secret-share",
SubOrganization = "sub-organization"
} }
export type AppConnectionSubjectFields = { export type AppConnectionSubjectFields = {
@@ -128,6 +134,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
| [OrgPermissionActions.Create, OrgPermissionSubjects.Project] | [OrgPermissionActions.Create, OrgPermissionSubjects.Project]
| [OrgPermissionActions, OrgPermissionSubjects.Role] | [OrgPermissionActions, OrgPermissionSubjects.Role]
| [OrgPermissionSubOrgActions, OrgPermissionSubjects.SubOrganization]
| [OrgPermissionActions, OrgPermissionSubjects.Member] | [OrgPermissionActions, OrgPermissionSubjects.Member]
| [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [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."), 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.") 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({ z.object({
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."), 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.") action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
@@ -308,6 +321,10 @@ const buildAdminPermission = () => {
// ws permissions // ws permissions
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Project); can(OrgPermissionActions.Create, OrgPermissionSubjects.Project);
can(OrgPermissionSubOrgActions.Create, OrgPermissionSubjects.SubOrganization);
can(OrgPermissionSubOrgActions.DirectAccess, OrgPermissionSubjects.SubOrganization);
// role permission // role permission
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Role); can(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
@@ -19,6 +19,7 @@ interface TPermissionDataReturn extends TMemberships {
orgAuthEnforced?: boolean | null; orgAuthEnforced?: boolean | null;
orgGoogleSsoAuthEnforced?: boolean | null; orgGoogleSsoAuthEnforced?: boolean | null;
shouldUseNewPrivilegeSystem?: boolean | null; shouldUseNewPrivilegeSystem?: boolean | null;
rootOrgId?: string | null;
bypassOrgAuthEnabled?: boolean | null; bypassOrgAuthEnabled?: boolean | null;
roles: { roles: {
id: string; id: string;
@@ -273,7 +274,8 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"), 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({ const data = sqlNestRelationships({
@@ -283,6 +285,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
MembershipsSchema.extend({ MembershipsSchema.extend({
orgAuthEnforced: z.boolean().optional().nullable(), orgAuthEnforced: z.boolean().optional().nullable(),
shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(), shouldUseNewPrivilegeSystem: z.boolean().optional().nullable(),
rootOrgId: z.string().optional().nullable(),
orgGoogleSsoAuthEnforced: z.boolean(), orgGoogleSsoAuthEnforced: z.boolean(),
bypassOrgAuthEnabled: z.boolean() bypassOrgAuthEnabled: z.boolean()
}).parse(el), }).parse(el),
@@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import { Knex } from "knex"; 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 { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission"; import { OrgPermissionSet } from "./org-permission";
@@ -18,21 +18,6 @@ export type TBuildOrgPermissionDTO = {
role: string; 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 = { export type TGetServiceTokenProjectPermissionArg = {
serviceTokenId: string; serviceTokenId: string;
projectId: string; projectId: string;
@@ -54,17 +39,12 @@ export type TGetOrgPermissionArg = {
actorId: string; actorId: string;
orgId: string; orgId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId?: string; actorOrgId: string;
scope: OrganizationActionScope;
}; };
export type TPermissionServiceFactory = { export type TPermissionServiceFactory = {
getOrgPermission: ( getOrgPermission: (arg: TGetOrgPermissionArg) => Promise<{
type: ActorType,
id: string,
orgId: string,
authMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => Promise<{
permission: MongoAbility<OrgPermissionSet, MongoQuery>; permission: MongoAbility<OrgPermissionSet, MongoQuery>;
memberships: Array< memberships: Array<
TMemberships & { TMemberships & {
@@ -7,6 +7,7 @@ import { Knex } from "knex";
import { import {
AccessScope, AccessScope,
ActionProjectType, ActionProjectType,
OrganizationActionScope,
OrgMembershipRole, OrgMembershipRole,
ProjectMembershipRole, ProjectMembershipRole,
ServiceTokenScopes ServiceTokenScopes
@@ -179,14 +180,15 @@ export const permissionServiceFactory = ({
// return minTtl; // return minTtl;
// }; // };
const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ( const getOrgPermission: TPermissionServiceFactory["getOrgPermission"] = async ({
type, actor,
id, actorId,
orgId, orgId,
authMethod, actorOrgId,
actorOrgId scope,
) => { actorAuthMethod
if (type !== ActorType.USER && type !== ActorType.IDENTITY) { }) => {
if (actor !== ActorType.USER && actor !== ActorType.IDENTITY) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid actor provided", message: "Invalid actor provided",
name: "Get org permission" name: "Get org permission"
@@ -202,11 +204,19 @@ export const permissionServiceFactory = ({
scope: AccessScope.Organization, scope: AccessScope.Organization,
orgId orgId
}, },
actorId: id, actorId,
actorType: type actorType: actor
}); });
if (!permissionData?.length) throw new ForbiddenRequestError({ name: "You are not member of this organization" }); 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 permissionFromRoles = permissionData.flatMap((membership) => {
const activeRoles = membership?.roles const activeRoles = membership?.roles
.filter( .filter(
@@ -227,7 +237,7 @@ export const permissionServiceFactory = ({
permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role))); permissionData.some((memberships) => memberships.roles.some((el) => role === (el.customRoleSlug || el.role)));
validateOrgSSO( validateOrgSSO(
authMethod, actorAuthMethod,
permissionData?.[0].orgAuthEnforced, permissionData?.[0].orgAuthEnforced,
Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced), Boolean(permissionData?.[0].orgGoogleSsoAuthEnforced),
Boolean(permissionData?.[0].bypassOrgAuthEnabled), Boolean(permissionData?.[0].bypassOrgAuthEnabled),
@@ -111,6 +111,14 @@ export enum ProjectPermissionPkiSubscriberActions {
ListCerts = "list-certs" ListCerts = "list-certs"
} }
export enum ProjectPermissionCertificateProfileActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueCert = "issue-cert"
}
export enum ProjectPermissionSecretSyncActions { export enum ProjectPermissionSecretSyncActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -249,7 +257,8 @@ export enum ProjectPermissionSub {
PamFolders = "pam-folders", PamFolders = "pam-folders",
PamResources = "pam-resources", PamResources = "pam-resources",
PamAccounts = "pam-accounts", PamAccounts = "pam-accounts",
PamSessions = "pam-sessions" PamSessions = "pam-sessions",
CertificateProfiles = "certificate-profiles"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -438,7 +447,8 @@ export type ProjectPermissionSet =
ProjectPermissionPamAccountActions, ProjectPermissionPamAccountActions,
ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields) ProjectPermissionSub.PamAccounts | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
] ]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionCertificateProfileActions, ProjectPermissionSub.CertificateProfiles];
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -1109,6 +1119,13 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.CertificateProfiles).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCertificateProfileActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; 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." message: "Failed to access project templates due to plan restriction. Upgrade plan to access project templates."
}); });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
actor.orgId, orgId: actor.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); 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}"` }); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with Name "${name}"` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
projectTemplate.orgId, orgId: projectTemplate.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); 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}` }); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
projectTemplate.orgId, orgId: projectTemplate.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates); 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." message: "Failed to create project template due to plan restriction. Upgrade plan to access project templates."
}); });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
actor.orgId, orgId: actor.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates); 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}` }); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
projectTemplate.orgId, orgId: projectTemplate.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (projectTemplate.type !== ProjectType.SecretManager && environments) 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}` }); if (!projectTemplate) throw new NotFoundError({ message: `Could not find project template with ID ${id}` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actor: actor.type,
actor.id, actorId: actor.id,
projectTemplate.orgId, orgId: projectTemplate.orgId,
actor.authMethod, actorAuthMethod: actor.authMethod,
actor.orgId actorOrgId: actor.orgId,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates); 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 { ProjectMembershipRole, ProjectType, TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; 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"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">; export type TProjectTemplateEnvironment = Pick<TProjectEnvironments, "name" | "slug" | "position">;
@@ -31,7 +31,7 @@ export enum InfisicalProjectTemplate {
export type TProjectTemplateServiceFactory = { export type TProjectTemplateServiceFactory = {
listProjectTemplatesByOrg: ( listProjectTemplatesByOrg: (
actor: OrgServiceActor, actor: ProjectServiceActor,
type?: ProjectType type?: ProjectType
) => Promise< ) => Promise<
( (
@@ -85,7 +85,7 @@ export type TProjectTemplateServiceFactory = {
>; >;
createProjectTemplate: ( createProjectTemplate: (
arg: TCreateProjectTemplateDTO, arg: TCreateProjectTemplateDTO,
actor: OrgServiceActor actor: ProjectServiceActor
) => Promise<{ ) => Promise<{
environments: TProjectTemplateEnvironment[]; environments: TProjectTemplateEnvironment[];
roles: { roles: {
@@ -109,7 +109,7 @@ export type TProjectTemplateServiceFactory = {
updateProjectTemplateById: ( updateProjectTemplateById: (
id: string, id: string,
{ roles, environments, ...params }: TUpdateProjectTemplateDTO, { roles, environments, ...params }: TUpdateProjectTemplateDTO,
actor: OrgServiceActor actor: ProjectServiceActor
) => Promise<{ ) => Promise<{
environments: TProjectTemplateEnvironment[]; environments: TProjectTemplateEnvironment[];
roles: { roles: {
@@ -132,7 +132,7 @@ export type TProjectTemplateServiceFactory = {
}>; }>;
deleteProjectTemplateById: ( deleteProjectTemplateById: (
id: string, id: string,
actor: OrgServiceActor actor: ProjectServiceActor
) => Promise<{ ) => Promise<{
environments: TProjectTemplateEnvironment[]; environments: TProjectTemplateEnvironment[];
roles: { roles: {
@@ -155,7 +155,7 @@ export type TProjectTemplateServiceFactory = {
}>; }>;
findProjectTemplateById: ( findProjectTemplateById: (
id: string, id: string,
actor: OrgServiceActor actor: ProjectServiceActor
) => Promise<{ ) => Promise<{
packedRoles: TProjectTemplateRole[]; packedRoles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[]; environments: TProjectTemplateEnvironment[];
@@ -179,7 +179,7 @@ export type TProjectTemplateServiceFactory = {
}>; }>;
findProjectTemplateByName: ( findProjectTemplateByName: (
name: string, name: string,
actor: OrgServiceActor actor: ProjectServiceActor
) => Promise<{ ) => Promise<{
packedRoles: TProjectTemplateRole[]; packedRoles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[]; environments: TProjectTemplateEnvironment[];
+25 -21
View File
@@ -3,7 +3,7 @@ import { isIP } from "node:net";
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; 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 { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@@ -126,8 +126,8 @@ export const relayServiceFactory = ({
// generate instance relay CA // generate instance relay CA
const instanceRelayCaSerialNumber = createSerialNumber(); const instanceRelayCaSerialNumber = createSerialNumber();
const instanceRelayCaIssuedAt = new Date();
const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045)); const instanceRelayCaExpiration = new Date(new Date().setFullYear(2045));
const instanceRelayCaIssuedAt = new Date();
const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const instanceRelayCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey); const instanceRelayCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceRelayCaKeys.privateKey);
const instanceRelayCaCert = await x509.X509CertificateGenerator.create({ const instanceRelayCaCert = await x509.X509CertificateGenerator.create({
@@ -972,13 +972,14 @@ export const relayServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
ActorType.IDENTITY, scope: OrganizationActionScope.Any,
identityId, actor: ActorType.IDENTITY,
actorId: identityId,
orgId, orgId,
actorAuthMethod!, actorAuthMethod: actorAuthMethod!,
orgId actorOrgId: orgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays, OrgPermissionRelayActions.CreateRelays,
@@ -1102,13 +1103,14 @@ export const relayServiceFactory = ({
}); });
} }
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
ActorType.IDENTITY, scope: OrganizationActionScope.Any,
identityId, actor: ActorType.IDENTITY,
actorId: identityId,
orgId, orgId,
actorAuthMethod!, actorAuthMethod: actorAuthMethod!,
orgId actorOrgId: orgId
); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays, OrgPermissionRelayActions.CreateRelays,
OrgPermissionSubjects.Relay OrgPermissionSubjects.Relay
@@ -1155,13 +1157,14 @@ export const relayServiceFactory = ({
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId: string; actorOrgId: string;
}) => { }) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod: actorAuthMethod!,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
@@ -1189,13 +1192,14 @@ export const relayServiceFactory = ({
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId: string; actorOrgId: string;
}) => { }) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
actorOrgId, orgId: actorOrgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
@@ -5,6 +5,7 @@ import RE2 from "re2";
import { import {
AccessScope, AccessScope,
OrganizationActionScope,
OrgMembershipRole, OrgMembershipRole,
OrgMembershipStatus, OrgMembershipStatus,
TableName, TableName,
@@ -251,7 +252,14 @@ export const samlConfigServiceFactory = ({
authProvider, authProvider,
enableGroupSync 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -317,7 +325,14 @@ export const samlConfigServiceFactory = ({
authProvider, authProvider,
enableGroupSync 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (!plan.samlSSO) if (!plan.samlSSO)
@@ -393,7 +408,7 @@ export const samlConfigServiceFactory = ({
}); });
} }
} else if (dto.type === "orgSlug") { } 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) { if (!org) {
throw new NotFoundError({ throw new NotFoundError({
message: `Organization with slug '${dto.orgSlug}' not found` 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 // when dto is type id means it's internally used
if (dto.type === "org") { if (dto.type === "org") {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
dto.actor, scope: OrganizationActionScope.ParentOrganization,
dto.actorId, actor: dto.actor,
samlConfig.orgId, actorId: dto.actorId,
dto.actorAuthMethod, orgId: samlConfig.orgId,
dto.actorOrgId actorAuthMethod: dto.actorAuthMethod,
); actorOrgId: dto.actorOrgId
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
} }
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
@@ -37,7 +37,7 @@ export type TGetSamlCfgDTO =
actor: ActorType; actor: ActorType;
actorId: string; actorId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined; actorOrgId: string;
} }
| { | {
type: "orgSlug"; type: "orgSlug";
+22 -5
View File
@@ -4,6 +4,7 @@ import { scimPatch } from "scim-patch";
import { import {
AccessScope, AccessScope,
OrganizationActionScope,
OrgMembershipRole, OrgMembershipRole,
OrgMembershipStatus, OrgMembershipStatus,
TableName, TableName,
@@ -56,6 +57,7 @@ type TScimServiceFactoryDep = {
TOrgDALFactory, TOrgDALFactory,
| "createMembership" | "createMembership"
| "findById" | "findById"
| "find"
| "findMembership" | "findMembership"
| "findMembershipWithScimFilter" | "findMembershipWithScimFilter"
| "deleteMembershipById" | "deleteMembershipById"
@@ -125,7 +127,14 @@ export const scimServiceFactory = ({
description, description,
ttlDays 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -160,7 +169,14 @@ export const scimServiceFactory = ({
actorAuthMethod, actorAuthMethod,
orgId 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
@@ -183,13 +199,14 @@ export const scimServiceFactory = ({
let scimToken = await scimDAL.findById(scimTokenId); let scimToken = await scimDAL.findById(scimTokenId);
if (!scimToken) throw new NotFoundError({ message: `SCIM token with ID '${scimTokenId}' not found` }); 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, actor,
actorId, actorId,
scimToken.orgId, orgId: scimToken.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
const plan = await licenseService.getPlan(scimToken.orgId); const plan = await licenseService.getPlan(scimToken.orgId);
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { WebhookEventMap } from "@octokit/webhooks-types"; import { WebhookEventMap } from "@octokit/webhooks-types";
import { ProbotOctokit } from "probot"; import { ProbotOctokit } from "probot";
import { OrganizationActionScope } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
@@ -49,7 +50,14 @@ export const secretScanningServiceFactory = ({
}: TInstallAppSessionDTO) => { }: TInstallAppSessionDTO) => {
const appCfg = getConfig(); 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const sessionId = crypto.randomBytes(16).toString("hex"); const sessionId = crypto.randomBytes(16).toString("hex");
@@ -68,13 +76,14 @@ export const secretScanningServiceFactory = ({
const session = await gitAppInstallSessionDAL.findOne({ sessionId }); const session = await gitAppInstallSessionDAL.findOne({ sessionId });
if (!session) throw new NotFoundError({ message: "Session was not found" }); if (!session) throw new NotFoundError({ message: "Session was not found" });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
scope: OrganizationActionScope.Any,
actor, actor,
actorId, actorId,
session.orgId, orgId: session.orgId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => { const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
await gitAppInstallSessionDAL.deleteById(session.id, tx); await gitAppInstallSessionDAL.deleteById(session.id, tx);
@@ -117,7 +126,14 @@ export const secretScanningServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TGetOrgInstallStatusDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const appInstallation = await gitAppOrgDAL.findOne({ orgId }); const appInstallation = await gitAppOrgDAL.findOne({ orgId });
@@ -125,7 +141,14 @@ export const secretScanningServiceFactory = ({
}; };
const getRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId, filter }: TGetOrgRisksDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const results = await secretScanningDAL.findByOrgId(orgId, filter); const results = await secretScanningDAL.findByOrgId(orgId, filter);
@@ -134,7 +157,14 @@ export const secretScanningServiceFactory = ({
}; };
const getAllRisksByOrg = async ({ actor, orgId, actorId, actorAuthMethod, actorOrgId }: TGetAllOrgRisksDTO) => { 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] }); const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
@@ -150,7 +180,14 @@ export const secretScanningServiceFactory = ({
riskId, riskId,
status status
}: TUpdateRiskStatusDTO) => { }: 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
const isRiskResolved = Boolean( 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;
};
+17
View File
@@ -33,6 +33,7 @@ export enum ApiDocsTags {
LdapAuth = "LDAP Auth", LdapAuth = "LDAP Auth",
Groups = "Groups", Groups = "Groups",
Organizations = "Organizations", Organizations = "Organizations",
SubOrganizations = "Sub Organizations",
Projects = "Projects", Projects = "Projects",
ProjectUsers = "Project Users", ProjectUsers = "Project Users",
ProjectGroups = "Project Groups", ProjectGroups = "Project Groups",
@@ -57,6 +58,7 @@ export enum ApiDocsTags {
PkiCertificateAuthorities = "PKI Certificate Authorities", PkiCertificateAuthorities = "PKI Certificate Authorities",
PkiCertificates = "PKI Certificates", PkiCertificates = "PKI Certificates",
PkiCertificateTemplates = "PKI Certificate Templates", PkiCertificateTemplates = "PKI Certificate Templates",
PkiCertificateProfiles = "PKI Certificate Profiles",
PkiCertificateCollections = "PKI Certificate Collections", PkiCertificateCollections = "PKI Certificate Collections",
PkiAlerting = "PKI Alerting", PkiAlerting = "PKI Alerting",
PkiSubscribers = "PKI Subscribers", PkiSubscribers = "PKI Subscribers",
@@ -716,6 +718,21 @@ export const ORGANIZATIONS = {
} }
} as const; } 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 = { export const PROJECTS = {
CREATE: { CREATE: {
organizationSlug: "The slug of the organization to create the project in.", organizationSlug: "The slug of the organization to create the project in.",
+10 -1
View File
@@ -5,7 +5,7 @@ export type TGenericPermission = {
actor: ActorType; actor: ActorType;
actorId: string; actorId: string;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined; actorOrgId: string;
}; };
/** /**
@@ -78,6 +78,15 @@ export type OrgServiceActor = {
id: string; id: string;
authMethod: ActorAuthMethod; authMethod: ActorAuthMethod;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
};
export type ProjectServiceActor = {
type: ActorType;
id: string;
authMethod: ActorAuthMethod;
orgId: string;
}; };
export enum QueueWorkerProfile { export enum QueueWorkerProfile {
@@ -8,6 +8,7 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; 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 { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; 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 tokenVersionId: string; // the session id of token used
user: TUsers; user: TUsers;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
authMethod: AuthMethod; authMethod: AuthMethod;
isMfaVerified?: boolean; isMfaVerified?: boolean;
token: AuthModeJwtTokenPayload; token: AuthModeJwtTokenPayload;
@@ -31,6 +34,8 @@ export type TAuthMode =
userId: string; userId: string;
user: TUsers; user: TUsers;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
token: string; token: string;
} }
| { | {
@@ -39,6 +44,8 @@ export type TAuthMode =
actor: ActorType.SERVICE; actor: ActorType.SERVICE;
serviceTokenId: string; serviceTokenId: string;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
token: string; token: string;
} }
@@ -48,6 +55,8 @@ export type TAuthMode =
identityId: string; identityId: string;
identityName: string; identityName: string;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
isInstanceAdmin?: boolean; isInstanceAdmin?: boolean;
token: TIdentityAccessTokenJwtPayload; token: TIdentityAccessTokenJwtPayload;
@@ -57,6 +66,8 @@ export type TAuthMode =
actor: ActorType.SCIM_CLIENT; actor: ActorType.SCIM_CLIENT;
scimTokenId: string; scimTokenId: string;
orgId: string; orgId: string;
rootOrgId: string;
parentOrgId: string;
authMethod: null; authMethod: null;
}; };
@@ -136,17 +147,26 @@ export const injectIdentity = fp(
if (!authMode) return; if (!authMode) return;
const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined;
if (subOrganizationSelector) {
await slugSchema().parseAsync(subOrganizationSelector);
}
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { 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); requestContext.set("orgId", orgId);
req.auth = { req.auth = {
authMode: AuthMode.JWT, authMode: AuthMode.JWT,
user, user,
userId: user.id, userId: user.id,
tokenVersionId, tokenVersionId,
actor, actor,
orgId: orgId as string, orgId,
rootOrgId,
parentOrgId,
authMethod: token.authMethod, authMethod: token.authMethod,
isMfaVerified: token.isMfaVerified, isMfaVerified: token.isMfaVerified,
token token
@@ -154,13 +174,19 @@ export const injectIdentity = fp(
break; break;
} }
case AuthMode.IDENTITY_ACCESS_TOKEN: { 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(); const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId); requestContext.set("orgId", identity.orgId);
req.auth = { req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor, actor,
orgId: identity.orgId, orgId: identity.orgId,
rootOrgId: identity.rootOrgId,
parentOrgId: identity.parentOrgId,
identityId: identity.identityId, identityId: identity.identityId,
identityName: identity.name, identityName: identity.name,
authMethod: null, authMethod: null,
@@ -190,8 +216,14 @@ export const injectIdentity = fp(
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
requestContext.set("orgId", serviceToken.orgId); requestContext.set("orgId", serviceToken.orgId);
if (subOrganizationSelector)
throw new BadRequestError({ message: `Service token doesn't support sub organization selector` });
req.auth = { req.auth = {
orgId: serviceToken.orgId, orgId: serviceToken.orgId,
rootOrgId: serviceToken.rootOrgId,
parentOrgId: serviceToken.parentOrgId,
authMode: AuthMode.SERVICE_TOKEN as const, authMode: AuthMode.SERVICE_TOKEN as const,
serviceToken, serviceToken,
serviceTokenId: serviceToken.id, serviceTokenId: serviceToken.id,
@@ -202,22 +234,27 @@ export const injectIdentity = fp(
break; break;
} }
case AuthMode.API_KEY: { case AuthMode.API_KEY: {
const user = await server.services.apiKey.fnValidateApiKey(token as string); throw new BadRequestError({
req.auth = { message: "API key authentication is not supported anymore. Please switch to identity authentication."
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;
} }
case AuthMode.SCIM_TOKEN: { case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
requestContext.set("orgId", orgId); 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; break;
} }
default: default:
@@ -14,7 +14,9 @@ export const injectPermission = fp(async (server) => {
type: ActorType.USER, type: ActorType.USER,
id: req.auth.userId, id: req.auth.userId,
orgId: req.auth.orgId, // if the req.auth.authMode is AuthMode.API_KEY, the orgId will be "API_KEY" 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( logger.info(
@@ -25,7 +27,9 @@ export const injectPermission = fp(async (server) => {
type: ActorType.IDENTITY, type: ActorType.IDENTITY,
id: req.auth.identityId, id: req.auth.identityId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
authMethod: null authMethod: null,
rootOrgId: req.auth.rootOrgId,
parentOrgId: req.auth.parentOrgId
}; };
logger.info( logger.info(
@@ -36,6 +40,8 @@ export const injectPermission = fp(async (server) => {
type: ActorType.SERVICE, type: ActorType.SERVICE,
id: req.auth.serviceTokenId, id: req.auth.serviceTokenId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
rootOrgId: req.auth.rootOrgId,
parentOrgId: req.auth.parentOrgId,
authMethod: null authMethod: null
}; };
@@ -47,6 +53,8 @@ export const injectPermission = fp(async (server) => {
type: ActorType.SCIM_CLIENT, type: ActorType.SCIM_CLIENT,
id: req.auth.scimTokenId, id: req.auth.scimTokenId,
orgId: req.auth.orgId, orgId: req.auth.orgId,
rootOrgId: req.auth.rootOrgId,
parentOrgId: req.auth.parentOrgId,
authMethod: null authMethod: null
}; };
+81 -4
View File
@@ -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 { 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 { 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 { 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 { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
import { keyValueStoreDALFactory } from "@app/keystore/key-value-store-dal"; 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 { internalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-dal";
import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns"; import { InternalCertificateAuthorityFns } from "@app/services/certificate-authority/internal/internal-certificate-authority-fns";
import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service"; import { internalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal"; import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal"; import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service"; import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
import { convertorServiceFactory } from "@app/services/convertor/convertor-service"; import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
import { apiEnrollmentConfigDALFactory } from "@app/services/enrollment-config/api-enrollment-config-dal";
import { estEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
@@ -560,11 +569,10 @@ export const registerRoutes = async (
orgDAL, orgDAL,
licenseDAL, licenseDAL,
keyStore, keyStore,
identityOrgMembershipDAL,
projectDAL projectDAL
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, membershipUserDAL, orgDAL });
const membershipUserService = membershipUserServiceFactory({ const membershipUserService = membershipUserServiceFactory({
licenseService, licenseService,
@@ -584,6 +592,7 @@ export const registerRoutes = async (
}); });
const membershipIdentityService = membershipIdentityServiceFactory({ const membershipIdentityService = membershipIdentityServiceFactory({
identityDAL,
membershipIdentityDAL, membershipIdentityDAL,
membershipRoleDAL, membershipRoleDAL,
orgDAL, orgDAL,
@@ -904,6 +913,15 @@ export const registerRoutes = async (
userGroupMembershipDAL, userGroupMembershipDAL,
additionalPrivilegeDAL additionalPrivilegeDAL
}); });
const subOrgService = subOrgServiceFactory({
licenseService,
membershipDAL,
membershipRoleDAL,
orgDAL,
permissionService
});
const signupService = authSignupServiceFactory({ const signupService = authSignupServiceFactory({
tokenService, tokenService,
smtpService, smtpService,
@@ -1036,6 +1054,10 @@ export const registerRoutes = async (
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db); const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db); const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
const certificateTemplateV2DAL = certificateTemplateV2DALFactory(db);
const certificateProfileDAL = certificateProfileDALFactory(db);
const apiEnrollmentConfigDAL = apiEnrollmentConfigDALFactory(db);
const estEnrollmentConfigDAL = estEnrollmentConfigDALFactory(db);
const certificateDAL = certificateDALFactory(db); const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
@@ -1120,6 +1142,21 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const certificateTemplateV2Service = certificateTemplateV2ServiceFactory({
certificateTemplateV2DAL,
permissionService
});
const certificateProfileService = certificateProfileServiceFactory({
certificateProfileDAL,
certificateTemplateV2DAL,
apiEnrollmentConfigDAL,
estEnrollmentConfigDAL,
permissionService,
kmsService,
projectDAL
});
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
pkiAlertDAL, pkiAlertDAL,
pkiCollectionDAL, pkiCollectionDAL,
@@ -1567,10 +1604,12 @@ export const registerRoutes = async (
permissionService, permissionService,
projectDAL, projectDAL,
accessTokenQueue, accessTokenQueue,
smtpService smtpService,
orgDAL
}); });
const identityService = identityServiceFactory({ const identityService = identityServiceFactory({
additionalPrivilegeDAL,
permissionService, permissionService,
identityDAL, identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
@@ -1601,10 +1640,12 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
accessTokenQueue, accessTokenQueue,
identityDAL, identityDAL,
membershipIdentityDAL membershipIdentityDAL,
orgDAL
}); });
const identityTokenAuthService = identityTokenAuthServiceFactory({ const identityTokenAuthService = identityTokenAuthServiceFactory({
identityDAL,
identityTokenAuthDAL, identityTokenAuthDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -1614,6 +1655,7 @@ export const registerRoutes = async (
}); });
const identityUaService = identityUaServiceFactory({ const identityUaService = identityUaServiceFactory({
identityDAL,
permissionService, permissionService,
identityAccessTokenDAL, identityAccessTokenDAL,
identityUaClientSecretDAL, identityUaClientSecretDAL,
@@ -1625,6 +1667,7 @@ export const registerRoutes = async (
}); });
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityDAL,
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -1638,6 +1681,7 @@ export const registerRoutes = async (
membershipIdentityDAL membershipIdentityDAL
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({ const identityGcpAuthService = identityGcpAuthServiceFactory({
identityDAL,
identityGcpAuthDAL, identityGcpAuthDAL,
orgDAL, orgDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
@@ -1647,6 +1691,7 @@ export const registerRoutes = async (
}); });
const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({ const identityAliCloudAuthService = identityAliCloudAuthServiceFactory({
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
orgDAL, orgDAL,
identityAliCloudAuthDAL, identityAliCloudAuthDAL,
@@ -1656,6 +1701,7 @@ export const registerRoutes = async (
}); });
const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({ const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityTlsCertAuthDAL, identityTlsCertAuthDAL,
licenseService, licenseService,
@@ -1665,6 +1711,7 @@ export const registerRoutes = async (
}); });
const identityAwsAuthService = identityAwsAuthServiceFactory({ const identityAwsAuthService = identityAwsAuthServiceFactory({
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
orgDAL, orgDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
@@ -1674,6 +1721,7 @@ export const registerRoutes = async (
}); });
const identityAzureAuthService = identityAzureAuthServiceFactory({ const identityAzureAuthService = identityAzureAuthServiceFactory({
identityDAL,
identityAzureAuthDAL, identityAzureAuthDAL,
orgDAL, orgDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
@@ -1683,6 +1731,7 @@ export const registerRoutes = async (
}); });
const identityOciAuthService = identityOciAuthServiceFactory({ const identityOciAuthService = identityOciAuthServiceFactory({
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
orgDAL, orgDAL,
identityOciAuthDAL, identityOciAuthDAL,
@@ -1706,6 +1755,7 @@ export const registerRoutes = async (
}); });
const identityOidcAuthService = identityOidcAuthServiceFactory({ const identityOidcAuthService = identityOidcAuthServiceFactory({
identityDAL,
identityOidcAuthDAL, identityOidcAuthDAL,
orgDAL, orgDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
@@ -1716,6 +1766,7 @@ export const registerRoutes = async (
}); });
const identityJwtAuthService = identityJwtAuthServiceFactory({ const identityJwtAuthService = identityJwtAuthServiceFactory({
identityDAL,
identityJwtAuthDAL, identityJwtAuthDAL,
orgDAL, orgDAL,
permissionService, permissionService,
@@ -2086,6 +2137,27 @@ export const registerRoutes = async (
pkiSyncQueue pkiSyncQueue
}); });
const certificateV3Service = certificateV3ServiceFactory({
certificateDAL,
certificateAuthorityDAL,
certificateProfileDAL,
certificateTemplateV2Service,
internalCaService: internalCertificateAuthorityService,
permissionService
});
const certificateEstV3Service = certificateEstV3ServiceFactory({
internalCertificateAuthorityService,
certificateTemplateV2Service,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService,
licenseService,
certificateProfileDAL,
estEnrollmentConfigDAL
});
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL, pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -2248,6 +2320,7 @@ export const registerRoutes = async (
groupProject: groupProjectService, groupProject: groupProjectService,
permission: permissionService, permission: permissionService,
org: orgService, org: orgService,
subOrganization: subOrgService,
oidc: oidcService, oidc: oidcService,
apiKey: apiKeyService, apiKey: apiKeyService,
authToken: tokenService, authToken: tokenService,
@@ -2296,6 +2369,8 @@ export const registerRoutes = async (
auditLog: auditLogService, auditLog: auditLogService,
auditLogStream: auditLogStreamService, auditLogStream: auditLogStreamService,
certificate: certificateService, certificate: certificateService,
certificateV3: certificateV3Service,
certificateEstV3: certificateEstV3Service,
sshCertificateAuthority: sshCertificateAuthorityService, sshCertificateAuthority: sshCertificateAuthorityService,
sshCertificateTemplate: sshCertificateTemplateService, sshCertificateTemplate: sshCertificateTemplateService,
sshHost: sshHostService, sshHost: sshHostService,
@@ -2303,6 +2378,8 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService, certificateAuthority: certificateAuthorityService,
internalCertificateAuthority: internalCertificateAuthorityService, internalCertificateAuthority: internalCertificateAuthorityService,
certificateTemplate: certificateTemplateService, certificateTemplate: certificateTemplateService,
certificateTemplateV2: certificateTemplateV2Service,
certificateProfile: certificateProfileService,
certificateAuthorityCrl: certificateAuthorityCrlService, certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService, certificateEst: certificateEstService,
pit: pitService, pit: pitService,
@@ -94,6 +94,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
decodedToken.userId, decodedToken.userId,
decodedToken.organizationId, decodedToken.organizationId,
decodedToken.authMethod, decodedToken.authMethod,
decodedToken.organizationId,
decodedToken.organizationId decodedToken.organizationId
); );
if (org && org.userTokenExpiration) { 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: { event: {
type: EventType.GET_CERTIFICATE_TEMPLATE, type: EventType.GET_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
} }
} }
}); });
@@ -121,7 +122,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
name: certificateTemplate.name, name: certificateTemplate.name,
commonName: certificateTemplate.commonName, commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName, subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl ttl: certificateTemplate.ttl,
projectId: certificateTemplate.projectId
} }
} }
}); });
@@ -184,9 +186,9 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
type: EventType.UPDATE_CERTIFICATE_TEMPLATE, type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id, certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name,
caId: certificateTemplate.caId, caId: certificateTemplate.caId,
pkiCollectionId: certificateTemplate.pkiCollectionId as string, pkiCollectionId: certificateTemplate.pkiCollectionId as string,
name: certificateTemplate.name,
commonName: certificateTemplate.commonName, commonName: certificateTemplate.commonName,
subjectAlternativeName: certificateTemplate.subjectAlternativeName, subjectAlternativeName: certificateTemplate.subjectAlternativeName,
ttl: certificateTemplate.ttl ttl: certificateTemplate.ttl
@@ -230,7 +232,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
event: { event: {
type: EventType.DELETE_CERTIFICATE_TEMPLATE, type: EventType.DELETE_CERTIFICATE_TEMPLATE,
metadata: { metadata: {
certificateTemplateId: certificateTemplate.id certificateTemplateId: certificateTemplate.id,
name: certificateTemplate.name
} }
} }
}); });
@@ -73,12 +73,12 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityAliCloudAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityAliCloudAuth, accessToken, identityAccessToken, identity } =
await server.services.identityAliCloudAuth.login(req.body); await server.services.identityAliCloudAuth.login(req.body);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH, type: EventType.LOGIN_IDENTITY_ALICLOUD_AUTH,
metadata: { metadata: {
@@ -40,12 +40,12 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityAwsAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityAwsAuth, accessToken, identityAccessToken, identity } =
await server.services.identityAwsAuth.login(req.body); await server.services.identityAwsAuth.login(req.body);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_AWS_AUTH, type: EventType.LOGIN_IDENTITY_AWS_AUTH,
metadata: { metadata: {
@@ -35,12 +35,12 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityAzureAuth, accessToken, identityAccessToken, identity } =
await server.services.identityAzureAuth.login(req.body); await server.services.identityAzureAuth.login(req.body);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_AZURE_AUTH, type: EventType.LOGIN_IDENTITY_AZURE_AUTH,
metadata: { metadata: {
@@ -35,12 +35,12 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityGcpAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityGcpAuth, accessToken, identityAccessToken, identity } =
await server.services.identityGcpAuth.login(req.body); await server.services.identityGcpAuth.login(req.body);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_GCP_AUTH, type: EventType.LOGIN_IDENTITY_GCP_AUTH,
metadata: { metadata: {
@@ -111,7 +111,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityJwtAuth, accessToken, identityAccessToken, identity } =
await server.services.identityJwtAuth.login({ await server.services.identityJwtAuth.login({
identityId: req.body.identityId, identityId: req.body.identityId,
jwt: req.body.jwt jwt: req.body.jwt
@@ -119,7 +119,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_JWT_AUTH, type: EventType.LOGIN_IDENTITY_JWT_AUTH,
metadata: { metadata: {
@@ -56,7 +56,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityKubernetesAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityKubernetesAuth, accessToken, identityAccessToken, identity } =
await server.services.identityKubernetesAuth.login({ await server.services.identityKubernetesAuth.login({
identityId: req.body.identityId, identityId: req.body.identityId,
jwt: req.body.jwt jwt: req.body.jwt
@@ -64,7 +64,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH, type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH,
metadata: { metadata: {
@@ -162,13 +162,13 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
const { identityId, user } = req.passportMachineIdentity; const { identityId, user } = req.passportMachineIdentity;
const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({ const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({
identityId identityId
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_LDAP_AUTH, type: EventType.LOGIN_IDENTITY_LDAP_AUTH,
metadata: { metadata: {
@@ -52,12 +52,12 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityOciAuth, accessToken, identityAccessToken, identity } =
await server.services.identityOciAuth.login(req.body); await server.services.identityOciAuth.login(req.body);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_OCI_AUTH, type: EventType.LOGIN_IDENTITY_OCI_AUTH,
metadata: { metadata: {
@@ -59,7 +59,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } = const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } =
await server.services.identityOidcAuth.login({ await server.services.identityOidcAuth.login({
identityId: req.body.identityId, identityId: req.body.identityId,
jwt: req.body.jwt jwt: req.body.jwt
@@ -67,7 +67,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_OIDC_AUTH, type: EventType.LOGIN_IDENTITY_OIDC_AUTH,
metadata: { 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, permissions: true,
description: true description: true
}).optional(), }).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()), authMethods: z.array(z.string()),
activeLockoutAuthMethods: z.array(z.string()) activeLockoutAuthMethods: z.array(z.string())
}) })
@@ -393,7 +393,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).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()) authMethods: z.array(z.string())
}) })
}).array(), }).array(),
@@ -64,7 +64,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
throw new BadRequestError({ message: "Missing TLS certificate in header" }); throw new BadRequestError({ message: "Missing TLS certificate in header" });
} }
const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityTlsCertAuth, accessToken, identityAccessToken, identity } =
await server.services.identityTlsCertAuth.login({ await server.services.identityTlsCertAuth.login({
identityId: req.body.identityId, identityId: req.body.identityId,
clientCertificate: clientCertificate as string clientCertificate: clientCertificate as string
@@ -72,7 +72,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH, type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH,
metadata: { metadata: {
@@ -319,7 +319,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityTokenAuth, accessToken, identityAccessToken, identityMembershipOrg } = const { identityTokenAuth, accessToken, identityAccessToken, identity } =
await server.services.identityTokenAuth.createTokenAuthToken({ await server.services.identityTokenAuth.createTokenAuthToken({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -332,7 +332,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH, type: EventType.CREATE_TOKEN_IDENTITY_TOKEN_AUTH,
metadata: { metadata: {
@@ -52,14 +52,14 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
accessToken, accessToken,
identityAccessToken, identityAccessToken,
validClientSecretInfo, validClientSecretInfo,
identityMembershipOrg, identity,
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL accessTokenMaxTTL
} = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
orgId: identityMembershipOrg.scopeOrgId, orgId: identity.orgId,
event: { event: {
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH,
metadata: { metadata: {
+4
View File
@@ -11,6 +11,7 @@ import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router"; import { registerProjectBotRouter } from "./bot-router";
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers"; import { CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP } from "./certificate-authority-routers";
import { registerCertificateProfilesRouter } from "./certificate-profiles-router";
import { registerCertRouter } from "./certificate-router"; import { registerCertRouter } from "./certificate-router";
import { registerCertificateTemplateRouter } from "./certificate-template-router"; import { registerCertificateTemplateRouter } from "./certificate-template-router";
import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router"; import { registerDeprecatedProjectEnvRouter } from "./deprecated-project-env-router";
@@ -32,6 +33,7 @@ import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-rou
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router"; import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router"; import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router"; import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerIdentityRouter } from "./identity-router"; import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-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(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" }); 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(registerCertRouter, { prefix: "/certificates" });
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" }); await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
await pkiRouter.register(registerCertificateProfilesRouter, { prefix: "/certificate-profiles" });
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
@@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { import {
AccessScope,
AuditLogsSchema, AuditLogsSchema,
GroupsSchema, GroupsSchema,
IncidentContactsSchema, IncidentContactsSchema,
@@ -59,7 +60,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 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.permission.id,
req.params.organizationId, req.params.organizationId,
req.permission.authMethod, req.permission.authMethod,
req.permission.rootOrgId,
req.permission.orgId req.permission.orgId
); );
return { organization }; return { organization };
@@ -467,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
return { groups }; 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, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, orgId: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}) })
+3
View File
@@ -1,4 +1,5 @@
import { registerCaRouter } from "./certificate-authority-router"; import { registerCaRouter } from "./certificate-authority-router";
import { registerCertificateTemplatesV2Router } from "./certificate-templates-v2-router";
import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router"; import { registerDeprecatedGroupProjectRouter } from "./deprecated-group-project-router";
import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router"; import { registerDeprecatedIdentityProjectRouter } from "./deprecated-identity-project-router";
import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router"; import { registerDeprecatedProjectMembershipRouter } from "./deprecated-project-membership-router";
@@ -19,6 +20,8 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerCertificateTemplatesV2Router, { prefix: "/certificate-templates" });
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaRouter, { prefix: "/ca" });
@@ -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;
}
});
};
+2
View File
@@ -1,3 +1,4 @@
import { registerCertificatesRouter } from "./certificates-router";
import { registerDeprecatedSecretRouter } from "./deprecated-secret-router"; import { registerDeprecatedSecretRouter } from "./deprecated-secret-router";
import { registerExternalMigrationRouter } from "./external-migration-router"; import { registerExternalMigrationRouter } from "./external-migration-router";
import { registerLoginRouter } from "./login-router"; import { registerLoginRouter } from "./login-router";
@@ -10,4 +11,5 @@ export const registerV3Routes = async (server: FastifyZodProvider) => {
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" }); await server.register(registerDeprecatedSecretRouter, { prefix: "/secrets" });
await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" }); await server.register(registerExternalMigrationRouter, { prefix: "/external-migration" });
await server.register(registerCertificatesRouter, { prefix: "/certificates" });
}; };
@@ -1,6 +1,6 @@
import { ForbiddenError, subject } from "@casl/ability"; 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 { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
@@ -215,13 +215,14 @@ export const appConnectionServiceFactory = ({
) )
); );
} else { } else {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
actor.orgId, orgId: actor.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, OrgPermissionAppConnectionActions.Read,
@@ -268,13 +269,14 @@ export const appConnectionServiceFactory = ({
subject(ProjectPermissionSub.AppConnections, { connectionId }) subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else { } else {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, OrgPermissionAppConnectionActions.Read,
@@ -318,13 +320,14 @@ export const appConnectionServiceFactory = ({
subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id }) subject(ProjectPermissionSub.AppConnections, { connectionId: appConnection.id })
); );
} else { } else {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, OrgPermissionAppConnectionActions.Read,
@@ -342,13 +345,14 @@ export const appConnectionServiceFactory = ({
{ method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO, { method, app, credentials, gatewayId, projectId, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
actor.orgId, orgId: actor.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
if (projectId) { if (projectId) {
const project = await projectDAL.findProjectById(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." "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections."
); );
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
if (appConnection.projectId) { if (appConnection.projectId) {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
@@ -635,13 +640,14 @@ export const appConnectionServiceFactory = ({
subject(ProjectPermissionSub.AppConnections, { connectionId }) subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else { } else {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Delete, OrgPermissionAppConnectionActions.Delete,
@@ -704,13 +710,14 @@ export const appConnectionServiceFactory = ({
subject(ProjectPermissionSub.AppConnections, { connectionId }) subject(ProjectPermissionSub.AppConnections, { connectionId })
); );
} else { } else {
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(orgPermission).throwUnlessCan( ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionAppConnectionActions.Connect, OrgPermissionAppConnectionActions.Connect,
@@ -747,13 +754,14 @@ export const appConnectionServiceFactory = ({
}; };
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => { const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor, projectId?: string) => {
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
actor.orgId, orgId: actor.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
let availableProjectConnections: TAppConnections[] = []; let availableProjectConnections: TAppConnections[] = [];
@@ -805,13 +813,14 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission({
actor.type, actorId: actor.id,
actor.id, actor: actor.type,
appConnection.orgId, orgId: appConnection.orgId,
actor.authMethod, actorOrgId: actor.orgId,
actor.orgId actorAuthMethod: actor.authMethod,
); scope: OrganizationActionScope.Any
});
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAppConnectionActions.Read, OrgPermissionAppConnectionActions.Read,
@@ -3,10 +3,11 @@ import { Knex } from "knex";
import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas"; import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type"; import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal"; import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal"; import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
@@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
type TAuthTokenServiceFactoryDep = { type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory; tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "transaction">; userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "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 createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
@@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
}; };
// to parse jwt identity in inject identity plugin // 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({ const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId, id: token.tokenVersionId,
userId: token.userId userId: token.userId
@@ -207,7 +209,35 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
const user = await userDAL.findById(session.userId); const user = await userDAL.findById(session.userId);
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
let orgId = "";
let rootOrgId = "";
let parentOrgId = "";
if (token.organizationId) { if (token.organizationId) {
if (subOrganizationSelector) {
const subOrganization = await orgDAL.findOne({
rootOrgId: token.organizationId,
slug: subOrganizationSelector
});
if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
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({ const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id, actorUserId: user.id,
scopeOrgId: token.organizationId, scopeOrgId: token.organizationId,
@@ -217,12 +247,18 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
if (!orgMembership) { if (!orgMembership) {
throw new ForbiddenRequestError({ message: "User not member of organization" }); throw new ForbiddenRequestError({ message: "User not member of organization" });
} }
if (!orgMembership.isActive) { if (!orgMembership.isActive) {
throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); 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 { return {
@@ -258,7 +258,13 @@ export const authSignupServiceFactory = ({
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
if (organizationId) { 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) { if (org && org.userTokenExpiration) {
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
refreshTokenExpiresIn = 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 * as x509 from "@peculiar/x509";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
@@ -68,6 +69,13 @@ export const parseDistinguishedName = (dn: string): TDNParts => {
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
switch (keyAlgorithm) { switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_3072:
return {
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength: 3072
};
case CertKeyAlgorithm.RSA_4096: case CertKeyAlgorithm.RSA_4096:
return { return {
name: "RSASSA-PKCS1-v1_5", name: "RSASSA-PKCS1-v1_5",
@@ -99,6 +107,73 @@ export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
} }
}; };
export const signatureAlgorithmToAlgCfg = (signatureAlgorithm: string, keyAlgorithm: CertKeyAlgorithm | string) => {
// Parse signature algorithm like "RSA-SHA256", "ECDSA-SHA256" etc.
if (!signatureAlgorithm || typeof signatureAlgorithm !== "string" || !signatureAlgorithm.includes("-")) {
throw new Error(`Invalid signature algorithm format: ${signatureAlgorithm}`);
}
const [keyType, hashType] = signatureAlgorithm.split("-");
if (!keyType || !hashType) {
throw new Error(`Malformed signature algorithm: ${signatureAlgorithm}`);
}
const normalizeHashType = (hash: string) => {
const upperHash = hash.toUpperCase();
if (upperHash === "SHA1" || upperHash === "SHA-1") return "SHA-1";
if (upperHash === "SHA224" || upperHash === "SHA-224") return "SHA-224";
if (upperHash === "SHA256" || upperHash === "SHA-256") return "SHA-256";
if (upperHash === "SHA384" || upperHash === "SHA-384") return "SHA-384";
if (upperHash === "SHA512" || upperHash === "SHA-512") return "SHA-512";
if (upperHash === "SHA3224" || upperHash === "SHA3-224") return "SHA3-224";
if (upperHash === "SHA3256" || upperHash === "SHA3-256") return "SHA3-256";
if (upperHash === "SHA3384" || upperHash === "SHA3-384") return "SHA3-384";
if (upperHash === "SHA3512" || upperHash === "SHA3-512") return "SHA3-512";
throw new Error(`Unsupported hash algorithm: ${hash}`);
};
const normalizedHash = hashType ? normalizeHashType(hashType) : undefined;
switch (keyType) {
case "RSA":
return {
name: "RSASSA-PKCS1-v1_5",
hash: normalizedHash || "SHA-256",
publicExponent: new Uint8Array([1, 0, 1]),
modulusLength:
keyAlgorithm === CertKeyAlgorithm.RSA_4096 ? 4096 : keyAlgorithm === CertKeyAlgorithm.RSA_3072 ? 3072 : 2048
};
case "ECDSA":
// eslint-disable-next-line no-case-declarations
const is384Curve =
keyAlgorithm === CertKeyAlgorithm.ECDSA_P384 || keyAlgorithm === "EC_secp384r1" || keyAlgorithm === "EC_P384";
// eslint-disable-next-line no-case-declarations
const is521Curve = keyAlgorithm === "EC_secp521r1" || keyAlgorithm === "EC_P521";
// eslint-disable-next-line no-case-declarations
let namedCurve: string;
if (is521Curve) {
namedCurve = "P-521";
} else if (is384Curve) {
namedCurve = "P-384";
} else {
namedCurve = "P-256";
}
return {
name: "ECDSA",
namedCurve,
hash: normalizedHash || (namedCurve === "P-384" ? "SHA-384" : "SHA-256")
};
default:
// Fallback to key algorithm default
return keyAlgorithmToAlgCfg(keyAlgorithm as CertKeyAlgorithm);
}
};
/** /**
* Return the public and private key of CA with id [caId] * Return the public and private key of CA with id [caId]
* Note: credentials are returned as crypto.webcrypto.CryptoKey * Note: credentials are returned as crypto.webcrypto.CryptoKey
@@ -111,7 +186,8 @@ export const getCaCredentials = async ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService kmsService,
signatureAlgorithm
}: TGetCaCredentialsDTO) => { }: TGetCaCredentialsDTO) => {
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId);
if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` }); if (!ca?.internalCa?.id) throw new NotFoundError({ message: `Internal CA with ID '${caId}' not found` });
@@ -132,7 +208,7 @@ export const getCaCredentials = async ({
cipherTextBlob: caSecret.encryptedPrivateKey cipherTextBlob: caSecret.encryptedPrivateKey
}); });
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const alg = signatureAlgorithm || keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const skObj = crypto.nativeCrypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" }); const skObj = crypto.nativeCrypto.createPrivateKey({ key: decryptedPrivateKey, format: "der", type: "pkcs8" });
const caPrivateKey = await crypto.nativeCrypto.subtle.importKey( const caPrivateKey = await crypto.nativeCrypto.subtle.importKey(
"pkcs8", "pkcs8",
@@ -18,7 +18,7 @@ export const BaseCertificateAuthoritySchema = CertificateAuthoritiesSchema.pick(
export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name), name: slugSchema({ field: "name" }).describe(CertificateAuthorities.CREATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.CREATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.CREATE(type).projectId),
enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().describe(CertificateAuthorities.CREATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status) status: z.nativeEnum(CaStatus).describe(CertificateAuthorities.CREATE(type).status)
}); });
@@ -26,7 +26,7 @@ export const GenericCreateCertificateAuthorityFieldsSchema = (type: CaType) =>
export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) => export const GenericUpdateCertificateAuthorityFieldsSchema = (type: CaType) =>
z.object({ z.object({
name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name), name: slugSchema({ field: "name" }).optional().describe(CertificateAuthorities.UPDATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(CertificateAuthorities.UPDATE(type).projectId), projectId: z.string().uuid("Project ID must be valid").describe(CertificateAuthorities.UPDATE(type).projectId),
enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance), enableDirectIssuance: z.boolean().optional().describe(CertificateAuthorities.UPDATE(type).enableDirectIssuance),
status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status) status: z.nativeEnum(CaStatus).optional().describe(CertificateAuthorities.UPDATE(type).status)
}); });
@@ -32,6 +32,8 @@ import {
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertSignatureAlgorithm,
CertSignatureType,
CertStatus, CertStatus,
TAltNameMapping TAltNameMapping
} from "../../certificate/certificate-types"; } from "../../certificate/certificate-types";
@@ -48,7 +50,8 @@ import {
getCaCertChains, getCaCertChains,
getCaCredentials, getCaCredentials,
keyAlgorithmToAlgCfg, keyAlgorithmToAlgCfg,
parseDistinguishedName parseDistinguishedName,
signatureAlgorithmToAlgCfg
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue"; import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
@@ -1174,7 +1177,10 @@ export const internalCertificateAuthorityServiceFactory = ({
actor, actor,
actorOrgId, actorOrgId,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
signatureAlgorithm,
keyAlgorithm,
isFromProfile
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO) => {
let ca: TCertificateAuthorityWithAssociatedCa | undefined; let ca: TCertificateAuthorityWithAssociatedCa | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
@@ -1221,7 +1227,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa.activeCaCertId) if (!ca.internalCa.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" }); throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (!ca.enableDirectIssuance && !certificateTemplate) { if (!isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" });
} }
@@ -1277,13 +1283,43 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
} }
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); const effectiveKeyAlgorithm =
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); (keyAlgorithm as CertKeyAlgorithm) || (ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const keyGenAlg = keyAlgorithmToAlgCfg(effectiveKeyAlgorithm);
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(keyGenAlg, true, ["sign", "verify"]);
if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
const requestedKeyType = signatureAlgorithm.split("-")[0];
const isRsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.RSA_2048.split("_")[0]);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertKeyAlgorithm.ECDSA_P256.split("_")[0]);
if (
(requestedKeyType === CertSignatureAlgorithm.RSA_SHA256.split("-")[0] && !isRsaCa) ||
(requestedKeyType === CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0] && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa
? CertSignatureAlgorithm.RSA_SHA256.split("-")[0]
: isEcdsaCa
? CertSignatureAlgorithm.ECDSA_SHA256.split("-")[0]
: "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
}
// Determine signing algorithm for certificate signing
const signingAlg = signatureAlgorithm
? signatureAlgorithmToAlgCfg(signatureAlgorithm, ca.internalCa.keyAlgorithm as CertKeyAlgorithm)
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${commonName}`, name: `CN=${commonName}`,
keys: leafKeys, keys: leafKeys,
signingAlgorithm: alg, signingAlgorithm: keyGenAlg,
extensions: [ extensions: [
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment) new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
@@ -1296,7 +1332,8 @@ export const internalCertificateAuthorityServiceFactory = ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
kmsService kmsService,
signatureAlgorithm: signingAlg
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
@@ -1319,7 +1356,7 @@ export const internalCertificateAuthorityServiceFactory = ({
// handle key usages // handle key usages
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? []; let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
if (keyUsages === undefined && !certificateTemplate) { if (keyUsages === undefined && !certificateTemplate) {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; selectedKeyUsages = isFromProfile ? [] : [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
} }
if (keyUsages === undefined && certificateTemplate) { if (keyUsages === undefined && certificateTemplate) {
@@ -1405,7 +1442,7 @@ export const internalCertificateAuthorityServiceFactory = ({
notAfter: notAfterDate, notAfter: notAfterDate,
signingKey: caPrivateKey, signingKey: caPrivateKey,
publicKey: csrObj.publicKey, publicKey: csrObj.publicKey,
signingAlgorithm: alg, signingAlgorithm: signingAlg,
extensions extensions
}); });
@@ -1517,7 +1554,9 @@ export const internalCertificateAuthorityServiceFactory = ({
notBefore, notBefore,
notAfter, notAfter,
keyUsages, keyUsages,
extendedKeyUsages extendedKeyUsages,
signatureAlgorithm,
keyAlgorithm
} = dto; } = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@@ -1563,7 +1602,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" }); if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
if (!ca.internalCa.activeCaCertId) if (!ca.internalCa.activeCaCertId)
throw new BadRequestError({ message: "CA does not have a certificate installed" }); throw new BadRequestError({ message: "CA does not have a certificate installed" });
if (!ca.enableDirectIssuance && !certificateTemplate) { if (!dto.isFromProfile && !ca.enableDirectIssuance && !certificateTemplate) {
throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" }); throw new BadRequestError({ message: "Certificate template or subscriber is required for issuance" });
} }
@@ -1622,7 +1661,29 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
} }
const alg = keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm); if (signatureAlgorithm) {
const caKeyAlgorithm = ca.internalCa.keyAlgorithm;
const requestedKeyType = signatureAlgorithm.split("-")[0]; // Get the first part (RSA, ECDSA)
const isRsaCa = caKeyAlgorithm.startsWith(CertSignatureType.RSA);
const isEcdsaCa = caKeyAlgorithm.startsWith(CertSignatureType.ECDSA);
if (
(requestedKeyType === CertSignatureType.RSA && !isRsaCa) ||
(requestedKeyType === CertSignatureType.ECDSA && !isEcdsaCa)
) {
// eslint-disable-next-line no-nested-ternary
const supportedType = isRsaCa ? CertSignatureType.RSA : isEcdsaCa ? CertSignatureType.ECDSA : "unknown";
throw new BadRequestError({
message: `Requested signature algorithm ${signatureAlgorithm} is not compatible with CA key algorithm ${caKeyAlgorithm}. CA can only sign with ${supportedType}-based signature algorithms.`
});
}
}
const effectiveKeyAlgorithm = (keyAlgorithm || ca.internalCa.keyAlgorithm) as CertKeyAlgorithm;
const alg = signatureAlgorithm
? signatureAlgorithmToAlgCfg(signatureAlgorithm, effectiveKeyAlgorithm)
: keyAlgorithmToAlgCfg(ca.internalCa.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr); const csrObj = new x509.Pkcs10CertificateRequest(csr);
@@ -1671,7 +1732,7 @@ export const internalCertificateAuthorityServiceFactory = ({
if (csrKeyUsageExtension) { if (csrKeyUsageExtension) {
selectedKeyUsages = csrKeyUsages; selectedKeyUsages = csrKeyUsages;
} else { } else {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]; selectedKeyUsages = dto.isFromProfile ? [] : [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
} }
} }
@@ -3,7 +3,12 @@ import { z } from "zod";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import {
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertSignatureAlgorithm
} from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -131,6 +136,9 @@ export type TIssueCertFromCaDTO = {
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: CertSignatureAlgorithm;
keyAlgorithm?: CertKeyAlgorithm;
isFromProfile?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = export type TSignCertFromCaDTO =
@@ -148,6 +156,9 @@ export type TSignCertFromCaDTO =
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: string;
keyAlgorithm?: string;
isFromProfile?: boolean;
} }
| ({ | ({
isInternal: false; isInternal: false;
@@ -163,6 +174,9 @@ export type TSignCertFromCaDTO =
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[]; keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
signatureAlgorithm?: string;
keyAlgorithm?: string;
isFromProfile?: boolean;
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = { export type TGetCaCertificateTemplatesDTO = {
@@ -184,6 +198,7 @@ export type TGetCaCredentialsDTO = {
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
signatureAlgorithm?: RsaHashedImportParams | EcKeyImportParams;
}; };
export type TGetCaCertChainsDTO = { export type TGetCaCertChainsDTO = {
@@ -0,0 +1,187 @@
export enum CertSubjectAlternativeNameType {
DNS_NAME = "dns_name",
IP_ADDRESS = "ip_address",
EMAIL = "email",
URI = "uri"
}
export enum CertKeyUsageType {
DIGITAL_SIGNATURE = "digital_signature",
KEY_ENCIPHERMENT = "key_encipherment",
NON_REPUDIATION = "non_repudiation",
DATA_ENCIPHERMENT = "data_encipherment",
KEY_AGREEMENT = "key_agreement",
KEY_CERT_SIGN = "key_cert_sign",
CRL_SIGN = "crl_sign",
ENCIPHER_ONLY = "encipher_only",
DECIPHER_ONLY = "decipher_only"
}
export enum CertExtendedKeyUsageType {
CLIENT_AUTH = "client_auth",
SERVER_AUTH = "server_auth",
CODE_SIGNING = "code_signing",
EMAIL_PROTECTION = "email_protection",
OCSP_SIGNING = "ocsp_signing",
TIME_STAMPING = "time_stamping"
}
export enum CertIncludeType {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
}
export enum CertAttributeRule {
ALLOW = "allow",
DENY = "deny"
}
export enum CertSanEffect {
ALLOW = "allow",
DENY = "deny",
REQUIRE = "require"
}
export enum CertDurationUnit {
DAYS = "days",
MONTHS = "months",
YEARS = "years"
}
export enum CertSubjectAttributeType {
COMMON_NAME = "common_name",
ORGANIZATION = "organization",
COUNTRY = "country"
}
export const mapKeyUsageToLegacy = (usage: CertKeyUsageType): string => {
switch (usage) {
case CertKeyUsageType.DIGITAL_SIGNATURE:
return "digitalSignature";
case CertKeyUsageType.KEY_ENCIPHERMENT:
return "keyEncipherment";
case CertKeyUsageType.NON_REPUDIATION:
return "nonRepudiation";
case CertKeyUsageType.DATA_ENCIPHERMENT:
return "dataEncipherment";
case CertKeyUsageType.KEY_AGREEMENT:
return "keyAgreement";
case CertKeyUsageType.KEY_CERT_SIGN:
return "keyCertSign";
case CertKeyUsageType.CRL_SIGN:
return "cRLSign";
case CertKeyUsageType.ENCIPHER_ONLY:
return "encipherOnly";
case CertKeyUsageType.DECIPHER_ONLY:
return "decipherOnly";
default:
return usage;
}
};
export const mapLegacyKeyUsageToStandard = (usage: string): CertKeyUsageType => {
switch (usage) {
case "digitalSignature":
case "digital_signature":
return CertKeyUsageType.DIGITAL_SIGNATURE;
case "keyEncipherment":
case "key_encipherment":
return CertKeyUsageType.KEY_ENCIPHERMENT;
case "nonRepudiation":
case "non_repudiation":
return CertKeyUsageType.NON_REPUDIATION;
case "dataEncipherment":
case "data_encipherment":
return CertKeyUsageType.DATA_ENCIPHERMENT;
case "keyAgreement":
case "key_agreement":
return CertKeyUsageType.KEY_AGREEMENT;
case "keyCertSign":
case "key_cert_sign":
return CertKeyUsageType.KEY_CERT_SIGN;
case "cRLSign":
case "crl_sign":
return CertKeyUsageType.CRL_SIGN;
case "encipherOnly":
case "encipher_only":
return CertKeyUsageType.ENCIPHER_ONLY;
case "decipherOnly":
case "decipher_only":
return CertKeyUsageType.DECIPHER_ONLY;
default:
throw new Error(`Unknown key usage: ${usage}`);
}
};
export const mapExtendedKeyUsageToLegacy = (usage: CertExtendedKeyUsageType): string => {
switch (usage) {
case CertExtendedKeyUsageType.CLIENT_AUTH:
return "clientAuth";
case CertExtendedKeyUsageType.SERVER_AUTH:
return "serverAuth";
case CertExtendedKeyUsageType.CODE_SIGNING:
return "codeSigning";
case CertExtendedKeyUsageType.EMAIL_PROTECTION:
return "emailProtection";
case CertExtendedKeyUsageType.OCSP_SIGNING:
return "ocspSigning";
case CertExtendedKeyUsageType.TIME_STAMPING:
return "timeStamping";
default:
return usage;
}
};
export const mapLegacyExtendedKeyUsageToStandard = (usage: string): CertExtendedKeyUsageType => {
switch (usage) {
case "clientAuth":
case "client_auth":
return CertExtendedKeyUsageType.CLIENT_AUTH;
case "serverAuth":
case "server_auth":
return CertExtendedKeyUsageType.SERVER_AUTH;
case "codeSigning":
case "code_signing":
return CertExtendedKeyUsageType.CODE_SIGNING;
case "emailProtection":
case "email_protection":
return CertExtendedKeyUsageType.EMAIL_PROTECTION;
case "ocspSigning":
case "ocsp_signing":
return CertExtendedKeyUsageType.OCSP_SIGNING;
case "timeStamping":
case "time_stamping":
return CertExtendedKeyUsageType.TIME_STAMPING;
default:
throw new Error(`Unknown extended key usage: ${usage}`);
}
};
export enum CertKeyAlgorithm {
RSA_2048 = "RSA_2048",
RSA_3072 = "RSA_3072",
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1"
}
export enum CertSignatureAlgorithm {
RSA_SHA256 = "RSA-SHA256",
RSA_SHA384 = "RSA-SHA384",
RSA_SHA512 = "RSA-SHA512",
ECDSA_SHA256 = "ECDSA-SHA256",
ECDSA_SHA384 = "ECDSA-SHA384",
ECDSA_SHA512 = "ECDSA-SHA512"
}
export const SAN_TYPE_OPTIONS = Object.values(CertSubjectAlternativeNameType);
export const KEY_USAGE_OPTIONS = Object.values(CertKeyUsageType);
export const EXTENDED_KEY_USAGE_OPTIONS = Object.values(CertExtendedKeyUsageType);
export const INCLUDE_TYPE_OPTIONS = Object.values(CertIncludeType);
export const DURATION_UNIT_OPTIONS = Object.values(CertDurationUnit);
export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttributeType);
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const KEY_ALGORITHM_OPTIONS = Object.values(CertKeyAlgorithm);
export const SIGNATURE_ALGORITHM_OPTIONS = Object.values(CertSignatureAlgorithm);
@@ -0,0 +1,198 @@
import RE2 from "re2";
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
import {
CertExtendedKeyUsageType,
CertKeyUsageType,
mapExtendedKeyUsageToLegacy,
mapKeyUsageToLegacy,
mapLegacyExtendedKeyUsageToStandard,
mapLegacyKeyUsageToStandard
} from "./certificate-constants";
interface CertificateRequestInput {
keyUsages?: string[];
extendedKeyUsages?: string[];
}
export const mapEnumsForValidation = <T extends CertificateRequestInput>(request: T): T => {
const mapKeyUsage = (usage: string): string => {
try {
return mapLegacyKeyUsageToStandard(usage);
} catch {
return usage;
}
};
const mapExtendedKeyUsage = (usage: string): string => {
try {
return mapLegacyExtendedKeyUsageToStandard(usage);
} catch {
return usage;
}
};
return {
...request,
keyUsages: request.keyUsages?.map(mapKeyUsage),
extendedKeyUsages: request.extendedKeyUsages?.map(mapExtendedKeyUsage)
} as T;
};
export const normalizeDateForApi = (date: Date | string | undefined): string | undefined => {
if (!date) return undefined;
return date instanceof Date ? date.toISOString() : date;
};
export const bufferToString = (data: Buffer | string): string => {
return String(data);
};
export const buildCertificateSubjectFromTemplate = (
request: Record<string, unknown>,
templateAttributes?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>
): Record<string, string | undefined> => {
const subject: Record<string, string> = {};
const attributeMap: Record<string, string> = {
common_name: "commonName",
organization: "organization",
country: "country"
};
if (!templateAttributes || templateAttributes.length === 0) {
return subject;
}
templateAttributes.forEach((attr) => {
const requestKey = attributeMap[attr.type];
const value = request[requestKey];
if (value && typeof value === "string" && (attr.allowed || attr.required)) {
subject[attr.type] = value;
}
});
return subject;
};
const isWildcardPattern = (value: string): boolean => {
return value.includes("*");
};
const createWildcardRegex = (pattern: string): RE2 => {
const escapeRegex = new RE2(/[.+?^${}()|[\]\\]/g);
const escaped = pattern.replace(escapeRegex, "\\$&");
const wildcardRegex = new RE2(/\*/g);
const regexPattern = escaped.replace(wildcardRegex, ".*");
return new RE2(`^${regexPattern}$`);
};
const validateValueAgainstPatterns = (value: string, patterns: string[]): boolean => {
if (!patterns || patterns.length === 0) {
return false;
}
for (const pattern of patterns) {
if (isWildcardPattern(pattern)) {
try {
const regex = createWildcardRegex(pattern);
if (regex.test(value)) {
return true;
}
} catch {
if (pattern === value) {
return true;
}
}
} else if (pattern === value) {
return true;
}
}
return false;
};
export const buildSubjectAlternativeNamesFromTemplate = (
request: { subjectAlternativeNames?: Array<{ type: string; value: string }> },
templateSans?: Array<{
type: string;
allowed?: string[];
required?: string[];
denied?: string[];
}>
): string => {
if (!request.subjectAlternativeNames || request.subjectAlternativeNames.length === 0) {
return "";
}
if (!templateSans || templateSans.length === 0) {
return request.subjectAlternativeNames.map((san) => san.value).join(",");
}
const allowedSans: string[] = [];
request.subjectAlternativeNames.forEach((san) => {
const templateSan = templateSans.find((template) => template.type === san.type);
if (!templateSan) {
allowedSans.push(san.value);
return;
}
if (templateSan.denied && validateValueAgainstPatterns(san.value, templateSan.denied)) {
throw new Error(`SAN value '${san.value}' is explicitly denied for type '${san.type}'`);
}
const isRequired = templateSan.required && validateValueAgainstPatterns(san.value, templateSan.required);
const isAllowed = templateSan.allowed && validateValueAgainstPatterns(san.value, templateSan.allowed);
if (isRequired || isAllowed || (!templateSan.allowed && !templateSan.required)) {
allowedSans.push(san.value);
} else {
throw new Error(`SAN value '${san.value}' is not allowed for type '${san.type}'`);
}
});
return allowedSans.join(",");
};
export const convertLegacyKeyUsage = (usage: CertKeyUsage): CertKeyUsageType => {
return mapLegacyKeyUsageToStandard(usage);
};
export const convertToLegacyKeyUsage = (usage: CertKeyUsageType): CertKeyUsage => {
return mapKeyUsageToLegacy(usage) as CertKeyUsage;
};
export const convertLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsage): CertExtendedKeyUsageType => {
return mapLegacyExtendedKeyUsageToStandard(usage);
};
export const convertToLegacyExtendedKeyUsage = (usage: CertExtendedKeyUsageType): CertExtendedKeyUsage => {
return mapExtendedKeyUsageToLegacy(usage) as CertExtendedKeyUsage;
};
export const convertKeyUsageArrayFromLegacy = (usages?: CertKeyUsage[]): CertKeyUsageType[] | undefined => {
return usages?.map(convertLegacyKeyUsage);
};
export const convertKeyUsageArrayToLegacy = (usages?: CertKeyUsageType[]): CertKeyUsage[] | undefined => {
return usages?.map(convertToLegacyKeyUsage);
};
export const convertExtendedKeyUsageArrayFromLegacy = (
usages?: CertExtendedKeyUsage[]
): CertExtendedKeyUsageType[] | undefined => {
return usages?.map(convertLegacyExtendedKeyUsage);
};
export const convertExtendedKeyUsageArrayToLegacy = (
usages?: CertExtendedKeyUsageType[]
): CertExtendedKeyUsage[] | undefined => {
return usages?.map(convertToLegacyExtendedKeyUsage);
};
@@ -0,0 +1,737 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-bitwise */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { EnrollmentType } from "../certificate-profile/certificate-profile-types";
import { certificateEstV3ServiceFactory, TCertificateEstV3ServiceFactory } from "./certificate-est-v3-service";
// Mock the x509 module
vi.mock("@peculiar/x509", () => ({
Pkcs10CertificateRequest: vi.fn(),
GeneralNames: vi.fn(),
KeyUsagesExtension: vi.fn(),
ExtendedKeyUsageExtension: vi.fn(),
X509Certificate: vi.fn(),
KeyUsageFlags: {
digitalSignature: 1,
nonRepudiation: 2,
keyEncipherment: 4,
dataEncipherment: 8,
keyAgreement: 16,
keyCertSign: 32,
cRLSign: 64,
encipherOnly: 128,
decipherOnly: 256
}
}));
// Mock other dependencies
vi.mock("@app/services/certificate-authority/certificate-authority-fns", () => ({
parseDistinguishedName: vi.fn((subject: string) => {
const parts = subject.split(",");
const result: any = {};
parts.forEach((part) => {
const [key, value] = part.split("=");
switch (key.trim()) {
case "CN":
result.commonName = value;
break;
case "O":
result.organization = value;
break;
case "OU":
result.ou = value;
break;
case "L":
result.locality = value;
break;
case "ST":
result.province = value;
break;
case "C":
result.country = value;
break;
default:
break;
}
});
return result;
})
}));
vi.mock("@app/services/certificate-authority/certificate-authority-validators", () => ({
validateAndMapAltNameType: vi.fn((value: string) => {
if (value.includes(".") && !value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return { type: "dns", value };
}
if (value.match(/^\d+\.\d+\.\d+\.\d+$/)) {
return { type: "ip", value };
}
return null;
})
}));
vi.mock("@app/services/certificate-common/certificate-constants", () => ({
mapLegacyKeyUsageToStandard: vi.fn((usage: string) => {
const mapping: Record<string, string> = {
digitalSignature: "digital_signature",
keyEncipherment: "key_encipherment",
keyCertSign: "key_cert_sign"
};
return mapping[usage] || usage;
}),
mapLegacyExtendedKeyUsageToStandard: vi.fn((usage: string) => {
const mapping: Record<string, string> = {
clientAuth: "client_auth",
serverAuth: "server_auth",
codeSigning: "code_signing"
};
return mapping[usage] || usage;
}),
CertKeyUsageType: {
DIGITAL_SIGNATURE: "digital_signature",
KEY_ENCIPHERMENT: "key_encipherment",
KEY_CERT_SIGN: "key_cert_sign"
},
CertExtendedKeyUsageType: {
CLIENT_AUTH: "client_auth",
SERVER_AUTH: "server_auth",
CODE_SIGNING: "code_signing"
},
CertSubjectAlternativeNameType: {
DNS_NAME: "dns_name",
IP_ADDRESS: "ip_address",
RFC822_NAME: "rfc822_name",
UNIFORM_RESOURCE_IDENTIFIER: "uniform_resource_identifier"
}
}));
vi.mock("@app/services/certificate/certificate-types", () => ({
mapLegacyAltNameType: vi.fn((type: string) => {
const mapping: Record<string, string> = {
dns: "dns_name",
ip: "ip_address",
email: "rfc822_name",
url: "uniform_resource_identifier"
};
return mapping[type] || type;
}),
TAltNameType: {
EMAIL: "email",
DNS: "dns",
IP: "ip",
URL: "url"
},
CertExtendedKeyUsageOIDToName: {
"1.3.6.1.5.5.7.3.1": "serverAuth",
"1.3.6.1.5.5.7.3.2": "clientAuth",
"1.3.6.1.5.5.7.3.3": "codeSigning"
},
CertKeyUsage: {
DIGITAL_SIGNATURE: "digitalSignature",
KEY_ENCIPHERMENT: "keyEncipherment",
KEY_CERT_SIGN: "keyCertSign",
NON_REPUDIATION: "nonRepudiation",
DATA_ENCIPHERMENT: "dataEncipherment",
KEY_AGREEMENT: "keyAgreement",
CRL_SIGN: "cRLSign",
ENCIPHER_ONLY: "encipherOnly",
DECIPHER_ONLY: "decipherOnly"
},
CertExtendedKeyUsage: {
CLIENT_AUTH: "clientAuth",
SERVER_AUTH: "serverAuth",
CODE_SIGNING: "codeSigning"
}
}));
vi.mock("@app/services/certificate-common/certificate-utils", () => ({
mapEnumsForValidation: vi.fn((req: any) => req)
}));
vi.mock("../../ee/services/certificate-est/certificate-est-fns", () => ({
convertRawCertsToPkcs7: vi.fn(() => "mocked-pkcs7-response")
}));
describe("CertificateEstV3Service Security Fix", () => {
let service: TCertificateEstV3ServiceFactory;
const mockInternalCertificateAuthorityService = {
signCertFromCa: vi.fn()
};
const mockCertificateTemplateV2Service = {
validateCertificateRequest: vi.fn()
};
const mockCertificateAuthorityDAL = {
findById: vi.fn(),
findByIdWithAssociatedCa: vi.fn()
};
const mockCertificateAuthorityCertDAL = {
find: vi.fn(),
findById: vi.fn()
};
const mockProjectDAL = {
findOne: vi.fn(),
updateById: vi.fn(),
transaction: vi.fn()
};
const mockKmsService = {
decryptWithKmsKey: vi.fn(),
generateKmsKey: vi.fn()
};
const mockLicenseService = {
getPlan: vi.fn()
};
const mockCertificateProfileDAL = {
findByIdWithConfigs: vi.fn()
};
const mockEstEnrollmentConfigDAL = {
findById: vi.fn()
};
const mockProfile = {
id: "profile-123",
projectId: "project-123",
caId: "ca-123",
certificateTemplateId: "template-v2-123",
enrollmentType: EnrollmentType.EST,
estConfigId: "est-config-123"
};
const mockEstConfig = {
id: "est-config-123",
disableBootstrapCaValidation: true
};
const mockProject = {
id: "project-123",
orgId: "org-123"
};
const mockPlan = {
pkiEst: true
};
beforeEach(async () => {
const { Pkcs10CertificateRequest, GeneralNames } = await import("@peculiar/x509");
service = certificateEstV3ServiceFactory({
internalCertificateAuthorityService: mockInternalCertificateAuthorityService,
certificateTemplateV2Service: mockCertificateTemplateV2Service,
certificateAuthorityDAL: mockCertificateAuthorityDAL,
certificateAuthorityCertDAL: mockCertificateAuthorityCertDAL,
projectDAL: mockProjectDAL,
kmsService: mockKmsService,
licenseService: mockLicenseService,
certificateProfileDAL: mockCertificateProfileDAL,
estEnrollmentConfigDAL: mockEstEnrollmentConfigDAL
});
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(mockProfile);
mockEstEnrollmentConfigDAL.findById.mockResolvedValue(mockEstConfig);
mockProjectDAL.findOne.mockResolvedValue(mockProject);
mockLicenseService.getPlan.mockResolvedValue(mockPlan);
// Set up the default CSR parsing behavior
(Pkcs10CertificateRequest as any).mockImplementation((csr: string) => {
const parsed = JSON.parse(csr);
const mockExtensions: any[] = [];
if (parsed.sans && parsed.sans.length > 0) {
mockExtensions.push({ type: "2.5.29.17", value: "mock-san-value" });
}
return {
subject: parsed.subject,
extensions: mockExtensions,
getExtension: vi.fn((oid: string) => {
if (oid === "2.5.29.15" && parsed.keyUsages && parsed.keyUsages.length > 0) {
// Calculate usages as bitwise OR of key usage flags
let usages = 0;
parsed.keyUsages.forEach((usage: string) => {
switch (usage) {
case "digital_signature":
usages |= 1; // KeyUsageFlags.digitalSignature
break;
case "key_encipherment":
usages |= 4; // KeyUsageFlags.keyEncipherment
break;
case "key_cert_sign":
usages |= 32; // KeyUsageFlags.keyCertSign
break;
default:
break;
}
});
return { usages };
}
if (oid === "2.5.29.37" && parsed.extendedKeyUsages && parsed.extendedKeyUsages.length > 0) {
const ekuOids = parsed.extendedKeyUsages.map((eku: string) => {
switch (eku) {
case "client_auth":
return "1.3.6.1.5.5.7.3.2";
case "server_auth":
return "1.3.6.1.5.5.7.3.1";
case "code_signing":
return "1.3.6.1.5.5.7.3.3";
default:
return "1.3.6.1.5.5.7.3.1";
}
});
return { usages: ekuOids };
}
return undefined;
})
};
});
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
}));
});
afterEach(() => {
vi.clearAllMocks();
});
const createMockCSR = (
options: {
subject?: string;
keyUsages?: string[];
extendedKeyUsages?: string[];
sans?: Array<{ type: string; value: string }>;
} = {}
) => {
const {
subject = "CN=test.example.com,O=Test Org,C=US",
keyUsages = [],
extendedKeyUsages = [],
sans = []
} = options;
return JSON.stringify({
subject,
keyUsages,
extendedKeyUsages,
sans
});
};
describe("CSR Extraction and Template Validation", () => {
it("should extract subject attributes from CSR", async () => {
const csr = createMockCSR({
subject: "CN=test.example.com,O=Test Organization,OU=IT Department,L=San Francisco,ST=California,C=US"
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
commonName: "test.example.com",
organization: "Test Organization",
organizationUnit: "IT Department",
locality: "San Francisco",
state: "California",
country: "US"
})
);
});
it("should extract key usages from CSR", async () => {
const csr = createMockCSR({
keyUsages: ["digital_signature", "key_encipherment"]
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
keyUsages: expect.arrayContaining(["digital_signature", "key_encipherment"])
})
);
});
it("should extract extended key usages from CSR", async () => {
const csr = createMockCSR({
extendedKeyUsages: ["client_auth", "server_auth"]
});
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
extendedKeyUsages: expect.arrayContaining(["client_auth", "server_auth"])
})
);
});
it("should extract Subject Alternative Names from CSR", async () => {
const { GeneralNames } = await import("@peculiar/x509");
const csr = createMockCSR({
sans: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
});
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "test.example.com" },
{ type: "ip", value: "192.168.1.1" }
]
}));
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
subjectAlternativeNames: expect.arrayContaining([
expect.objectContaining({
type: "dns_name",
value: "test.example.com"
}),
expect.objectContaining({
type: "ip_address",
value: "192.168.1.1"
})
])
})
);
});
});
describe("Template Validation Enforcement", () => {
const basicCSR = createMockCSR();
it("should enforce template validation and reject invalid requests", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Common name 'test.example.com' is not allowed", "Key usage 'digital_signature' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
});
it("should allow valid requests that pass template validation", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.any(Object)
);
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
isInternal: true,
caId: "ca-123",
csr: basicCSR,
isFromProfile: true
});
});
it("should validate template for both simpleEnrollByProfile and simpleReenrollByProfile", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["SAN value 'evil.com' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalled();
expect(mockInternalCertificateAuthorityService.signCertFromCa).not.toHaveBeenCalled();
});
});
describe("Policy Bypass Prevention", () => {
const maliciousCSR = createMockCSR({
subject: "CN=evil.com,O=Evil Corp,C=XX",
keyUsages: ["key_cert_sign"],
sans: [
{ type: "dns", value: "*.example.com" },
{ type: "ip", value: "127.0.0.1" }
]
});
it("should block attempts to bypass subject attribute policies", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Organization 'Evil Corp' is denied", "Country 'XX' is not allowed"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
commonName: "evil.com",
organization: "Evil Corp",
country: "XX"
})
);
});
it("should block attempts to bypass key usage policies", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["Key usage 'key_cert_sign' is denied - certificate authority privileges not allowed"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
expect(mockCertificateTemplateV2Service.validateCertificateRequest).toHaveBeenCalledWith(
"template-v2-123",
expect.objectContaining({
keyUsages: expect.arrayContaining(["key_cert_sign"])
})
);
});
it("should block attempts to bypass SAN policies", async () => {
const { GeneralNames } = await import("@peculiar/x509");
(GeneralNames as any).mockImplementation(() => ({
items: [
{ type: "dns", value: "*.example.com" },
{ type: "ip", value: "127.0.0.1" }
]
}));
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: false,
errors: ["SAN value '*.example.com' matches denied wildcard pattern", "SAN value '127.0.0.1' is denied"],
warnings: []
});
await expect(
service.simpleEnrollByProfile({
csr: maliciousCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
});
});
describe("Error Handling", () => {
const basicCSR = createMockCSR();
it("should handle profile not found", async () => {
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue(null);
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "nonexistent",
sslClientCert: ""
})
).rejects.toThrow(NotFoundError);
});
it("should handle non-EST enrollment type", async () => {
mockCertificateProfileDAL.findByIdWithConfigs.mockResolvedValue({
...mockProfile,
enrollmentType: EnrollmentType.API
});
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow(BadRequestError);
});
it("should handle template validation service errors", async () => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockRejectedValue(
new Error("Template validation service unavailable")
);
await expect(
service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
})
).rejects.toThrow("Template validation service unavailable");
});
});
describe("Integration with existing flow", () => {
const basicCSR = createMockCSR();
beforeEach(() => {
mockCertificateTemplateV2Service.validateCertificateRequest.mockResolvedValue({
isValid: true,
errors: [],
warnings: []
});
});
it("should call internal CA service with correct parameters after validation", async () => {
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith({
isInternal: true,
caId: "ca-123",
isFromProfile: true,
csr: basicCSR
});
});
it("should use profile's CA ID instead of template ID to avoid v1/v2 mismatch", async () => {
mockInternalCertificateAuthorityService.signCertFromCa.mockResolvedValue({
certificate: { rawData: new ArrayBuffer(0) }
});
await service.simpleEnrollByProfile({
csr: basicCSR,
profileId: "profile-123",
sslClientCert: ""
});
// Verify it uses caId from profile, not certificateTemplateId
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
expect.objectContaining({
caId: "ca-123"
})
);
// Verify it does NOT pass certificateTemplateId to avoid v1/v2 confusion
expect(mockInternalCertificateAuthorityService.signCertFromCa).toHaveBeenCalledWith(
expect.not.objectContaining({
certificateTemplateId: expect.anything()
})
);
});
});
});
@@ -0,0 +1,408 @@
import * as x509 from "@peculiar/x509";
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import {
CertExtendedKeyUsageOIDToName,
CertKeyUsage,
mapLegacyAltNameType,
TAltNameMapping,
TAltNameType
} from "@app/services/certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import {
getCaCertChain,
getCaCertChains,
parseDistinguishedName
} from "@app/services/certificate-authority/certificate-authority-fns";
import { validateAndMapAltNameType } from "@app/services/certificate-authority/certificate-authority-validators";
import { TInternalCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/internal/internal-certificate-authority-service";
import {
mapLegacyExtendedKeyUsageToStandard,
mapLegacyKeyUsageToStandard
} from "@app/services/certificate-common/certificate-constants";
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { EnrollmentType } from "@app/services/certificate-profile/certificate-profile-types";
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
import { TCertificateRequest } from "@app/services/certificate-template-v2/certificate-template-v2-types";
import { TEstEnrollmentConfigDALFactory } from "@app/services/enrollment-config/est-enrollment-config-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { convertRawCertsToPkcs7 } from "../../ee/services/certificate-est/certificate-est-fns";
import { TLicenseServiceFactory } from "../../ee/services/license/license-service";
type TCertificateEstV3ServiceFactoryDep = {
internalCertificateAuthorityService: Pick<TInternalCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById" | "findByIdWithAssociatedCa">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithConfigs">;
estEnrollmentConfigDAL: Pick<TEstEnrollmentConfigDALFactory, "findById">;
};
export type TCertificateEstV3ServiceFactory = ReturnType<typeof certificateEstV3ServiceFactory>;
export const certificateEstV3ServiceFactory = ({
internalCertificateAuthorityService,
certificateTemplateV2Service,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService,
certificateProfileDAL,
estEnrollmentConfigDAL
}: TCertificateEstV3ServiceFactoryDep) => {
const extractCertificateRequestFromCSR = (csr: string): TCertificateRequest => {
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const subject = parseDistinguishedName(csrObj.subject);
const certificateRequest: TCertificateRequest = {
commonName: subject.commonName,
organization: subject.organization,
organizationUnit: subject.ou,
locality: subject.locality,
state: subject.province,
country: subject.country
};
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
if (csrKeyUsageExtension) {
const csrKeyUsages = Object.values(CertKeyUsage).filter(
// eslint-disable-next-line no-bitwise
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
);
certificateRequest.keyUsages = csrKeyUsages.map(mapLegacyKeyUsageToStandard);
}
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
if (csrExtendedKeyUsageExtension) {
const csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
);
certificateRequest.extendedKeyUsages = csrExtendedKeyUsages.map(mapLegacyExtendedKeyUsageToStandard);
}
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (sanExtension) {
const sanNames = new x509.GeneralNames(sanExtension.value);
const altNamesArray: TAltNameMapping[] = sanNames.items
.filter(
(value) =>
value.type === TAltNameType.EMAIL ||
value.type === TAltNameType.DNS ||
value.type === TAltNameType.IP ||
value.type === TAltNameType.URL
)
.map((name): TAltNameMapping => {
const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new BadRequestError({ message: `Invalid altName from CSR: ${name.value}` });
}
return altNameType;
});
certificateRequest.subjectAlternativeNames = altNamesArray.map((altName) => ({
type: mapLegacyAltNameType(altName.type),
value: altName.value
}));
}
return certificateRequest;
};
const simpleEnrollByProfile = async ({
csr,
profileId,
sslClientCert
}: {
csr: string;
profileId: string;
sslClientCert: string;
}) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.disableBootstrapCaValidation) {
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: profile.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaChain = estConfig.encryptedCaChain
? (
await kmsDecryptor({
cipherTextBlob: estConfig.encryptedCaChain
})
).toString()
: "";
const caCerts = extractX509CertFromChain(decryptedCaChain)?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
}
const certificateRequest = extractCertificateRequestFromCSR(csr);
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
});
}
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
caId: profile.caId,
csr,
isFromProfile: true
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleReenrollByProfile = async ({
csr,
profileId,
sslClientCert
}: {
csr: string;
profileId: string;
sslClientCert: string;
}) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
const caCertChains = await getCaCertChains({
caId: profile.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const certificateRequest = extractCertificateRequestFromCSR(csr);
const mappedCertificateRequest = mapEnumsForValidation(certificateRequest);
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
profile.certificateTemplateId,
mappedCertificateRequest
);
if (!validationResult.isValid) {
throw new BadRequestError({
message: `Certificate request validation failed: ${validationResult.errors.join(", ")}`
});
}
const { certificate } = await internalCertificateAuthorityService.signCertFromCa({
isInternal: true,
caId: profile.caId,
csr,
isFromProfile: true
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const getCaCertsByProfile = async ({ profileId }: { profileId: string }) => {
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
if (!profile) {
throw new NotFoundError({ message: "Certificate profile not found" });
}
if (profile.enrollmentType !== EnrollmentType.EST) {
throw new BadRequestError({ message: "Profile is not configured for EST enrollment" });
}
if (!profile.estConfigId) {
throw new BadRequestError({ message: "EST enrollment not configured for this profile" });
}
const estConfig = await estEnrollmentConfigDAL.findById(profile.estConfigId);
if (!estConfig) {
throw new NotFoundError({ message: "EST configuration not found" });
}
const project = await projectDAL.findOne({ id: profile.projectId });
if (!project) {
throw new NotFoundError({ message: "Project not found" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan."
});
}
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(profile.caId);
if (!ca?.internalCa?.id) {
throw new NotFoundError({
message: `Internal Certificate Authority with ID '${profile.caId}' not found`
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.internalCa.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
let certificates: x509.X509Certificate[] = [];
if (caCertChain && caCertChain.trim()) {
try {
certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
} catch (error) {
certificates = [];
}
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnrollByProfile,
simpleReenrollByProfile,
getCaCertsByProfile
};
};
@@ -0,0 +1,552 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import {
EnrollmentType,
TCertificateProfile,
TCertificateProfileCertificate,
TCertificateProfileInsert,
TCertificateProfileMetrics,
TCertificateProfileUpdate,
TCertificateProfileWithConfigs,
TCertificateProfileWithRawMetrics
} from "./certificate-profile-types";
export type TCertificateProfileDALFactory = ReturnType<typeof certificateProfileDALFactory>;
export const certificateProfileDALFactory = (db: TDbClient) => {
const certificateProfileOrm = ormify(db, TableName.PkiCertificateProfile);
const create = async (data: TCertificateProfileInsert, tx?: Knex): Promise<TCertificateProfile> => {
try {
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile).insert(data).returning("*")) as [
TCertificateProfile
];
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Create certificate profile" });
}
};
const updateById = async (id: string, data: TCertificateProfileUpdate, tx?: Knex): Promise<TCertificateProfile> => {
try {
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ id })
.update(data)
.returning("*")) as [TCertificateProfile];
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Update certificate profile" });
}
};
const deleteById = async (id: string, tx?: Knex): Promise<TCertificateProfile> => {
try {
const [certificateProfile] = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ id })
.del()
.returning("*")) as [TCertificateProfile];
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Delete certificate profile" });
}
};
const findById = async (id: string, tx?: Knex): Promise<TCertificateProfile | undefined> => {
try {
const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile).where({ id }).first()) as
| TCertificateProfile
| undefined;
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by id" });
}
};
const findByIdWithConfigs = async (id: string, tx?: Knex): Promise<TCertificateProfileWithConfigs | undefined> => {
try {
const query = (tx || db)(TableName.PkiCertificateProfile)
.leftJoin(
TableName.CertificateAuthority,
`${TableName.PkiCertificateProfile}.caId`,
`${TableName.CertificateAuthority}.id`
)
.leftJoin(
TableName.PkiCertificateTemplateV2,
`${TableName.PkiCertificateProfile}.certificateTemplateId`,
`${TableName.PkiCertificateTemplateV2}.id`
)
.leftJoin(
TableName.PkiEstEnrollmentConfig,
`${TableName.PkiCertificateProfile}.estConfigId`,
`${TableName.PkiEstEnrollmentConfig}.id`
)
.leftJoin(
TableName.PkiApiEnrollmentConfig,
`${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id`
)
.select(selectAllTableCols(TableName.PkiCertificateProfile))
.select(
db.ref("id").withSchema(TableName.CertificateAuthority).as("caId"),
db.ref("projectId").withSchema(TableName.CertificateAuthority).as("caProjectId"),
db.ref("status").withSchema(TableName.CertificateAuthority).as("caStatus"),
db.ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
db.ref("id").withSchema(TableName.PkiCertificateTemplateV2).as("templateId"),
db.ref("projectId").withSchema(TableName.PkiCertificateTemplateV2).as("templateProjectId"),
db.ref("name").withSchema(TableName.PkiCertificateTemplateV2).as("templateName"),
db.ref("description").withSchema(TableName.PkiCertificateTemplateV2).as("templateDescription"),
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estConfigDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
)
.where(`${TableName.PkiCertificateProfile}.id`, id)
.first();
const result = await query;
if (!result) return undefined;
const estConfig =
result.estConfigId && result.estConfigHashedPassphrase
? ({
id: result.estConfigId,
disableBootstrapCaValidation: !!result.estConfigDisableBootstrapCaValidation,
passphrase: result.estConfigHashedPassphrase,
caChain: result.estConfigEncryptedCaChain ? result.estConfigEncryptedCaChain.toString("utf8") : ""
} as TCertificateProfileWithConfigs["estConfig"])
: undefined;
const apiConfig = result.apiConfigId
? ({
id: result.apiConfigId,
autoRenew: !!result.apiConfigAutoRenew,
autoRenewDays: result.apiConfigAutoRenewDays || undefined
} as TCertificateProfileWithConfigs["apiConfig"])
: undefined;
const certificateAuthority =
result.caId && result.caProjectId && result.caStatus && result.caName
? ({
id: result.caId,
projectId: result.caProjectId,
status: result.caStatus,
name: result.caName
} as TCertificateProfileWithConfigs["certificateAuthority"])
: undefined;
const certificateTemplate =
result.templateId && result.templateProjectId && result.templateName
? ({
id: result.templateId,
projectId: result.templateProjectId,
name: result.templateName,
description: result.templateDescription || undefined
} as TCertificateProfileWithConfigs["certificateTemplate"])
: undefined;
const transformedResult: TCertificateProfileWithConfigs = {
id: result.id,
projectId: result.projectId,
caId: result.caId,
certificateTemplateId: result.certificateTemplateId,
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
estConfig,
apiConfig,
certificateAuthority,
certificateTemplate
};
return transformedResult;
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by id with configs" });
}
};
const findBySlugAndProjectId = async (
slug: string,
projectId: string,
tx?: Knex
): Promise<TCertificateProfile | undefined> => {
try {
const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ slug, projectId })
.first()) as TCertificateProfile | undefined;
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by slug and project id" });
}
};
const findByProjectId = async (
projectId: string,
options: {
offset?: number;
limit?: number;
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
includeMetrics?: boolean;
expiringDays?: number;
} = {},
tx?: Knex
): Promise<TCertificateProfile[] | TCertificateProfileWithRawMetrics[] | TCertificateProfileWithConfigs[]> => {
try {
const {
offset = 0,
limit = 20,
search,
enrollmentType,
caId,
includeMetrics = false,
expiringDays = 7
} = options;
let baseQuery = (tx || db)(TableName.PkiCertificateProfile).where(
`${TableName.PkiCertificateProfile}.projectId`,
projectId
);
if (search) {
baseQuery = baseQuery.where((builder) => {
void builder.where((qb) => {
void qb
.whereILike(`${TableName.PkiCertificateProfile}.slug`, `%${search}%`)
.orWhereILike(`${TableName.PkiCertificateProfile}.description`, `%${search}%`);
});
});
}
if (enrollmentType) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.enrollmentType`, enrollmentType);
}
if (caId) {
baseQuery = baseQuery.where(`${TableName.PkiCertificateProfile}.caId`, caId);
}
let query = baseQuery
.leftJoin(
TableName.PkiEstEnrollmentConfig,
`${TableName.PkiCertificateProfile}.estConfigId`,
`${TableName.PkiEstEnrollmentConfig}.id`
)
.leftJoin(
TableName.PkiApiEnrollmentConfig,
`${TableName.PkiCertificateProfile}.apiConfigId`,
`${TableName.PkiApiEnrollmentConfig}.id`
)
.select(selectAllTableCols(TableName.PkiCertificateProfile))
.select(
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays")
);
if (includeMetrics) {
query = query.leftJoin(
TableName.Certificate,
`${TableName.PkiCertificateProfile}.id`,
`${TableName.Certificate}.profileId`
);
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
query = query
.select(
selectAllTableCols(TableName.PkiCertificateProfile),
db.ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estId"),
db
.ref("disableBootstrapCaValidation")
.withSchema(TableName.PkiEstEnrollmentConfig)
.as("estDisableBootstrapCaValidation"),
db.ref("hashedPassphrase").withSchema(TableName.PkiEstEnrollmentConfig).as("estHashedPassphrase"),
db.ref("encryptedCaChain").withSchema(TableName.PkiEstEnrollmentConfig).as("estEncryptedCaChain"),
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
db.ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenewDays"),
db.raw("COUNT(certificates.id) as total_certificates"),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? THEN 1 END) as active_certificates',
[expiringDate]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" <= ? THEN 1 END) as expired_certificates',
[now]
),
db.raw(
'COUNT(CASE WHEN certificates."revokedAt" IS NULL AND certificates."notAfter" > ? AND certificates."notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN certificates."revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.groupBy(
`${TableName.PkiCertificateProfile}.id`,
`${TableName.PkiEstEnrollmentConfig}.id`,
`${TableName.PkiApiEnrollmentConfig}.id`
);
}
const results = (await query
.orderBy(`${TableName.PkiCertificateProfile}.createdAt`, "desc")
.offset(offset)
.limit(limit)) as Record<string, unknown>[];
return results.map((result: Record<string, unknown>) => {
const estConfig =
result.estId && result.estHashedPassphrase
? {
id: result.estId as string,
disableBootstrapCaValidation: !!result.estDisableBootstrapCaValidation,
passphrase: result.estConfigHashedPassphrase,
caChain: result.estEncryptedCaChain ? (result.estEncryptedCaChain as Buffer).toString("utf8") : ""
}
: undefined;
const apiConfig = result.apiId
? {
id: result.apiId as string,
autoRenew: !!result.apiAutoRenew,
autoRenewDays: (result.apiAutoRenewDays as number) || undefined
}
: undefined;
const baseProfile = {
id: result.id,
projectId: result.projectId,
caId: result.caId,
certificateTemplateId: result.certificateTemplateId,
slug: result.slug,
description: result.description,
enrollmentType: result.enrollmentType as EnrollmentType,
estConfigId: result.estConfigId,
apiConfigId: result.apiConfigId,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
estConfig,
apiConfig
};
if (includeMetrics) {
return {
...baseProfile,
total_certificates: result.total_certificates,
active_certificates: result.active_certificates,
expired_certificates: result.expired_certificates,
expiring_certificates: result.expiring_certificates,
revoked_certificates: result.revoked_certificates
} as TCertificateProfileWithRawMetrics & TCertificateProfileWithConfigs;
}
return baseProfile as TCertificateProfileWithConfigs;
});
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profiles by project id" });
}
};
const countByProjectId = async (
projectId: string,
options: {
search?: string;
enrollmentType?: EnrollmentType;
caId?: string;
} = {},
tx?: Knex
): Promise<number> => {
try {
const { search, enrollmentType, caId } = options;
let query = (tx || db)(TableName.PkiCertificateProfile).where({ projectId });
if (search) {
query = query.where((builder) => {
void builder.where((qb) => {
void qb.whereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`);
});
});
}
if (enrollmentType) {
query = query.where({ enrollmentType });
}
if (caId) {
query = query.where({ caId });
}
const result = await query.count("*").first();
return parseInt((result as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Count certificate profiles by project id" });
}
};
const findByNameAndProjectId = async (
name: string,
projectId: string,
tx?: Knex
): Promise<TCertificateProfile | undefined> => {
try {
const certificateProfile = (await (tx || db)(TableName.PkiCertificateProfile)
.where({ slug: name, projectId })
.first()) as TCertificateProfile | undefined;
return certificateProfile;
} catch (error) {
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
}
};
const getCertificatesByProfile = async (
profileId: string,
options: {
offset?: number;
limit?: number;
status?: "active" | "expired" | "revoked";
search?: string;
} = {},
tx?: Knex
): Promise<TCertificateProfileCertificate[]> => {
try {
const { offset = 0, limit = 20, status, search } = options;
const now = new Date();
let query = (tx || db)(TableName.Certificate).where("profileId", profileId);
if (search) {
query = query.where((builder) => {
void builder.where((qb) => {
void qb.whereILike("cn", `%${search}%`).orWhereILike("serialNumber", `%${search}%`);
});
});
}
if (status) {
switch (status) {
case "active":
query = query.where("notAfter", ">", now).whereNull("revokedAt");
break;
case "expired":
query = query.where("notAfter", "<=", now).whereNull("revokedAt");
break;
case "revoked":
query = query.whereNotNull("revokedAt");
break;
default:
break;
}
}
const certificates = await query
.select((tx || db).ref("id").withSchema(TableName.Certificate))
.select((tx || db).ref("serialNumber").withSchema(TableName.Certificate))
.select((tx || db).ref("cn").withSchema(TableName.Certificate))
.select((tx || db).ref("status").withSchema(TableName.Certificate))
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
.select((tx || db).ref("revokedAt").withSchema(TableName.Certificate))
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
.orderBy("createdAt", "desc")
.offset(offset)
.limit(limit);
return certificates.map((cert) => ({
...cert,
revokedAt: cert.revokedAt ?? null
}));
} catch (error) {
throw new DatabaseError({ error, name: "Get certificates by profile" });
}
};
const getProfileMetrics = async (
profileId: string,
expiringDays: number = 7,
tx?: Knex
): Promise<TCertificateProfileMetrics> => {
try {
const now = new Date();
const expiringDate = new Date();
expiringDate.setDate(now.getDate() + expiringDays);
const metrics = await (tx || db)(TableName.Certificate)
.where("profileId", profileId)
.select(
db.raw("COUNT(*) as total_certificates"),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
expiringDate
]),
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
db.raw(
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
[now, expiringDate]
),
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
)
.first();
return {
profileId,
totalCertificates: parseInt(String((metrics as Record<string, unknown>)?.total_certificates || 0), 10),
activeCertificates: parseInt(String((metrics as Record<string, unknown>)?.active_certificates || 0), 10),
expiredCertificates: parseInt(String((metrics as Record<string, unknown>)?.expired_certificates || 0), 10),
expiringCertificates: parseInt(String((metrics as Record<string, unknown>)?.expiring_certificates || 0), 10),
revokedCertificates: parseInt(String((metrics as Record<string, unknown>)?.revoked_certificates || 0), 10)
};
} catch (error) {
throw new DatabaseError({ error, name: "Get certificate profile metrics" });
}
};
const isProfileInUse = async (profileId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.Certificate).where("profileId", profileId).count("*").first();
return parseInt((doc as unknown as { count: string }).count || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "Check if certificate profile is in use" });
}
};
return {
...certificateProfileOrm,
create,
updateById,
deleteById,
findById,
findByIdWithConfigs,
findBySlugAndProjectId,
findByProjectId,
countByProjectId,
findByNameAndProjectId,
getCertificatesByProfile,
getProfileMetrics,
isProfileInUse
};
};
@@ -0,0 +1,134 @@
import RE2 from "re2";
import { z } from "zod";
import { EnrollmentType } from "./certificate-profile-types";
export const createCertificateProfileSchema = z
.object({
projectId: z.string().uuid("Project ID must be valid"),
caId: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1),
encryptedCaChain: z.string()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (!data.estConfig) {
return false;
}
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (!data.apiConfig) {
return false;
}
if (data.estConfig) {
return false;
}
}
return true;
},
{
message:
"EST enrollment type requires EST configuration and cannot have API configuration. API enrollment type requires API configuration and cannot have EST configuration."
}
);
export const updateCertificateProfileSchema = z
.object({
slug: z
.string()
.min(1)
.max(255)
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
.optional(),
description: z.string().max(1000).optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
estConfig: z
.object({
disableBootstrapCaValidation: z.boolean().default(false),
passphrase: z.string().min(1),
encryptedCaChain: z.string()
})
.optional(),
apiConfig: z
.object({
autoRenew: z.boolean().default(false),
autoRenewDays: z.number().min(1).max(365).optional()
})
.optional()
})
.refine(
(data) => {
if (data.enrollmentType === EnrollmentType.EST) {
if (data.apiConfig) {
return false;
}
}
if (data.enrollmentType === EnrollmentType.API) {
if (data.estConfig) {
return false;
}
}
return true;
},
{
message: "Cannot have EST config with API enrollment type or API config with EST enrollment type."
}
);
export const getCertificateProfileByIdSchema = z.object({
id: z.string().uuid()
});
export const getCertificateProfileBySlugSchema = z.object({
projectId: z.string().uuid("Project ID must be valid"),
slug: z.string().min(1)
});
export const listCertificateProfilesSchema = z.object({
projectId: z.string().uuid("Project ID must be valid"),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
caId: z.string().uuid().optional()
});
export const deleteCertificateProfileSchema = z.object({
id: z.string().uuid()
});
export const listCertificatesByProfileSchema = z.object({
profileId: z.string().uuid(),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
status: z.enum(["active", "expired", "revoked"]).optional(),
search: z.string().optional()
});
export const getCertificateProfileMetricsSchema = z.object({
profileId: z.string().uuid(),
expiringDays: z.coerce.number().min(1).max(365).default(30)
});

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