approval request permissions

This commit is contained in:
x032205
2025-12-07 22:16:31 -05:00
parent fcd8e28b76
commit 2fe316c81c
5 changed files with 157 additions and 12 deletions
@@ -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;
};
@@ -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<ProjectPermissionSub.CertificateProfiles> & 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."
)
})
];
@@ -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) => {