diff --git a/backend/src/db/migrations/20251023100246_project-identity.ts b/backend/src/db/migrations/20251023100246_project-identity.ts new file mode 100644 index 000000000..99bd3edb8 --- /dev/null +++ b/backend/src/db/migrations/20251023100246_project-identity.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId"); + if (!hasProjectIdCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.string("projectId"); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasProjectIdCol = await knex.schema.hasColumn(TableName.Identity, "projectId"); + if (hasProjectIdCol) { + await knex.schema.alterTable(TableName.Identity, (t) => { + t.dropColumn("projectId"); + }); + } +} diff --git a/backend/src/db/schemas/identities.ts b/backend/src/db/schemas/identities.ts index 06c37ff22..0814d665e 100644 --- a/backend/src/db/schemas/identities.ts +++ b/backend/src/db/schemas/identities.ts @@ -14,7 +14,8 @@ export const IdentitiesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), hasDeleteProtection: z.boolean().default(false), - orgId: z.string().uuid() + orgId: z.string().uuid(), + projectId: z.string().nullable().optional() }); export type TIdentities = z.infer; diff --git a/backend/src/services/scoped-identity/identity-dal.ts b/backend/src/services/scoped-identity/identity-dal.ts new file mode 100644 index 000000000..0375a1b08 --- /dev/null +++ b/backend/src/services/scoped-identity/identity-dal.ts @@ -0,0 +1,185 @@ +import { TDbClient } from "@app/db"; +import { AccessScope, AccessScopeData, IdentitiesSchema, TableName } from "@app/db/schemas"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +import { buildAuthMethods } from "../identity/identity-fns"; + +export type TIdentityDALFactory = ReturnType; + +export const identityDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Identity); + + const getIdentityById = async (scopeData: AccessScopeData, identityId: string) => { + const doc = await db + .replicaNode()(TableName.Identity) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Identity}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .leftJoin( + TableName.IdentityUniversalAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityUniversalAuth}.identityId` + ) + .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`) + .leftJoin( + TableName.IdentityAliCloudAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityAliCloudAuth}.identityId` + ) + .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`) + .leftJoin( + TableName.IdentityKubernetesAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityKubernetesAuth}.identityId` + ) + .leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`) + .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) + .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`) + .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) + .leftJoin( + TableName.IdentityTlsCertAuth, + `${TableName.Identity}.id`, + `${TableName.IdentityTlsCertAuth}.identityId` + ) + .leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`) + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) + .where(`${TableName.Identity}.id`, identityId) + .where(`${TableName.Identity}.orgId`, scopeData.orgId) + .where((qb) => { + if (scopeData.scope === AccessScope.Project) { + void qb.where(`${TableName.Identity}.projectId`, scopeData.projectId); + } else { + void qb.whereNull(`${TableName.Identity}.projectId`); + } + }) + .select( + selectAllTableCols(TableName.Identity), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), + db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), + db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), + db.ref("id").as("alicloudId").withSchema(TableName.IdentityAliCloudAuth), + db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth), + db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth), + db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth), + db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth), + db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth), + db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), + db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), + db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), + db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth) + ); + + if (!doc) return doc; + + const formattedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (el) => { + const { + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + alicloudId, + tokenId, + jwtId, + ociId, + ldapId, + tlsCertId + } = el; + return { + ...IdentitiesSchema.parse(el), + authMethods: buildAuthMethods({ + uaId, + awsId, + gcpId, + kubernetesId, + oidcId, + azureId, + tokenId, + alicloudId, + jwtId, + ldapId, + ociId, + tlsCertId + }) + }; + }, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return formattedDoc?.[0]; + }; + + const listIdentities = async ( + scopeData: AccessScopeData, + filter: { limit?: number; offset?: number; search?: string } = {} + ) => { + const query = db + .replicaNode()(TableName.Identity) + .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { + void queryBuilder + .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) + .andOn(`${TableName.Identity}.orgId`, `${TableName.IdentityMetadata}.orgId`); + }) + .where(`${TableName.Identity}.orgId`, scopeData.orgId) + .where((qb) => { + if (scopeData.scope === AccessScope.Project) { + void qb.where(`${TableName.Identity}.projectId`, scopeData.projectId); + } else { + void qb.whereNull(`${TableName.Identity}.projectId`); + } + }) + .select( + selectAllTableCols(TableName.Identity), + db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue") + ) + .select(db.raw(`count(distinct ??) over () as ??`, [`${TableName.Identity}.id`, "count"])); + + if (filter.limit) void query.limit(filter.limit); + if (filter.offset) void query.offset(filter.offset || 0); + + if (filter.search) void query.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`); + + const docs = await query; + + const formattedDoc = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => IdentitiesSchema.parse(el), + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return { docs: formattedDoc, count: Number((docs?.[0] as unknown as { count: number })?.count) }; + }; + + return { ...orm, listIdentities, getIdentityById }; +}; diff --git a/backend/src/services/scoped-identity/identity-service.ts b/backend/src/services/scoped-identity/identity-service.ts new file mode 100644 index 000000000..b3605bd93 --- /dev/null +++ b/backend/src/services/scoped-identity/identity-service.ts @@ -0,0 +1,245 @@ +import { AccessScope, OrgMembershipRole } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; + +import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; +import { TMembershipRoleDALFactory } from "../membership/membership-role-dal"; +import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TIdentityDALFactory } from "./identity-dal"; +import { + TCreateIdentityDTO, + TDeleteIdentityDTO, + TGetIdentityByIdDTO, + TListIdentityDTO, + TUpdateIdentityDTO +} from "./identity-types"; +import { newOrgIdentityFactory } from "./org/org-identity-factory"; +import { newProjectIdentityFactory } from "./project/project-identity-factory"; + +type TScopedIdentityServiceFactoryDep = { + identityDAL: TIdentityDALFactory; + permissionService: TPermissionServiceFactory; + licenseService: Pick; + membershipIdentityDAL: TMembershipIdentityDALFactory; + membershipRoleDAL: TMembershipRoleDALFactory; + identityMetadataDAL: TIdentityMetadataDALFactory; +}; + +export type TScopedIdentityServiceFactory = ReturnType; + +export const identityServiceFactory = ({ + identityDAL, + permissionService, + licenseService, + membershipIdentityDAL, + membershipRoleDAL, + identityMetadataDAL +}: TScopedIdentityServiceFactoryDep) => { + const orgFactory = newOrgIdentityFactory({ + permissionService + }); + const projectFactory = newProjectIdentityFactory({ + permissionService + }); + + const scopeFactory = { + [AccessScope.Organization]: orgFactory, + [AccessScope.Project]: projectFactory, + // namespace will get stripped off + [AccessScope.Namespace]: orgFactory + }; + + const createIdentity = async (dto: TCreateIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onCreateIdentityGuard(dto); + + const plan = await licenseService.getPlan(dto.permission.orgId); + + if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities." + }); + } + + const identity = await identityDAL.transaction(async (tx) => { + const newIdentity = await identityDAL.create( + { + name: data.name, + hasDeleteProtection: data.hasDeleteProtection, + orgId: dto.permission.orgId, + projectId: scopeData.scope === AccessScope.Project ? scopeData.projectId : null + }, + tx + ); + const orgMembership = await membershipIdentityDAL.create( + { + scope: AccessScope.Organization, + actorIdentityId: newIdentity.id, + scopeOrgId: dto.permission.orgId + }, + tx + ); + + const newMembershipIds = [orgMembership.id]; + if (scopeData.scope === AccessScope.Project) { + const projectMembership = await membershipIdentityDAL.create( + { + scope: AccessScope.Project, + actorIdentityId: newIdentity.id, + scopeOrgId: dto.permission.orgId, + scopeProjectId: scopeData.projectId + }, + tx + ); + newMembershipIds.push(projectMembership.id); + } + + await membershipRoleDAL.insertMany( + newMembershipIds.map((membershipId) => ({ + membershipId, + role: OrgMembershipRole.NoAccess + })), + tx + ); + + let insertedMetadata: Array<{ + id: string; + key: string; + value: string; + }> = []; + + if (data.metadata && data.metadata.length) { + const rowsToInsert = data.metadata.map(({ key, value }) => ({ + identityId: newIdentity.id, + orgId: dto.permission.orgId, + key, + value + })); + + insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx); + } + + return { + ...newIdentity, + authMethods: [], + metadata: insertedMetadata + }; + }); + await licenseService.updateSubscriptionOrgMemberCount(dto.permission.orgId); + + return { identity }; + }; + + const updateIdentity = async (dto: TUpdateIdentityDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onUpdateIdentityGuard(dto); + const existingIdentity = await identityDAL.findOne({ + id: dto.selector.identityId, + orgId: dto.permission.orgId, + projectId: dto.scopeData.scope === AccessScope.Project ? dto.scopeData.projectId : null + }); + if (!existingIdentity) + throw new NotFoundError({ message: `Identity with id ${dto.selector.identityId} not found` }); + + const identity = await identityDAL.transaction(async (tx) => { + const newIdentity = + data?.name || data?.hasDeleteProtection + ? await identityDAL.updateById( + dto.selector.identityId, + { name: data.name, hasDeleteProtection: data.hasDeleteProtection }, + tx + ) + : existingIdentity; + + let insertedMetadata: Array<{ + id: string; + key: string; + value: string; + }> = []; + + if (data.metadata) { + await identityMetadataDAL.delete({ orgId: dto.permission.orgId, identityId: dto.selector.identityId }, tx); + + if (data.metadata.length) { + const rowsToInsert = data.metadata.map(({ key, value }) => ({ + identityId: newIdentity.id, + orgId: dto.permission.orgId, + key, + value + })); + + insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx); + } + } + + return { + ...newIdentity, + metadata: insertedMetadata + }; + }); + + return { identity }; + }; + + const deleteIdentity = async (dto: TDeleteIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onDeleteIdentityGuard(dto); + + const existingIdentity = await identityDAL.findOne({ + id: dto.selector.identityId, + orgId: dto.permission.orgId, + projectId: dto.scopeData.scope === AccessScope.Project ? dto.scopeData.projectId : null + }); + if (!existingIdentity) + throw new NotFoundError({ message: `Identity with id ${dto.selector.identityId} not found` }); + + const deletedIdentity = await identityDAL.deleteById(dto.selector.identityId); + + await licenseService.updateSubscriptionOrgMemberCount(scopeData.orgId); + + return { identity: deletedIdentity }; + }; + + const getIdentityById = async (dto: TGetIdentityByIdDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onGetIdentityByIdGuard(dto); + + const identity = await identityDAL.getIdentityById(dto.scopeData, dto.selector.identityId); + if (!identity) throw new NotFoundError({ message: `Identity with id ${dto.selector.identityId} not found` }); + + return { identity }; + }; + + const listIdentities = async (dto: TListIdentityDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + + await factory.onListIdentityGuard(dto); + + const identities = await identityDAL.listIdentities(dto.scopeData, { + search: dto.data.search, + offset: dto.data.offset, + limit: dto.data.limit + }); + + return { identities }; + }; + + return { + createIdentity, + updateIdentity, + deleteIdentity, + getIdentityById, + listIdentities + }; +}; diff --git a/backend/src/services/scoped-identity/identity-types.ts b/backend/src/services/scoped-identity/identity-types.ts new file mode 100644 index 000000000..4d3e3e0ff --- /dev/null +++ b/backend/src/services/scoped-identity/identity-types.ts @@ -0,0 +1,67 @@ +import { AccessScopeData } from "@app/db/schemas"; +import { OrderByDirection, OrgServiceActor } from "@app/lib/types"; + +export interface TIdentityFactory { + onCreateIdentityGuard: (arg: TCreateIdentityDTO) => Promise; + onUpdateIdentityGuard: (arg: TUpdateIdentityDTO) => Promise; + onDeleteIdentityGuard: (arg: TDeleteIdentityDTO) => Promise; + onListIdentityGuard: (arg: TListIdentityDTO) => Promise; + onGetIdentityByIdGuard: (arg: TGetIdentityByIdDTO) => Promise; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; +} + +export enum IdentityOrderBy { + Name = "name", + Role = "role" +} + +export type TCreateIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + name: string; + hasDeleteProtection: boolean; + metadata?: { key: string; value: string }[]; + }; +}; + +export type TUpdateIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; + data: Partial<{ + name: string; + hasDeleteProtection: boolean; + metadata?: { key: string; value: string }[]; + }>; +}; + +export type TDeleteIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; + +export type TGetIdentityByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + identityId: string; + }; +}; + +export type TListIdentityDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: Partial<{ + limit: number; + offset: number; + orderBy: IdentityOrderBy; + orderDirection: OrderByDirection; + search: string; + }>; +}; diff --git a/backend/src/services/scoped-identity/org/org-identity-factory.ts b/backend/src/services/scoped-identity/org/org-identity-factory.ts new file mode 100644 index 000000000..cec7a4e66 --- /dev/null +++ b/backend/src/services/scoped-identity/org/org-identity-factory.ts @@ -0,0 +1,90 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { InternalServerError } from "@app/lib/errors"; + +import { TIdentityFactory } from "../identity-types"; + +type TOrgIdentityFactoryDep = { + permissionService: Pick; +}; + +export const newOrgIdentityFactory = ({ permissionService }: TOrgIdentityFactoryDep): TIdentityFactory => { + const getScopeField: TIdentityFactory["getScopeField"] = (scopeData) => { + if (scopeData.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: scopeData.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const onCreateIdentityGuard: TIdentityFactory["onCreateIdentityGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + }; + + const onUpdateIdentityGuard: TIdentityFactory["onUpdateIdentityGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + }; + + const onDeleteIdentityGuard: TIdentityFactory["onDeleteIdentityGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + }; + + const onListIdentityGuard: TIdentityFactory["onListIdentityGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + const onGetIdentityByIdGuard: TIdentityFactory["onGetIdentityByIdGuard"] = async (dto) => { + const { permission } = await permissionService.getOrgPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + orgId: dto.permission.orgId, + actorAuthMethod: dto.permission.authMethod, + actorOrgId: dto.permission.orgId, + scope: OrganizationActionScope.Any + }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + }; + + return { + onCreateIdentityGuard, + onUpdateIdentityGuard, + onDeleteIdentityGuard, + onListIdentityGuard, + onGetIdentityByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/scoped-identity/project/project-identity-factory.ts b/backend/src/services/scoped-identity/project/project-identity-factory.ts new file mode 100644 index 000000000..cf6976a5b --- /dev/null +++ b/backend/src/services/scoped-identity/project/project-identity-factory.ts @@ -0,0 +1,110 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { InternalServerError } from "@app/lib/errors"; + +import { TIdentityFactory } from "../identity-types"; + +type TProjectIdentityFactoryDep = { + permissionService: Pick; +}; + +export const newProjectIdentityFactory = ({ permissionService }: TProjectIdentityFactoryDep): TIdentityFactory => { + const getScopeField: TIdentityFactory["getScopeField"] = (scopeData) => { + if (scopeData.scope === AccessScope.Project) { + return { key: "projectId" as const, value: scopeData.projectId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the project factory" }); + }; + + const onCreateIdentityGuard: TIdentityFactory["onCreateIdentityGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Create, + ProjectPermissionSub.Identity + ); + }; + + const onUpdateIdentityGuard: TIdentityFactory["onUpdateIdentityGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Edit, + ProjectPermissionSub.Identity + ); + }; + + const onDeleteIdentityGuard: TIdentityFactory["onDeleteIdentityGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Delete, + ProjectPermissionSub.Identity + ); + }; + + const onListIdentityGuard: TIdentityFactory["onListIdentityGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + const onGetIdentityByIdGuard: TIdentityFactory["onGetIdentityByIdGuard"] = async (dto) => { + const scope = getScopeField(dto.scopeData); + const { permission } = await permissionService.getProjectPermission({ + actor: dto.permission.type, + actorId: dto.permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: dto.permission.authMethod, + projectId: scope.value, + actorOrgId: dto.permission.orgId + }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); + }; + + return { + onCreateIdentityGuard, + onUpdateIdentityGuard, + onDeleteIdentityGuard, + onListIdentityGuard, + onGetIdentityByIdGuard, + getScopeField + }; +};