From a9e6c229d0c67a313de2fc28df405968305ddf3b Mon Sep 17 00:00:00 2001 From: = Date: Wed, 9 Oct 2024 00:31:27 +0530 Subject: [PATCH] feat: completed migration of permission v1 to v2. Pending intense testing --- ...20241008172622_project-permission-split.ts | 99 +++++++++++++++++++ ...ity-project-additional-privilege-router.ts | 15 ++- .../src/ee/routes/v1/project-role-router.ts | 11 ++- .../v1/user-additional-privilege-router.ts | 53 ++++++++-- ...ty-project-additional-privilege-service.ts | 13 +-- .../services/permission/project-permission.ts | 47 ++++++--- ...oject-user-additional-privilege-service.ts | 42 ++++++-- backend/src/server/routes/sanitizedSchemas.ts | 3 +- .../routes/santizedSchemas/permission.ts | 11 +++ .../project-role/project-role-service.ts | 2 +- .../mutation.tsx | 8 +- .../queries.tsx | 8 +- .../projectUserAdditionalPrivilege/types.tsx | 17 +++- .../SpecificPrivilegeSection.tsx | 30 +++--- 14 files changed, 278 insertions(+), 81 deletions(-) create mode 100644 backend/src/db/migrations/20241008172622_project-permission-split.ts create mode 100644 backend/src/server/routes/santizedSchemas/permission.ts diff --git a/backend/src/db/migrations/20241008172622_project-permission-split.ts b/backend/src/db/migrations/20241008172622_project-permission-split.ts new file mode 100644 index 000000000..508c2ea42 --- /dev/null +++ b/backend/src/db/migrations/20241008172622_project-permission-split.ts @@ -0,0 +1,99 @@ +/* eslint-disable no-await-in-loop */ +import { unpackRules } from "@casl/ability/extra"; +import { Knex } from "knex"; + +import { + backfillPermissionV1SchemaToV2Schema, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; + +import { TableName } from "../schemas"; + +const CHUNK_SIZE = 1000; +export async function up(knex: Knex): Promise { + const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version"); + if (!hasVersion) { + await knex.schema.alterTable(TableName.ProjectRoles, (t) => { + t.integer("version").defaultTo(1).notNullable(); + }); + + const docs = await knex(TableName.ProjectRoles).select("*"); + const updatedDocs = docs + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (updatedDocs.length) { + for (let i = 0; i < updatedDocs.length; i += CHUNK_SIZE) { + const chunk = updatedDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge(); + } + } + + const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*"); + const updatedIdentityPrivilegesDocs = identityPrivileges + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) && + !permissionString.includes(ProjectPermissionSub.SecretFolders) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (updatedIdentityPrivilegesDocs.length) { + for (let i = 0; i < updatedIdentityPrivilegesDocs.length; i += CHUNK_SIZE) { + const chunk = updatedIdentityPrivilegesDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge(); + } + } + + const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*"); + const updatedUserPriviegeDocs = userPrivileges + .filter((i) => { + const permissionString = JSON.stringify(i.permissions || []); + return ( + !permissionString.includes(ProjectPermissionSub.SecretImports) && + !permissionString.includes(ProjectPermissionSub.DynamicSecrets) && + !permissionString.includes(ProjectPermissionSub.SecretFolders) + ); + }) + .map((el) => ({ + ...el, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions)))) + })); + if (docs.length) { + for (let i = 0; i < updatedUserPriviegeDocs.length; i += CHUNK_SIZE) { + const chunk = updatedUserPriviegeDocs.slice(i, i + CHUNK_SIZE); + await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge(); + } + } + } +} + +export async function down(knex: Knex): Promise { + const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version"); + if (hasVersion) { + await knex.schema.alterTable(TableName.ProjectRoles, (t) => { + t.dropColumn("version"); + }); + + // permission change can be ignored + } +} 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 2e7fd60d3..91a4402fb 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 @@ -4,6 +4,7 @@ import ms from "ms"; import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; @@ -79,7 +80,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - permissions: JSON.stringify(packRules(permission)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) }); return { privilege }; } @@ -159,7 +162,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: true, - permissions: JSON.stringify(packRules(permission)) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) }); return { privilege }; } @@ -244,7 +249,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F projectSlug: req.body.projectSlug, data: { ...updatedInfo, - permissions: permission ? JSON.stringify(packRules(permission)) : undefined + permissions: permission + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error this is valid ts + JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission))) + : undefined } }); return { privilege }; diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index 0af8df43d..9edd29030 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -3,7 +3,10 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; -import { ProjectPermissionV1Schema } from "@app/ee/services/permission/project-permission"; +import { + backfillPermissionV1SchemaToV2Schema, + ProjectPermissionV1Schema +} from "@app/ee/services/permission/project-permission"; import { PROJECT_ROLE } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -61,7 +64,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { projectSlug: req.params.projectSlug, data: { ...req.body, - permissions: JSON.stringify(packRules(req.body.permissions)) + permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) } }); return { role }; @@ -122,7 +125,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { roleId: req.params.roleId, data: { ...req.body, - permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined + permissions: req.body.permissions + ? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions))) + : undefined } }); 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 7225caecf..1ba7e0e33 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -1,13 +1,16 @@ +import { packRules } from "@casl/ability/extra"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { z } from "zod"; import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; +import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ProjectSpecificPrivilegePermissionSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { @@ -31,7 +34,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions + ) }), response: { 200: z.object({ @@ -49,7 +54,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr ...req.body, slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), isTemporary: false, - permissions: JSON.stringify(req.body.permissions) + permissions: JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions.subject, + conditions: req.body.permissions.conditions + })) + ) + ) + ) }); return { privilege }; } @@ -75,7 +90,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr }) .optional() .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions + ), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), @@ -104,7 +121,17 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr ...req.body, slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`, isTemporary: true, - permissions: JSON.stringify(req.body.permissions) + permissions: JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions.subject, + conditions: req.body.permissions.conditions + })) + ) + ) + ) }); return { privilege }; } @@ -131,7 +158,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr message: "Slug must be a valid slug" }) .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), - permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + permissions: ProjectSpecificPrivilegePermissionSchema.describe( + PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions + ).optional(), isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), temporaryMode: z .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) @@ -160,7 +189,19 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, ...req.body, - permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, + permissions: req.body.permissions + ? JSON.stringify( + packRules( + backfillPermissionV1SchemaToV2Schema( + req.body.permissions.actions.map((action) => ({ + action, + subject: req.body.permissions!.subject, + conditions: req.body.permissions!.conditions + })) + ) + ) + ) + : undefined, privilegeId: req.params.privilegeId }); return { privilege }; 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 7456648bb..b5e5037b0 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,10 +1,10 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import ms from "ms"; -import { z } from "zod"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; 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"; @@ -33,15 +33,6 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< >; // TODO(akhilmhdh): move this to more centralized -export const UnpackedPermissionSchema = z.object({ - subject: z - .union([z.string().min(1), z.string().array()]) - .transform((el) => (typeof el !== "string" ? el[0] : el)) - .optional(), - action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), - conditions: z.unknown().optional(), - inverted: z.boolean().optional() -}); const unpackPermissions = (permissions: unknown) => UnpackedPermissionSchema.array().parse( @@ -204,7 +195,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); return { ...additionalPrivilege, - permissions: unpackPermissions(additionalPrivilege.permissions) }; }; @@ -325,7 +315,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); return identityPrivileges.map((el) => ({ ...el, - permissions: unpackPermissions(el.permissions) })); }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 050a02f6b..1803137b6 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -1,9 +1,8 @@ import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { z } from "zod"; -import { TableName } from "@app/db/schemas"; import { conditionsMatcher } from "@app/lib/casl"; -import { BadRequestError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types"; @@ -752,17 +751,35 @@ export const isAtLeastAsPrivilegedWorkspace = ( }; /* eslint-enable */ -export const SecretV2SubjectFieldMapper = (arg: string) => { - switch (arg) { - case "environment": - return null; - case "secretPath": - return null; - case "secretName": - return `${TableName.SecretV2}.key`; - case "secretTags": - return `${TableName.SecretTag}.slug`; - default: - throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` }); - } +export const backfillPermissionV1SchemaToV2Schema = (data: z.infer[]) => { + const formattedData = UnpackedPermissionSchema.array().parse(data); + const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets); + + // this means the folder permission as readonly is set + const hasReadOnlyFolder = formattedData.filter((el) => el.subject === ProjectPermissionSub.SecretFolders); + const secretImportPolicies = secretSubjects.map(({ subject, ...el }) => ({ + ...el, + subject: ProjectPermissionSub.SecretImports as const + })); + + const secretFolderPolicies = secretSubjects.map(({ subject, ...el }) => ({ + ...el, + subject: ProjectPermissionSub.DynamicSecrets + })); + + const dynamicSecretPolicies = secretSubjects.map(({ subject, ...el }) => ({ + ...el, + action: el.action.includes(ProjectPermissionActions.Edit) + ? [...el.action, ProjectPermissionDynamicSecretActions.Lease] + : el.action, + subject: ProjectPermissionSub.DynamicSecrets + })); + + 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 ea46e132c..8db487289 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,11 +1,13 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, @@ -26,6 +28,11 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< typeof projectUserAdditionalPrivilegeServiceFactory >; +const unpackPermissions = (permissions: unknown) => + UnpackedPermissionSchema.array().parse( + unpackRules((permissions || []) as PackRule>>[]) + ); + export const projectUserAdditionalPrivilegeServiceFactory = ({ projectUserAdditionalPrivilegeDAL, projectMembershipDAL, @@ -82,7 +89,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) }); - return additionalPrivilege; + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; }; const updateById = async ({ @@ -131,7 +141,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) }); - return additionalPrivilege; + + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; } const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { @@ -142,7 +156,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ temporaryRange: null, temporaryMode: null }); - return additionalPrivilege; + return { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + }; }; const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { @@ -165,7 +182,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); - return deletedPrivilege; + return { + ...deletedPrivilege, + permissions: unpackPermissions(deletedPrivilege.permissions) + }; }; const getPrivilegeDetailsById = async ({ @@ -193,7 +213,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - return userPrivilege; + return { + ...userPrivilege, + permissions: unpackPermissions(userPrivilege.permissions) + }; }; const listPrivileges = async ({ @@ -219,7 +242,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ userId: projectMembership.userId, projectId: projectMembership.projectId }); - return userPrivileges; + return userPrivileges.map((el) => ({ + ...el, + permissions: unpackPermissions(el.permissions) + })); }; return { diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index aa53ed50e..2bf170fc0 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -9,9 +9,10 @@ import { SecretApprovalPoliciesSchema, UsersSchema } from "@app/db/schemas"; -import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { UnpackedPermissionSchema } from "./santizedSchemas/permission"; + // sometimes the return data must be santizied to avoid leaking important values // always prefer pick over omit in zod export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({ diff --git a/backend/src/server/routes/santizedSchemas/permission.ts b/backend/src/server/routes/santizedSchemas/permission.ts new file mode 100644 index 000000000..94c1dc57c --- /dev/null +++ b/backend/src/server/routes/santizedSchemas/permission.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const UnpackedPermissionSchema = z.object({ + subject: z + .union([z.string().min(1), z.string().array()]) + .transform((el) => (typeof el !== "string" ? el[0] : el)) + .optional(), + action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)), + conditions: z.unknown().optional(), + inverted: z.boolean().optional() +}); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 49c4f81c9..e9c8cb1da 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { ProjectMembershipRole } from "@app/db/schemas"; -import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, @@ -10,6 +9,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { ActorAuthMethod } from "../auth/auth-type"; import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal"; diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx index fb21a425e..2ee6519f6 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx @@ -1,4 +1,3 @@ -import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -16,10 +15,7 @@ export const useCreateProjectUserAdditionalPrivilege = () => { return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ mutationFn: async (dto) => { - const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", { - ...dto, - permissions: packRules(dto.permissions) - }); + const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto); return data.privilege; }, onSuccess: (_, { projectMembershipId }) => { @@ -35,7 +31,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => { mutationFn: async (dto) => { const { data } = await apiRequest.patch( `/api/v1/additional-privilege/users/${dto.privilegeId}`, - { ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined } + dto ); return data.privilege; }, diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx index 41c9a0dcc..261821c1f 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx @@ -1,4 +1,3 @@ -import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; @@ -18,10 +17,7 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => { } = await apiRequest.get<{ privilege: Omit & { permissions: unknown }; }>(`/api/v1/additional-privilege/users/${privilegeId}`); - return { - ...privilege, - permissions: unpackRules(privilege.permissions as PackRule[]) - }; + return privilege; }; export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => { @@ -44,7 +40,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => { }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); return privileges.map((el) => ({ ...el, - permissions: unpackRules(el.permissions as PackRule[]) + permissions: el.permissions as TProjectPermission[] })); } }); diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx index b757a07ab..da40005c2 100644 --- a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx @@ -4,6 +4,15 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode { Relative = "relative" } +export type TProjectSpecificPrivilegePermission = { + conditions: { + environment: string; + secretPath?: { $glob: string }; + }; + actions: string[]; + subject: string; +}; + export type TProjectUserPrivilege = { projectMembershipId: string; slug: string; @@ -12,21 +21,21 @@ export type TProjectUserPrivilege = { updatedAt: Date; permissions?: TProjectPermission[]; } & ( - | { + | { isTemporary: true; temporaryMode: string; temporaryRange: string; temporaryAccessStartTime: string; temporaryAccessEndTime?: string; } - | { + | { isTemporary: false; temporaryMode?: null; temporaryRange?: null; temporaryAccessStartTime?: null; temporaryAccessEndTime?: null; } - ); +); export type TCreateProjectUserPrivilegeDTO = { projectMembershipId: string; @@ -35,7 +44,7 @@ export type TCreateProjectUserPrivilegeDTO = { temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; temporaryRange?: string; temporaryAccessStartTime?: string; - permissions: TProjectPermission[]; + permissions: TProjectSpecificPrivilegePermission; }; export type TUpdateProjectUserPrivlegeDTO = { diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 7894d0e78..51581de4d 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -184,20 +184,20 @@ export const SpecificPrivilegeSecretForm = ({ { action: ProjectPermissionActions.Delete, allowed: data.delete }, { action: ProjectPermissionActions.Edit, allowed: data.edit } ]; - const conditions: Record = { environment: data.environmentSlug }; + const conditions: { environment: string; secretPath?: { $glob: string } } = { + environment: data.environmentSlug + }; if (data.secretPath) { conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) }; } await updateUserPrivilege.mutateAsync({ privilegeId: privilege.id, ...data.temporaryAccess, - permissions: actions - .filter(({ allowed }) => allowed) - .map(({ action }) => ({ - action, - subject: [ProjectPermissionSub.Secrets], - conditions - })), + permissions: { + subject: ProjectPermissionSub.Secrets, + conditions, + actions: actions.filter((i) => i.allowed).map((i) => i.action) + }, projectMembershipId: privilege.projectMembershipId }); createNotification({ @@ -642,15 +642,13 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => { if (createUserPrivilege.isLoading) return; try { await createUserPrivilege.mutateAsync({ - permissions: [ - { - action: ProjectPermissionActions.Read, - subject: [ProjectPermissionSub.Secrets], - conditions: { - environment: currentWorkspace?.environments?.[0].slug - } + permissions: { + actions: [ProjectPermissionActions.Read], + subject: ProjectPermissionSub.Secrets, + conditions: { + environment: currentWorkspace?.environments?.[0].slug || "" } - ], + }, projectMembershipId: membershipId }); createNotification({