diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index a46d3ea96..d3537b028 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -310,6 +310,16 @@ export enum ActionProjectType { Any = "any" } +export enum TemporaryPermissionMode { + Relative = "relative" +} + +export enum MembershipActors { + Group = "group", + User = "user", + Identity = "identity" +} + export enum SortDirection { ASC = "asc", DESC = "desc" diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index c2876572f..3c6356e9a 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -292,6 +292,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + // TODO(simp): look into this in the new ap service await accessApprovalRequestDAL.update( { privilegeId: userPrivilege.id diff --git a/backend/src/services/additional-privilege/additional-privilege-dal.ts b/backend/src/services/additional-privilege/additional-privilege-dal.ts new file mode 100644 index 000000000..040fbcb0b --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAdditionalPrivilegeDALFactory = ReturnType; + +export const additionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.AdditionalPrivilege); + return orm; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-service.ts b/backend/src/services/additional-privilege/additional-privilege-service.ts new file mode 100644 index 000000000..803a581b0 --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-service.ts @@ -0,0 +1,208 @@ +import { RawRule } from "@casl/ability"; +import { packRules } from "@casl/ability/extra"; + +import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; +import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; + +import { TMembershipDALFactory } from "../membership/membership-dal"; +import { TOrgDALFactory } from "../org/org-dal"; +import { TAdditionalPrivilegeDALFactory } from "./additional-privilege-dal"; +import { + TAdditionalPrivilegesScopeFactory, + TCreateAdditionalPrivilegesDTO, + TDeleteAdditionalPrivilegesDTO, + TGetAdditionalPrivilegesByIdDTO, + TListAdditionalPrivilegesDTO, + TUpdateAdditionalPrivilegesDTO +} from "./additional-privilege-types"; +import { newNamespaceAdditionalPrivilegesFactory } from "./namespace/namespace-additional-privilege-factory"; +import { newOrgAdditionalPrivilegesFactory } from "./org/org-additional-privilege-factory"; +import { newProjectAdditionalPrivilegesFactory } from "./project/project-additional-privilege-factory"; + +type TAdditionalPrivilegeServiceFactoryDep = { + additionalPrivilegeDAL: TAdditionalPrivilegeDALFactory; + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export type TAdditionalPrivilegeServiceFactory = ReturnType; + +export const additionalPrivilegeServiceFactory = ({ + additionalPrivilegeDAL, + permissionService, + orgDAL, + membershipDAL +}: TAdditionalPrivilegeServiceFactoryDep) => { + const scopeFactory: Record = { + [AccessScope.Organization]: newOrgAdditionalPrivilegesFactory({}), + [AccessScope.Project]: newProjectAdditionalPrivilegesFactory({ + membershipDAL, + orgDAL, + permissionService + }), + [AccessScope.Namespace]: newNamespaceAdditionalPrivilegesFactory({}) + }; + + const createAdditionalPrivilege = async (dto: TCreateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + const { membershipId } = await factory.onCreateAdditionalPrivilegesGuard(dto); + + const existingSlug = await additionalPrivilegeDAL.findOne({ + name: data.name, + membershipId + }); + if (existingSlug) throw new BadRequestError({ message: `Additional privilege with name ${data.name} exist` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + if (!data.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.create({ + membershipId, + name: data.name, + isTemporary: data.isTemporary, + permissions: JSON.stringify(packRules(data.permissions as RawRule[])) + }); + + return { additionalPrivilege: { ...additionalPrivilege, permissions: data.permissions } }; + } + + if (!data.temporaryAccessStartTime || !data.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(data.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.create({ + membershipId, + name: data.name, + isTemporary: data.isTemporary, + permissions: data.permissions, + temporaryAccessEndTime: new Date( + new Date(data.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(data.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: data.temporaryRange + }); + + return { additionalPrivilege: { ...additionalPrivilege, permissions: data.permissions } }; + }; + + const updateAdditionalPrivilege = async (dto: TUpdateAdditionalPrivilegesDTO) => { + const { scopeData, data } = dto; + const factory = scopeFactory[scopeData.scope]; + const { membershipId } = await factory.onUpdateAdditionalPrivilegesGuard(dto); + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + id: dto.selector.id, + membershipId + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with name ${data.name} doesn't exist` }); + + validateHandlebarTemplate("Additional Privilege Create", JSON.stringify(data.permissions || []), { + allowedExpressions: (val) => val.includes("identity.") + }); + + const updatedData = { ...existingPrivilege, ...data }; + + if (!updatedData.isTemporary) { + const additionalPrivilege = await additionalPrivilegeDAL.updateById(existingPrivilege.id, { + name: updatedData.name, + isTemporary: data.isTemporary, + permissions: data.permissions ? JSON.stringify(packRules(data.permissions as RawRule[])) : undefined + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + } + + if (!updatedData.temporaryAccessStartTime || !updatedData.temporaryRange) { + throw new BadRequestError({ message: "Temporary mode expects start time and range" }); + } + + const relativeTempAllocatedTimeInMs = ms(updatedData.temporaryRange); + const additionalPrivilege = await additionalPrivilegeDAL.create({ + membershipId, + name: updatedData.name, + isTemporary: updatedData.isTemporary, + permissions: updatedData.permissions, + temporaryAccessEndTime: new Date( + new Date(updatedData.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs + ), + temporaryAccessStartTime: new Date(updatedData.temporaryAccessStartTime), + temporaryMode: TemporaryPermissionMode.Relative, + temporaryRange: updatedData.temporaryRange + }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const deleteAdditionalPrivilege = async (dto: TDeleteAdditionalPrivilegesDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + const { membershipId } = await factory.onDeleteAdditionalPrivilegesGuard(dto); + + const existingPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + membershipId + }); + if (!existingPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id); + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const getAdditionalPrivilegeById = async (dto: TGetAdditionalPrivilegesByIdDTO) => { + const { scopeData, selector } = dto; + const factory = scopeFactory[scopeData.scope]; + const { membershipId } = await factory.onListAdditionalPrivilegesGuard(dto); + const additionalPrivilege = await additionalPrivilegeDAL.findOne({ + id: selector.id, + membershipId + }); + if (!additionalPrivilege) + throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); + + return { + additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + }; + }; + + const listAdditionalPrivileges = async (dto: TListAdditionalPrivilegesDTO) => { + const { scopeData } = dto; + const factory = scopeFactory[scopeData.scope]; + const { membershipId } = await factory.onListAdditionalPrivilegesGuard(dto); + const additionalPrivileges = await additionalPrivilegeDAL.find({ + membershipId + }); + + return { + additionalPrivileges: additionalPrivileges.map((el) => ({ + ...el, + permissions: unpackPermissions(el.permissions) + })) + }; + }; + + return { + createAdditionalPrivilege, + updateAdditionalPrivilege, + deleteAdditionalPrivilege, + getAdditionalPrivilegeById, + listAdditionalPrivileges + }; +}; diff --git a/backend/src/services/additional-privilege/additional-privilege-types.ts b/backend/src/services/additional-privilege/additional-privilege-types.ts new file mode 100644 index 000000000..7c708ba06 --- /dev/null +++ b/backend/src/services/additional-privilege/additional-privilege-types.ts @@ -0,0 +1,73 @@ +import { AccessScopeData, MembershipActors, TemporaryPermissionMode } from "@app/db/schemas"; +import { OrgServiceActor } from "@app/lib/types"; + +export interface TAdditionalPrivilegesScopeFactory { + onCreateAdditionalPrivilegesGuard: (arg: TCreateAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>; + onUpdateAdditionalPrivilegesGuard: (arg: TUpdateAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>; + onDeleteAdditionalPrivilegesGuard: (arg: TDeleteAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>; + onListAdditionalPrivilegesGuard: (arg: TListAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>; + onGetAdditionalPrivilegesByIdGuard: (arg: TGetAdditionalPrivilegesByIdDTO) => Promise<{ membershipId: string }>; + getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string }; +} + +export type TCreateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + data: { + actorId: string; + actorType: MembershipActors; + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }; +}; + +export type TUpdateAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: MembershipActors; + }; + data: Partial<{ + name: string; + permissions: unknown; + isTemporary: boolean; + temporaryMode?: TemporaryPermissionMode.Relative; + temporaryRange?: string; + temporaryAccessStartTime?: string; + }>; +}; + +export type TListAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + actorId: string; + actorType: MembershipActors; + }; +}; + +export type TDeleteAdditionalPrivilegesDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: MembershipActors; + }; +}; + +export type TGetAdditionalPrivilegesByIdDTO = { + permission: OrgServiceActor; + scopeData: AccessScopeData; + selector: { + id: string; + actorId: string; + actorType: MembershipActors; + }; +}; diff --git a/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts new file mode 100644 index 000000000..e2ccde286 --- /dev/null +++ b/backend/src/services/additional-privilege/namespace/namespace-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TNamespaceAdditionalPrivilegesScopeFactoryDep = Record; + +export const newNamespaceAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TNamespaceAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Namespace) { + return { key: "namespaceId" as const, value: dto.namespaceId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the namespace factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Namespace additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts new file mode 100644 index 000000000..cc1fbdd6c --- /dev/null +++ b/backend/src/services/additional-privilege/org/org-additional-privilege-factory.ts @@ -0,0 +1,52 @@ +import { AccessScope } from "@app/db/schemas"; +import { InternalServerError } from "@app/lib/errors"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TOrgAdditionalPrivilegesScopeFactoryDep = Record; + +export const newOrgAdditionalPrivilegesFactory = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deps: TOrgAdditionalPrivilegesScopeFactoryDep +): TAdditionalPrivilegesScopeFactory => { + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Organization) { + return { key: "orgId" as const, value: dto.orgId }; + } + throw new InternalServerError({ message: "Invalid scope provided for the org factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges create not implemented" }); + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges update not implemented" }); + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges delete not implemented" }); + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges list not implemented" }); + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async () => { + throw new InternalServerError({ message: "Org additional privileges get by id not implemented" }); + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts new file mode 100644 index 000000000..4956f7eac --- /dev/null +++ b/backend/src/services/additional-privilege/project/project-additional-privilege-factory.ts @@ -0,0 +1,222 @@ +import { ForbiddenError } from "@casl/ability"; + +import { AccessScope, ActionProjectType, MembershipActors } from "@app/db/schemas"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionIdentityActions, + ProjectPermissionMemberActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, InternalServerError, PermissionBoundaryError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TMembershipDALFactory } from "@app/services/membership/membership-dal"; +import { TOrgDALFactory } from "@app/services/org/org-dal"; + +import { TAdditionalPrivilegesScopeFactory } from "../additional-privilege-types"; + +type TProjectAdditionalPrivilegesScopeFactoryDep = { + permissionService: Pick; + orgDAL: Pick; + membershipDAL: Pick; +}; + +export const newProjectAdditionalPrivilegesFactory = ({ + permissionService, + orgDAL, + membershipDAL +}: TProjectAdditionalPrivilegesScopeFactoryDep): TAdditionalPrivilegesScopeFactory => { + const $getPermission = (permission: OrgServiceActor, projectId: string) => { + return permissionService.getProjectPermission({ + actor: permission.type, + actorId: permission.id, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: permission.authMethod, + projectId, + actorOrgId: permission.orgId + }); + }; + + const $getActorType = (type: MembershipActors) => { + if (type === MembershipActors.Group) + throw new InternalServerError({ message: "Group additional privilege not implemented for projects" }); + + if (type === MembershipActors.Identity) return ActorType.IDENTITY; + if (type === MembershipActors.User) return ActorType.USER; + + throw new InternalServerError({ message: `Group additional privilege not implemented for actor: ${String(type)}` }); + }; + + const getScopeField: TAdditionalPrivilegesScopeFactory["getScopeField"] = (dto) => { + if (dto.scope === AccessScope.Project) { + return { key: "projectId" as const, value: dto.projectId }; + } + throw new BadRequestError({ message: "Invalid scope provided for the factory" }); + }; + + const onCreateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onCreateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const actorType = $getActorType(dto.data.actorType); + + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.data.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.data.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + return { membershipId: membership.id }; + }; + + const onUpdateAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onUpdateAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const actorType = $getActorType(dto.selector.actorType); + + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + + const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId); + const { permission: targetUserPermission, memberships } = await $getPermission( + { ...dto.permission, type: actorType, id: dto.selector.actorId }, + scope.value + ); + + const permissionAction = + actorType === ActorType.USER + ? ProjectPermissionMemberActions.GrantPrivileges + : ProjectPermissionIdentityActions.GrantPrivileges; + const permissionSubject = + actorType === ActorType.USER ? ProjectPermissionSub.Member : ProjectPermissionSub.Identity; + const permissionBoundary = validatePrivilegeChangeOperation( + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject, + permission, + targetUserPermission + ); + if (!permissionBoundary.isValid) + throw new PermissionBoundaryError({ + message: constructPermissionErrorMessage( + "Failed to update more privileged actor", + shouldUseNewPrivilegeSystem, + permissionAction, + permissionSubject + ), + details: { missingPermissions: permissionBoundary.missingPermissions } + }); + + const membership = memberships.find( + (el) => el[actorType === ActorType.IDENTITY ? "actorIdentityId" : "actorUserId"] === dto.selector.actorId + ); + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + return { membershipId: membership.id }; + }; + + const onDeleteAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onDeleteAdditionalPrivilegesGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const actorType = $getActorType(dto.selector.actorType); + + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + return { membershipId: membership.id }; + }; + + const onListAdditionalPrivilegesGuard: TAdditionalPrivilegesScopeFactory["onListAdditionalPrivilegesGuard"] = async ( + dto + ) => { + const scope = getScopeField(dto.scopeData); + const actorType = $getActorType(dto.selector.actorType); + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + return { membershipId: membership.id }; + }; + + const onGetAdditionalPrivilegesByIdGuard: TAdditionalPrivilegesScopeFactory["onGetAdditionalPrivilegesByIdGuard"] = + async (dto) => { + const scope = getScopeField(dto.scopeData); + const actorType = $getActorType(dto.selector.actorType); + + const permissionSet = + actorType === ActorType.USER + ? ([ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member] as const) + : ([ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity] as const); + const { permission } = await $getPermission(dto.permission, scope.value); + ForbiddenError.from(permission).throwUnlessCan(...permissionSet); + + const membership = await membershipDAL.findOne({ + scopeOrgId: dto.permission.orgId, + scopeProjectId: scope.value, + [actorType === ActorType.USER ? "actorUserId" : "actorIdentityId"]: dto.selector.actorId + }); + + if (!membership) throw new BadRequestError({ message: "Actor doesn't have membership" }); + return { membershipId: membership.id }; + }; + + return { + onCreateAdditionalPrivilegesGuard, + onUpdateAdditionalPrivilegesGuard, + onDeleteAdditionalPrivilegesGuard, + onListAdditionalPrivilegesGuard, + onGetAdditionalPrivilegesByIdGuard, + getScopeField + }; +}; diff --git a/backend/src/services/membership/membership-dal.ts b/backend/src/services/membership/membership-dal.ts new file mode 100644 index 000000000..03ee6af92 --- /dev/null +++ b/backend/src/services/membership/membership-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TMembershipDALFactory = ReturnType; + +export const membershipDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Membership); + return orm; +}; diff --git a/backend/src/services/membership/membership-service.ts b/backend/src/services/membership/membership-service.ts new file mode 100644 index 000000000..c9dc34081 --- /dev/null +++ b/backend/src/services/membership/membership-service.ts @@ -0,0 +1,11 @@ +import { TMembershipDALFactory } from "./membership-dal"; + +type TMembershipServiceFactoryDep = { + membershipDAL: TMembershipDALFactory; +}; + +export type TMembershipServiceFactory = ReturnType; + +export const membershipServiceFactory = ({ membershipDAL }: TMembershipServiceFactoryDep) => { + return {}; +}; diff --git a/backend/src/services/membership/membership-types.ts b/backend/src/services/membership/membership-types.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/services/role/role-service.ts b/backend/src/services/role/role-service.ts index 3611749e7..f9a066cb0 100644 --- a/backend/src/services/role/role-service.ts +++ b/backend/src/services/role/role-service.ts @@ -44,11 +44,17 @@ export const roleServiceFactory = ({ roleDAL, permissionService }: TRoleServiceF const factory = scopeFactory[scopeData.scope]; await factory.onCreateRoleGuard(dto); + const scope = factory.getScopeField(scopeData); + const existingRole = await roleDAL.findOne({ + slug: data.slug, + [scope.key]: scope.value + }); + if (existingRole) throw new NotFoundError({ message: `Role with ${data.slug} exist` }); + validateHandlebarTemplate("Role Creation", JSON.stringify(data.permissions || []), { allowedExpressions: (val) => val.includes("identity.") }); - const scope = factory.getScopeField(scopeData); const role = await roleDAL.create({ name: data.name, description: data.description,