diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 7b2e0ae00..bbf6adf27 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -3,6 +3,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability" import { ProjectPermissionActions, ProjectPermissionAppConnectionActions, + ProjectPermissionApprovalRequestActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, ProjectPermissionCertificateAuthorityActions, @@ -339,6 +340,11 @@ const buildAdminPermissionRules = () => { can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions); + can( + [ProjectPermissionApprovalRequestActions.Read, ProjectPermissionApprovalRequestActions.Create], + ProjectPermissionSub.ApprovalRequests + ); + return rules; }; @@ -586,6 +592,8 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.PamAccounts ); + can([ProjectPermissionApprovalRequestActions.Create], ProjectPermissionSub.ApprovalRequests); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 20f9c1f09..a1beaf071 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -224,6 +224,11 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -274,7 +279,8 @@ export enum ProjectPermissionSub { PamResources = "pam-resources", PamAccounts = "pam-accounts", PamSessions = "pam-sessions", - CertificateProfiles = "certificate-profiles" + CertificateProfiles = "certificate-profiles", + ApprovalRequests = "approval-requests" } export type SecretSubjectFields = { @@ -500,7 +506,8 @@ export type ProjectPermissionSet = | ProjectPermissionSub.CertificateProfiles | (ForcedSubject & CertificateProfileSubjectFields) ) - ]; + ] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -1105,6 +1112,12 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequests).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestActions).describe( + "Describe what action an entity can take." + ) }) ]; diff --git a/backend/src/services/approval-policy/approval-policy-service.ts b/backend/src/services/approval-policy/approval-policy-service.ts index 4cb1d147c..3207e9eeb 100644 --- a/backend/src/services/approval-policy/approval-policy-service.ts +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -34,6 +34,12 @@ import { TCreateRequestDTO, TUpdatePolicyDTO } from "./approval-policy-types"; +import { ForbiddenError } from "@casl/ability"; +import { + ProjectPermissionActions, + ProjectPermissionApprovalRequestActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; type TApprovalPolicyServiceFactoryDep = { approvalPolicyDAL: TApprovalPolicyDALFactory; @@ -367,7 +373,19 @@ export const approvalPolicyServiceFactory = ({ }, actor: OrgServiceActor ) => { - // TODO(andrey): Perm check + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Create, + ProjectPermissionSub.ApprovalRequests + ); const fac = APPROVAL_POLICY_FACTORY_MAP[policyType](policyType); @@ -466,9 +484,7 @@ export const approvalPolicyServiceFactory = ({ }; }; - const getRequestById = async (requestId: string, _actor: OrgServiceActor) => { - // TODO(andrey): Perm check - + const getRequestById = async (requestId: string, actor: OrgServiceActor) => { const request = await approvalRequestDAL.findById(requestId); if (!request) { throw new ForbiddenRequestError({ message: "Request not found" }); @@ -476,6 +492,38 @@ export const approvalPolicyServiceFactory = ({ const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + const isRequester = request.requesterId === actor.id; + + // Check if user is an eligible approver for any step + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isApprover = steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + // If user is requester or approver, allow access regardless of role permission + if (!isRequester && !isApprover) { + // Otherwise, check role permission + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: request.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); + } + return { request: { ...request, steps } }; @@ -670,11 +718,55 @@ export const approvalPolicyServiceFactory = ({ }; const listRequests = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { - // TODO(andrey): Perm check + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + const hasReadPermission = permission.can( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); const requests = await approvalRequestDAL.findByProjectId(policyType, projectId); - return { requests }; + // If user has read permission, return all requests + if (hasReadPermission) { + return { requests }; + } + + // Otherwise, filter to only requests where user is requester or approver + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const filteredRequests = []; + for (const request of requests) { + const isRequester = request.requesterId === actor.id; + + if (isRequester) { + filteredRequests.push(request); + continue; + } + + // Check if user is an eligible approver for any step + const isApprover = request.steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + if (isApprover) { + filteredRequests.push(request); + } + } + + return { requests: filteredRequests }; }; const cancelRequest = async (requestId: string, actor: OrgServiceActor) => { diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 83eb2fecd..9b88a451a 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -228,6 +228,11 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + export type IdentityManagementSubjectFields = { identityId: string; }; @@ -339,7 +344,8 @@ export enum ProjectPermissionSub { PamFolders = "pam-folders", PamResources = "pam-resources", PamAccounts = "pam-accounts", - PamSessions = "pam-sessions" + PamSessions = "pam-sessions", + ApprovalRequests = "approval-requests" } export type SecretSubjectFields = { @@ -577,6 +583,7 @@ export type ProjectPermissionSet = | (ForcedSubject & PamAccountSubjectFields) ) ] - | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; + | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 566a5f93a..a0dc268cd 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -15,6 +15,7 @@ import { import { PermissionConditionOperators, ProjectPermissionAppConnectionActions, + ProjectPermissionApprovalRequestActions, ProjectPermissionAuditLogsActions, ProjectPermissionCommitsActions, ProjectPermissionDynamicSecretActions, @@ -258,6 +259,11 @@ const PamSessionPolicyActionSchema = z.object({ [ProjectPermissionPamSessionActions.Read]: z.boolean().optional() }); +const ApprovalRequestPolicyActionSchema = z.object({ + [ProjectPermissionApprovalRequestActions.Read]: z.boolean().optional(), + [ProjectPermissionApprovalRequestActions.Create]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -468,7 +474,8 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), - [ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([]) + [ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([]), + [ProjectPermissionSub.ApprovalRequests]: ApprovalRequestPolicyActionSchema.array().default([]) }) .partial() .optional() @@ -1289,6 +1296,16 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { // Map actions to the keys defined in ApprovalPolicyActionSchema if (canRead) formVal[subject]![0][ProjectPermissionPamAccountActions.Read] = true; } + + if (subject === ProjectPermissionSub.ApprovalRequests) { + const canRead = action.includes(ProjectPermissionApprovalRequestActions.Read); + const canCreate = action.includes(ProjectPermissionApprovalRequestActions.Create); + + if (!formVal[subject]) formVal[subject] = [{}]; + + if (canRead) formVal[subject]![0][ProjectPermissionApprovalRequestActions.Read] = true; + if (canCreate) formVal[subject]![0][ProjectPermissionApprovalRequestActions.Create] = true; + } }); return formVal; @@ -1958,6 +1975,13 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.PamSessions]: { title: "Sessions", actions: [{ label: "Read", value: ProjectPermissionPamSessionActions.Read }] + }, + [ProjectPermissionSub.ApprovalRequests]: { + title: "Approval Requests", + actions: [ + { label: "Read", value: ProjectPermissionApprovalRequestActions.Read }, + { label: "Create", value: ProjectPermissionApprovalRequestActions.Create } + ] } }; @@ -1968,7 +1992,8 @@ const SharedPermissionSubjects = { [ProjectPermissionSub.Identity]: true, [ProjectPermissionSub.Project]: true, [ProjectPermissionSub.Role]: true, - [ProjectPermissionSub.Settings]: true + [ProjectPermissionSub.Settings]: true, + [ProjectPermissionSub.ApprovalRequests]: true }; const SecretsManagerPermissionSubjects = (enabled = false) => ({