From a908471e66fb65f51726e2c055fb44b1fdd93526 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 15 Mar 2024 15:53:49 +0530 Subject: [PATCH] feat(server): completed user additional privilege services --- .../ee/services/permission/permission-dal.ts | 57 +++++- .../services/permission/permission-service.ts | 12 +- .../project-user-additional-privilege-dal.ts | 10 + ...oject-user-additional-privilege-service.ts | 175 ++++++++++++++++++ ...project-user-additional-privilege-types.ts | 44 +++++ 5 files changed, 291 insertions(+), 7 deletions(-) create mode 100644 backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts create mode 100644 backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts create mode 100644 backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index fc0a85ac6..dda4bee50 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -56,6 +56,11 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .where("userId", userId) @@ -69,9 +74,22 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") - ) - .select("permissions"); + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"), + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userApTemporaryAccessEndTime") + ); const permission = sqlNestRelationships({ data: docs, @@ -102,15 +120,44 @@ export const permissionDALFactory = (db: TDbClient) => { permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) + }, + { + key: "userApId", + label: "additionalPrivileges" as const, + mapper: ({ + userApId, + userApPermissions, + userApIsTemporary, + userApTemporaryMode, + userApTemporaryRange, + userApTemporaryAccessEndTime, + userApTemporaryAccessStartTime + }) => ({ + id: userApId, + permissions: userApPermissions, + temporaryRange: userApTemporaryRange, + temporaryMode: userApTemporaryMode, + temporaryAccessEndTime: userApTemporaryAccessEndTime, + temporaryAccessStartTime: userApTemporaryAccessStartTime, + isTemporary: userApIsTemporary + }) } ] }); // when introducting cron mode change it here - const activeRoles = permission?.[0]?.roles.filter( + const activeRoles = permission?.[0]?.roles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ); - return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; + + const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + + return permission?.[0] + ? { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges } + : undefined; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); } diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index d632be3df..8062a1c43 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -198,9 +198,17 @@ export const permissionServiceFactory = ({ validateOrgSAML(authMethod, membership.orgAuthEnforced); + // join two permissions and pass to build the final permission set + const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + return { - permission: buildProjectPermission(membership.roles), - membership, + permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), + membership: userProjectPermission, hasRole: (role: string) => membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1 }; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts new file mode 100644 index 000000000..6c15d2d5d --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-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 TProjectUserAdditionalPrivilegeDALFactory = ReturnType; + +export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege); + return orm; +}; 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 new file mode 100644 index 000000000..e1f3ba823 --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -0,0 +1,175 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { BadRequestError } from "@app/lib/errors"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; +import { + ProjectUserAdditionalPrivilegeTemporaryMode, + TCreateUserPrivilegeDTO, + TDeleteUserPrivilegeDTO, + TGetUserPrivilegeDetailsDTO, + TUpdateUserPrivilegeDTO +} from "./project-user-additional-privilege-types"; + +type TProjectUserAdditionalPrivilegeServiceFactoryDep = { + projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; + projectMembershipDAL: Pick; + permissionService: Pick; +}; + +export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< + typeof projectUserAdditionalPrivilegeServiceFactory +>; + +export const projectUserAdditionalPrivilegeServiceFactory = ({ + projectUserAdditionalPrivilegeDAL, + projectMembershipDAL, + permissionService +}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => { + const create = async ({ + name, + slug, + actor, + actorId, + permissions: customPermission, + actorOrgId, + description, + projectMembershipId, + ...dto + }: TCreateUserPrivilegeDTO) => { + const projectMembership = await projectMembershipDAL.findById(projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId }); + if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + + if (!dto.isTemporary) { + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ + projectMembershipId, + slug, + permissions: customPermission, + name, + description + }); + return additionalPrivilege; + } + + const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ + projectMembershipId, + slug, + permissions: customPermission, + name, + description, + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: dto.temporaryRange, + temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) + }); + return additionalPrivilege; + }; + + const updateById = async ({ privilegeId, actorOrgId, actor, actorId, ...dto }: TUpdateUserPrivilegeDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + if (dto?.slug) { + const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ + slug: dto.slug, + projectMembershipId: projectMembership.id + }); + if (existingSlug && existingSlug.id !== userPrivilege.id) + throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + } + + const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; + if (isTemporary) { + const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; + const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { + ...dto, + temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), + temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) + }); + return additionalPrivilege; + } + + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { + ...dto, + isTemporary: false, + temporaryAccessStartTime: null, + temporaryAccessEndTime: null, + temporaryRange: null, + temporaryMode: null + }); + return additionalPrivilege; + }; + + const deleteById = async ({ actorId, actor, actorOrgId, privilegeId }: TDeleteUserPrivilegeDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); + return deletedPrivilege; + }; + + const getPrivilegeDetailsById = async ({ privilegeId, actorOrgId, actor, actorId }: TGetUserPrivilegeDetailsDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + return userPrivilege; + }; + + return { + create, + updateById, + deleteById, + getPrivilegeDetailsById + }; +}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts new file mode 100644 index 000000000..efc8e81b1 --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts @@ -0,0 +1,44 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum ProjectUserAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TCreateUserPrivilegeDTO = ( + | { + permissions: unknown; + projectMembershipId: string; + name: string; + slug: string; + description?: string; + isTemporary: false; + } + | { + permissions: unknown; + projectMembershipId: string; + name: string; + slug: string; + description?: string; + isTemporary: true; + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } +) & + Omit; + +export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & + Partial<{ + permissions: unknown; + name: string; + slug: string; + description?: string; + isTemporary: boolean; + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + +export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string }; + +export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string };