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

View File

@@ -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;
};

View File

@@ -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."
)
})
];

View File

@@ -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) => {

View File

@@ -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<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
)
]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions];
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;

View File

@@ -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) => ({