diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index 18c196409..aec3a9c97 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -1,12 +1,13 @@ -import { packRules } from "@casl/ability/extra"; +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { zpStr } from "@app/lib/zod"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,37 +19,37 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F body: z.union([ z.object({ identityId: z.string().min(1), - projectId: z.string().min(1), - // disallow empty string - slug: zpStr( - z - .string() - .max(60) - .trim() - .optional() - .default(`privilege-${slugify(alphaNumericNanoId(12))}`) - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }) - ), + projectSlug: z.string().min(1), + slug: z + .string() + .min(1) + .max(60) + .trim() + .optional() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), permissions: z.any().array(), - isPackedPermission: z.boolean().optional().default(true), + isPackedPermission: z.boolean().optional().default(false), isTemporary: z.literal(false).default(false) }), z.object({ identityId: z.string(), - projectId: z.string(), + projectSlug: z.string().min(1), slug: z .string() + .min(1) .max(60) .trim() + .optional() .default(`privilege-${slugify(alphaNumericNanoId(12))}`) .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" }), permissions: z.any().array(), isTemporary: z.literal(true), - isPackedPermission: z.boolean().optional().default(true), + isPackedPermission: z.boolean().optional().default(false), temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), temporaryAccessStartTime: z.string().datetime() @@ -77,30 +78,33 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }); server.route({ - url: "/:privilegeId", + url: "/", method: "PATCH", schema: { - params: z.object({ - privilegeId: z.string() + body: z.object({ + // disallow empty string + slug: z.string().min(1), + identityId: z.string().min(1), + projectSlug: z.string().min(1), + data: z + .object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), + isPackedPermission: z.boolean().optional().default(false), + permissions: z.any().array(), + isTemporary: z.boolean(), + temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), + temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), + temporaryAccessStartTime: z.string().datetime() + }) + .partial() }), - body: z - .object({ - // disallow empty string - slug: z - .string() - .max(60) - .trim() - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), - permissions: z.any().array(), - isPackedPermission: z.boolean().optional().default(true), - isTemporary: z.boolean(), - temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), - temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), - temporaryAccessStartTime: z.string().datetime() - }) - .partial(), response: { 200: z.object({ privilege: IdentityProjectAdditionalPrivilegeSchema @@ -109,27 +113,32 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.updateById({ + const { isPackedPermission, ...data } = req.body.data; + const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ actorId: req.permission.id, actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - permissions: req.body.permissions - ? JSON.stringify(req.body.isPackedPermission ? req.body.permissions : packRules(req.body.permissions)) - : undefined, - privilegeId: req.params.privilegeId + data: { + ...data, + permissions: data?.permissions + ? JSON.stringify(isPackedPermission ? data?.permissions : packRules(data.permissions)) + : undefined + } }); return { privilege }; } }); server.route({ - url: "/:privilegeId", + url: "/", method: "DELETE", schema: { - params: z.object({ - privilegeId: z.string() + body: z.object({ + slug: z.string().min(1), + identityId: z.string().min(1), + projectSlug: z.string().min(1) }), response: { 200: z.object({ @@ -139,23 +148,27 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.deleteById({ + const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({ actorId: req.permission.id, actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - privilegeId: req.params.privilegeId + ...req.body }); return { privilege }; } }); server.route({ - url: "/:privilegeId", + url: "/:slug", method: "GET", schema: { params: z.object({ - privilegeId: z.string() + slug: z.string() + }), + querystring: z.object({ + identityId: z.string().min(1), + projectSlug: z.string().min(1) }), response: { 200: z.object({ @@ -165,24 +178,29 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsById({ + const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({ actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, actorOrgId: req.permission.orgId, - privilegeId: req.params.privilegeId + slug: req.params.slug, + ...req.query }); return { privilege }; } }); server.route({ - url: "/permissions", - method: "POST", + url: "/", + method: "GET", schema: { - body: z.object({ - identityId: z.string(), - projectId: z.string() + querystring: z.object({ + identityId: z.string().min(1), + projectSlug: z.string().min(1), + unpacked: z + .enum(["false", "true"]) + .transform((el) => el === "true") + .default("true") }), response: { 200: z.object({ @@ -197,9 +215,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F actor: req.permission.type, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.projectId, - identityId: req.body.identityId + ...req.query }); + if (req.query.unpacked) { + return { + privileges: privileges.map(({ permissions, ...el }) => ({ + ...el, + permissions: unpackRules(permissions as PackRule>>[]) + })) + }; + } return { privileges }; } }); diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 3a919d983..81dc11a00 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -5,6 +5,7 @@ import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; @@ -21,6 +22,7 @@ import { type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectDAL: Pick; + projectDAL: Pick; permissionService: Pick; }; @@ -31,19 +33,24 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< export const identityProjectAdditionalPrivilegeServiceFactory = ({ identityProjectAdditionalPrivilegeDAL, identityProjectDAL, - permissionService + permissionService, + projectDAL }: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { const create = async ({ slug, actor, actorId, - projectId, identityId, + projectSlug, permissions: customPermission, actorOrgId, actorAuthMethod, ...dto }: TCreateIdentityPrivilegeDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); @@ -65,7 +72,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -96,19 +103,23 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ return additionalPrivilege; }; - const updateById = async ({ - privilegeId, + const updateBySlug = async ({ + projectSlug, + slug, + identityId, + data, actorOrgId, actor, actorId, - actorAuthMethod, - ...dto + actorAuthMethod }: TUpdateIdentityPrivilegeDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); - if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; - const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); - if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getProjectPermission( actor, @@ -127,23 +138,28 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); - if (dto?.slug) { + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + if (data?.slug) { const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ - slug: dto.slug, + slug: data.slug, projectMembershipId: identityProjectMembership.id }); if (existingSlug && existingSlug.id !== identityPrivilege.id) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); } - const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : identityPrivilege.isTemporary; + const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; if (isTemporary) { - const temporaryAccessStartTime = dto?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; - const temporaryRange = dto?.temporaryRange || identityPrivilege?.temporaryRange; + const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; + const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...dto, + ...data, temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); @@ -151,7 +167,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...dto, + ...data, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, @@ -161,18 +177,22 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ return additionalPrivilege; }; - const deleteById = async ({ + const deleteBySlug = async ({ actorId, + slug, + identityId, + projectSlug, actor, actorOrgId, - privilegeId, actorAuthMethod }: TDeleteIdentityPrivilegeDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); - if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; - const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); - if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getProjectPermission( actor, @@ -191,25 +211,34 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ ); const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); return deletedPrivilege; }; - const getPrivilegeDetailsById = async ({ - privilegeId, + const getPrivilegeDetailsBySlug = async ({ + projectSlug, + identityId, + slug, actorOrgId, actor, actorId, actorAuthMethod }: TGetIdentityPrivilegeDetailsDTO) => { - const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findById(privilegeId); - if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); - - const identityProjectMembership = await identityProjectDAL.findById(identityPrivilege.projectMembershipId); - if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -218,31 +247,31 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); return identityPrivilege; }; const listIdentityProjectPrivileges = async ({ - projectId, identityId, actorOrgId, actor, actorId, - actorAuthMethod + actorAuthMethod, + projectSlug }: TListIdentityPrivilegesDTO) => { - const identityProjectMembership = await identityProjectDAL.findOne({ projectId, identityId }); - if (!identityProjectMembership) throw new BadRequestError({ message: `Failed to find identity` }); + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -251,29 +280,18 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( - ActorType.IDENTITY, - identityProjectMembership.identityId, - identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ projectMembershipId: identityProjectMembership.id }); - if (!identityPrivileges) throw new BadRequestError({ message: "Identity additional privilege not found" }); return identityPrivileges; }; return { create, - updateById, - deleteById, - getPrivilegeDetailsById, + updateBySlug, + deleteBySlug, + getPrivilegeDetailsBySlug, listIdentityProjectPrivileges }; }; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts index 0d2b64cc5..88ff01d7d 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts @@ -4,19 +4,16 @@ export enum IdentityProjectAdditionalPrivilegeTemporaryMode { Relative = "relative" } -export type TCreateIdentityPrivilegeDTO = ( +export type TCreateIdentityPrivilegeDTO = { + permissions: unknown; + identityId: string; + projectSlug: string; + slug: string; +} & ( | { - permissions: unknown; - identityId: string; - projectId: string; - slug: string; isTemporary: false; } | { - permissions: unknown; - identityId: string; - projectId: string; - slug: string; isTemporary: true; temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; temporaryRange: string; @@ -25,21 +22,33 @@ export type TCreateIdentityPrivilegeDTO = ( ) & Omit; -export type TUpdateIdentityPrivilegeDTO = { privilegeId: string } & Omit & - Partial<{ - permissions: unknown; - slug: string; - isTemporary: boolean; - temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; - temporaryRange: string; - temporaryAccessStartTime: string; - }>; +export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit< + TProjectPermission, + "projectId" +> & { + data: Partial<{ + permissions: unknown; + slug: string; + isTemporary: boolean; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + }; -export type TDeleteIdentityPrivilegeDTO = Omit & { privilegeId: string }; +export type TDeleteIdentityPrivilegeDTO = Omit & { + slug: string; + identityId: string; + projectSlug: string; +}; -export type TGetIdentityPrivilegeDetailsDTO = Omit & { privilegeId: string }; +export type TGetIdentityPrivilegeDetailsDTO = Omit & { + slug: string; + identityId: string; + projectSlug: string; +}; export type TListIdentityPrivilegesDTO = Omit & { identityId: string; - projectId: string; + projectSlug: string; }; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index dda4bee50..a8072c0d3 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -144,6 +144,8 @@ export const permissionDALFactory = (db: TDbClient) => { } ] }); + + if (!permission?.[0]) return undefined; // when introducting cron mode change it here const activeRoles = permission?.[0]?.roles?.filter( ({ isTemporary, temporaryAccessEndTime }) => @@ -155,9 +157,7 @@ export const permissionDALFactory = (db: TDbClient) => { !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ); - return permission?.[0] - ? { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges } - : undefined; + return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); } @@ -176,6 +176,11 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) .join( // Join the Project table to later select orgId TableName.Project, @@ -191,9 +196,28 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - 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.IdentityProjectAdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessEndTime") + ); const permission = sqlNestRelationships({ data: docs, @@ -218,16 +242,44 @@ export const permissionDALFactory = (db: TDbClient) => { permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) + }, + { + key: "identityApId", + label: "additionalPrivileges" as const, + mapper: ({ + identityApId, + identityApPermissions, + identityApIsTemporary, + identityApTemporaryMode, + identityApTemporaryRange, + identityApTemporaryAccessEndTime, + identityApTemporaryAccessStartTime + }) => ({ + id: identityApId, + permissions: identityApPermissions, + temporaryRange: identityApTemporaryRange, + temporaryMode: identityApTemporaryMode, + temporaryAccessEndTime: identityApTemporaryAccessEndTime, + temporaryAccessStartTime: identityApTemporaryAccessStartTime, + isTemporary: identityApIsTemporary + }) } ] }); + if (!permission?.[0]) return undefined; + // when introducting cron mode change it here 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], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; } catch (error) { throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); } diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index d836e88df..f4e423797 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -238,8 +238,16 @@ export const permissionServiceFactory = ({ throw new UnauthorizedError({ name: "You are not a member of this organization" }); } + const rolePermissions = + identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + return { - permission: buildProjectPermission(identityProjectPermission.roles), + permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), membership: identityProjectPermission, hasRole: (role: string) => identityProjectPermission.roles.findIndex( diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c33f62029..7745a8a04 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -560,6 +560,7 @@ export const registerRoutes = async ( projectRoleDAL }); const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({ + projectDAL, identityProjectAdditionalPrivilegeDAL, permissionService, identityProjectDAL diff --git a/backend/src/server/routes/v2/identity-project-router.ts b/backend/src/server/routes/v2/identity-project-router.ts index 81a59e995..d170f87e6 100644 --- a/backend/src/server/routes/v2/identity-project-router.ts +++ b/backend/src/server/routes/v2/identity-project-router.ts @@ -156,20 +156,6 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider) identityId: z.string(), createdAt: z.date(), updatedAt: z.date(), - additionalPrivileges: z.array( - z.object({ - id: z.string(), - name: z.string(), - slug: z.string(), - description: z.string().optional().nullable(), - isTemporary: z.boolean(), - temporaryMode: z.string().optional().nullable(), - temporaryRange: z.string().nullable().optional(), - temporaryAccessStartTime: z.date().nullable().optional(), - temporaryAccessEndTime: z.date().nullable().optional(), - createdAt: z.date() - }) - ), roles: z.array( z.object({ id: z.string(), diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index b6de21d05..e932d2068 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -47,29 +47,7 @@ export const identityProjectDALFactory = (db: TDbClient) => { db.ref("isTemporary").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), - db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), - db.ref("name").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApName"), - db.ref("description").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApDescription"), - db.ref("slug").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApSlug"), - db - .ref("temporaryMode") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryMode"), - db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), - db.ref("createdAt").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApCreatedAt"), - db - .ref("temporaryRange") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryRange"), - db - .ref("temporaryAccessStartTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessStartTime"), - db - .ref("temporaryAccessEndTime") - .withSchema(TableName.IdentityProjectAdditionalPrivilege) - .as("identityApTemporaryAccessEndTime") + db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole) ); const members = sqlNestRelationships({ @@ -113,33 +91,6 @@ export const identityProjectDALFactory = (db: TDbClient) => { temporaryAccessStartTime, isTemporary }) - }, - { - label: "additionalPrivileges" as const, - key: "identityApId", - mapper: ({ - identityApId, - identityApDescription, - identityApName, - identityApSlug, - identityApIsTemporary, - identityApTemporaryMode, - identityApTemporaryRange, - identityApCreatedAt, - identityApTemporaryAccessEndTime, - identityApTemporaryAccessStartTime - }) => ({ - id: identityApId, - name: identityApName, - description: identityApDescription, - slug: identityApSlug, - temporaryRange: identityApTemporaryRange, - temporaryMode: identityApTemporaryMode, - temporaryAccessEndTime: identityApTemporaryAccessEndTime, - temporaryAccessStartTime: identityApTemporaryAccessStartTime, - isTemporary: identityApIsTemporary, - createdAt: identityApCreatedAt - }) } ] });