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 91a4402fb..d342f95ce 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,4 +1,3 @@ -import { packRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; @@ -82,7 +81,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F isTemporary: false, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + permissions: backfillPermissionV1SchemaToV2Schema(permission) }); return { privilege }; } @@ -164,7 +163,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F isTemporary: true, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + permissions: backfillPermissionV1SchemaToV2Schema(permission) }); return { privilege }; } @@ -249,10 +248,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F projectSlug: req.body.projectSlug, data: { ...updatedInfo, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts permissions: permission ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts - JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + backfillPermissionV1SchemaToV2Schema(permission) : undefined } }); diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index b79ced69b..dc53a125b 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -68,9 +68,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, data: { ...req.body, - permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) } }); + return { role }; } }); @@ -129,7 +130,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { data: { ...req.body, permissions: req.body.permissions - ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) + ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) : undefined } }); @@ -241,6 +242,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { }, roleSlug: req.params.slug }); + return { role }; } }); diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index 06f79fc32..e58a6335b 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,4 +1,3 @@ -import { packRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; @@ -70,7 +69,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr projectMembershipId: req.body.projectMembershipId, ...req.body.type, slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: req.body.permissions }); return { privilege }; } @@ -137,7 +136,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr permissions: req.body.permissions ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error this is valid ts - JSON.stringify(packRules(req.body.permissions)) + req.body.permissions : undefined, privilegeId: req.params.privilegeId }); diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 8de129ae8..5df03f68d 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -1,4 +1,3 @@ -import { packRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; @@ -78,7 +77,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F identityId: req.body.identityId, ...req.body.type, slug: req.body.slug || slugify(alphaNumericNanoId(8)), - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: req.body.permissions }); return { privilege }; } @@ -149,11 +148,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F data: { ...req.body, ...req.body.type, - permissions: req.body.permissions - ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - JSON.stringify(packRules(req.body.permissions)) - : undefined + permissions: req.body.permissions || undefined } }); return { privilege }; diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 2469903bd..5132b0fc9 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { packRules } from "@casl/ability/extra"; import ms from "ms"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; @@ -61,14 +62,18 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityId, identityProjectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -78,11 +83,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ }); if (existingSlug) throw new BadRequestError({ message: "Additional privilege with provided slug already exists" }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ projectMembershipId: identityProjectMembership.id, slug, - permissions: customPermission + permissions: packedPermission }); return { @@ -95,7 +101,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ projectMembershipId: identityProjectMembership.id, slug, - permissions: customPermission, + permissions: packedPermission, isTemporary: true, temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, temporaryRange: dto.temporaryRange, @@ -133,14 +139,18 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityProjectMembership.identityId, identityProjectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -154,12 +164,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ } const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; if (isTemporary) { const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { slug: data.slug, - permissions: data.permissions, + permissions: packedPermission, isTemporary: data.isTemporary, temporaryRange: data.temporaryRange, temporaryMode: data.temporaryMode, @@ -174,7 +185,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { slug: data.slug, - permissions: data.permissions, + permissions: packedPermission, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts index 9825b0ef3..aab6b8510 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types.ts @@ -1,11 +1,13 @@ import { TProjectPermission } from "@app/lib/types"; +import { TProjectPermissionV2Schema } from "../permission/project-permission"; + export enum IdentityProjectAdditionalPrivilegeTemporaryMode { Relative = "relative" } export type TCreateIdentityPrivilegeDTO = { - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; identityId: string; projectId: string; slug: string; @@ -24,7 +26,7 @@ export type TCreateIdentityPrivilegeDTO = { export type TUpdateIdentityPrivilegeByIdDTO = { id: string } & Omit & { data: Partial<{ - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; slug: string; isTemporary: boolean; temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; 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 cd6565660..4811eb52a 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 @@ -1,5 +1,5 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, unpackRules } from "@casl/ability/extra"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; @@ -70,14 +70,18 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityId, identityProjectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -87,11 +91,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ projectMembershipId: identityProjectMembership.id, slug, - permissions: customPermission + permissions: packedPermission }); return { ...additionalPrivilege, @@ -103,7 +108,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ projectMembershipId: identityProjectMembership.id, slug, - permissions: customPermission, + permissions: packedPermission, isTemporary: true, temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, temporaryRange: dto.temporaryRange, @@ -142,14 +147,19 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); - const { permission: identityRolePermission } = await permissionService.getProjectPermission( + + const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( ActorType.IDENTITY, identityProjectMembership.identityId, identityProjectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -172,23 +182,29 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ } const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + + const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined; if (isTemporary) { const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...data, + slug: data.slug, + permissions: packedPermission, + isTemporary: data.isTemporary, + temporaryRange: data.temporaryRange, + temporaryMode: data.temporaryMode, temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); return { ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) }; } const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { - ...data, + slug: data.slug, + permissions: packedPermission, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, 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 88ff01d7d..6a0ecee5f 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 @@ -1,11 +1,13 @@ import { TProjectPermission } from "@app/lib/types"; +import { TProjectPermissionV2Schema } from "../permission/project-permission"; + export enum IdentityProjectAdditionalPrivilegeTemporaryMode { Relative = "relative" } export type TCreateIdentityPrivilegeDTO = { - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; identityId: string; projectSlug: string; slug: string; @@ -27,7 +29,7 @@ export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; pr "projectId" > & { data: Partial<{ - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; slug: string; isTemporary: boolean; temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 031c9f1a2..947e7e83a 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -21,7 +21,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ secretVersioning: true, pitRecovery: false, ipAllowlisting: false, - rbac: false, + rbac: true, customRateLimits: false, customAlerts: false, auditLogs: false, diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index f18c27888..e86dab4da 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -420,6 +420,8 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ ...GeneralPermissionSchema ]); +export type TProjectPermissionV2Schema = z.infer; + const buildAdminPermissionRules = () => { const { can, rules } = new AbilityBuilder>(createMongoAbility); @@ -757,8 +759,11 @@ export const isAtLeastAsPrivilegedWorkspace = ( }; /* eslint-enable */ -export const backfillPermissionV1SchemaToV2Schema = (data: z.infer[]) => { - const formattedData = UnpackedPermissionSchema.array().parse(data); +export const backfillPermissionV1SchemaToV2Schema = ( + data: z.infer[], + dontRemoveReadFolderPermission?: boolean +) => { + let formattedData = UnpackedPermissionSchema.array().parse(data); const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets); // this means the folder permission as readonly is set @@ -802,13 +807,15 @@ export const backfillPermissionV1SchemaToV2Schema = (data: z.infer i.subject !== ProjectPermissionSub.SecretFolders) - .concat( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error this is valid ts - secretImportPolicies, - dynamicSecretPolicies, - hasReadOnlyFolder.length ? [] : secretFolderPolicies - ); + if (!dontRemoveReadFolderPermission) { + formattedData = formattedData.filter((i) => i.subject !== ProjectPermissionSub.SecretFolders); + } + + return formattedData.concat( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + secretImportPolicies, + dynamicSecretPolicies, + hasReadOnlyFolder.length ? [] : secretFolderPolicies + ); }; 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 4c5bd36eb..cadcd610a 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 @@ -1,5 +1,5 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; -import { PackRule, unpackRules } from "@casl/ability/extra"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; @@ -62,14 +62,18 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: entityPermission } = await permissionService.getProjectPermission( + const { permission: targetUserPermission } = await permissionService.getProjectPermission( ActorType.USER, projectMembership.userId, projectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, entityPermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -81,12 +85,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (existingSlug) throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` }); + const packedPermission = JSON.stringify(packRules(customPermission)); if (!dto.isTemporary) { const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ userId: projectMembership.userId, projectId: projectMembership.projectId, slug, - permissions: customPermission + permissions: packedPermission }); return { ...additionalPrivilege, @@ -99,7 +104,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ projectId: projectMembership.projectId, userId: projectMembership.userId, slug, - permissions: customPermission, + permissions: packedPermission, isTemporary: true, temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, temporaryRange: dto.temporaryRange, @@ -142,14 +147,18 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); - const { permission: entityPermission } = await permissionService.getProjectPermission( + const { permission: targetUserPermission } = await permissionService.getProjectPermission( ActorType.USER, projectMembership.userId, projectMembership.projectId, actorAuthMethod, actorOrgId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, entityPermission); + + // we need to validate that the privilege given is not higher than the assigning users permission + // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules + targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -164,12 +173,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ } const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; + + const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions)); if (isTemporary) { const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { slug: dto.slug, - permissions: dto.permissions, + permissions: packedPermission, isTemporary: dto.isTemporary, temporaryRange: dto.temporaryRange, temporaryMode: dto.temporaryMode, @@ -185,7 +196,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { slug: dto.slug, - permissions: dto.permissions, + permissions: packedPermission, isTemporary: false, temporaryAccessStartTime: null, temporaryAccessEndTime: null, 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 index 572474270..cfcf75872 100644 --- 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 @@ -1,18 +1,20 @@ import { TProjectPermission } from "@app/lib/types"; +import { TProjectPermissionV2Schema } from "../permission/project-permission"; + export enum ProjectUserAdditionalPrivilegeTemporaryMode { Relative = "relative" } export type TCreateUserPrivilegeDTO = ( | { - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; projectMembershipId: string; slug: string; isTemporary: false; } | { - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; projectMembershipId: string; slug: string; isTemporary: true; @@ -25,7 +27,7 @@ export type TCreateUserPrivilegeDTO = ( export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & Partial<{ - permissions: unknown; + permissions: TProjectPermissionV2Schema[]; slug: string; isTemporary: boolean; temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 9c43e9bd9..74b830c6d 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -7,6 +7,7 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; +import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -36,7 +37,7 @@ import { import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal"; type TProjectMembershipServiceFactoryDep = { - permissionService: Pick; + permissionService: Pick; smtpService: TSmtpService; projectBotDAL: TProjectBotDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; @@ -262,6 +263,21 @@ export const projectMembershipServiceFactory = ({ throw new ForbiddenRequestError({ message: "Forbidden member update" }); } + for await (const { role: requestedRoleChange } of roles) { + const { permission: rolePermission } = await permissionService.getProjectPermissionByRole( + requestedRoleChange, + projectId + ); + + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + + if (!hasRequiredPriviledges) { + throw new ForbiddenRequestError({ + message: `Failed to change to a more privileged role ${requestedRoleChange}` + }); + } + } + // validate custom roles input const customInputRoles = roles.filter( ({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole) diff --git a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx index 4bb591376..e5a6adf4d 100644 --- a/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx +++ b/frontend/src/views/Project/MemberDetailsPage/components/MemberProjectAdditionalPrivilegeSection/MembershipProjectAdditionalPrivilegeModifySection.tsx @@ -94,7 +94,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({ privilegeId || "" ); - console.log(privilegeDetails); const { permission } = useProjectPermission(); const isMemberEditDisabled = permission.cannot( ProjectPermissionActions.Edit, diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx index d717162d3..25a390ac5 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.tsx @@ -179,6 +179,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { permissions.forEach((permission) => { const { subject: caslSub, action, conditions, inverted } = permission; const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub; + if (!action.length) return; if ( [