diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index bbf6adf27..561cb3bb5 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -4,6 +4,7 @@ import { ProjectPermissionActions, ProjectPermissionAppConnectionActions, ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, ProjectPermissionCertificateAuthorityActions, @@ -345,6 +346,11 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.ApprovalRequests ); + can( + [ProjectPermissionApprovalRequestGrantActions.Read, ProjectPermissionApprovalRequestGrantActions.Revoke], + ProjectPermissionSub.ApprovalRequestGrants + ); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index a1beaf071..85efd19aa 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -229,6 +229,11 @@ export enum ProjectPermissionApprovalRequestActions { Create = "create" } +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -280,7 +285,8 @@ export enum ProjectPermissionSub { PamAccounts = "pam-accounts", PamSessions = "pam-sessions", CertificateProfiles = "certificate-profiles", - ApprovalRequests = "approval-requests" + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -507,7 +513,8 @@ export type ProjectPermissionSet = | (ForcedSubject & CertificateProfileSubjectFields) ) ] - | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]; + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -1118,6 +1125,12 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequestGrants).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestGrantActions).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 3207e9eeb..10e46d3b8 100644 --- a/backend/src/services/approval-policy/approval-policy-service.ts +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -38,6 +38,7 @@ import { ForbiddenError } from "@casl/ability"; import { ProjectPermissionActions, ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; @@ -87,7 +88,7 @@ export const approvalPolicyServiceFactory = ({ if (userIdsToNotify.size === 0) return; - // TODO: Potentially link to requests in the future? + // TODO: Potentially link to requests in the future to support click redirects await notificationService.createUserNotifications( Array.from(userIdsToNotify).map((userId) => ({ userId, @@ -793,20 +794,44 @@ export const approvalPolicyServiceFactory = ({ }; const listGrants = 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 + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); const grants = await approvalRequestGrantsDAL.find({ projectId, type: policyType }); return { grants }; }; const getGrantById = async (grantId: string, actor: OrgServiceActor) => { - // TODO(andrey): Perm check - const grant = await approvalRequestGrantsDAL.findById(grantId); if (!grant) { throw new NotFoundError({ message: "Grant not found" }); } + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); + return { grant }; }; @@ -815,13 +840,25 @@ export const approvalPolicyServiceFactory = ({ { revocationReason }: { revocationReason?: string }, actor: OrgServiceActor ) => { - // TODO(andrey): Perm check - const grant = await approvalRequestGrantsDAL.findById(grantId); if (!grant) { throw new NotFoundError({ message: "Grant not found" }); } + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Revoke, + ProjectPermissionSub.ApprovalRequestGrants + ); + if (grant.status !== ApprovalRequestGrantStatus.Active) { throw new BadRequestError({ message: "Grant is not active" }); } diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 9b88a451a..0838b0c36 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -233,6 +233,11 @@ export enum ProjectPermissionApprovalRequestActions { Create = "create" } +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export type IdentityManagementSubjectFields = { identityId: string; }; @@ -345,7 +350,8 @@ export enum ProjectPermissionSub { PamResources = "pam-resources", PamAccounts = "pam-accounts", PamSessions = "pam-sessions", - ApprovalRequests = "approval-requests" + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -584,6 +590,7 @@ export type ProjectPermissionSet = ) ] | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] - | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests]; + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; 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 a0dc268cd..86b39498e 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -16,6 +16,7 @@ import { PermissionConditionOperators, ProjectPermissionAppConnectionActions, ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, ProjectPermissionAuditLogsActions, ProjectPermissionCommitsActions, ProjectPermissionDynamicSecretActions, @@ -264,6 +265,11 @@ const ApprovalRequestPolicyActionSchema = z.object({ [ProjectPermissionApprovalRequestActions.Create]: z.boolean().optional() }); +const ApprovalRequestGrantPolicyActionSchema = z.object({ + [ProjectPermissionApprovalRequestGrantActions.Read]: z.boolean().optional(), + [ProjectPermissionApprovalRequestGrantActions.Revoke]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -475,7 +481,11 @@ export const projectRoleFormSchema = z.object({ .array() .default([]), [ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([]), - [ProjectPermissionSub.ApprovalRequests]: ApprovalRequestPolicyActionSchema.array().default([]) + [ProjectPermissionSub.ApprovalRequests]: ApprovalRequestPolicyActionSchema.array().default( + [] + ), + [ProjectPermissionSub.ApprovalRequestGrants]: + ApprovalRequestGrantPolicyActionSchema.array().default([]) }) .partial() .optional() @@ -1306,6 +1316,17 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if (canRead) formVal[subject]![0][ProjectPermissionApprovalRequestActions.Read] = true; if (canCreate) formVal[subject]![0][ProjectPermissionApprovalRequestActions.Create] = true; } + + if (subject === ProjectPermissionSub.ApprovalRequestGrants) { + const canRead = action.includes(ProjectPermissionApprovalRequestGrantActions.Read); + const canRevoke = action.includes(ProjectPermissionApprovalRequestGrantActions.Revoke); + + if (!formVal[subject]) formVal[subject] = [{}]; + + if (canRead) formVal[subject]![0][ProjectPermissionApprovalRequestGrantActions.Read] = true; + if (canRevoke) + formVal[subject]![0][ProjectPermissionApprovalRequestGrantActions.Revoke] = true; + } }); return formVal; @@ -1982,6 +2003,13 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Read", value: ProjectPermissionApprovalRequestActions.Read }, { label: "Create", value: ProjectPermissionApprovalRequestActions.Create } ] + }, + [ProjectPermissionSub.ApprovalRequestGrants]: { + title: "Approval Request Grants", + actions: [ + { label: "Read", value: ProjectPermissionApprovalRequestGrantActions.Read }, + { label: "Revoke", value: ProjectPermissionApprovalRequestGrantActions.Revoke } + ] } }; @@ -1993,7 +2021,8 @@ const SharedPermissionSubjects = { [ProjectPermissionSub.Project]: true, [ProjectPermissionSub.Role]: true, [ProjectPermissionSub.Settings]: true, - [ProjectPermissionSub.ApprovalRequests]: true + [ProjectPermissionSub.ApprovalRequests]: true, + [ProjectPermissionSub.ApprovalRequestGrants]: true }; const SecretsManagerPermissionSubjects = (enabled = false) => ({