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 { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionAppConnectionActions, ProjectPermissionAppConnectionActions,
ProjectPermissionApprovalRequestActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCertificateAuthorityActions, ProjectPermissionCertificateAuthorityActions,
@@ -339,6 +340,11 @@ const buildAdminPermissionRules = () => {
can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions); can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions);
can(
[ProjectPermissionApprovalRequestActions.Read, ProjectPermissionApprovalRequestActions.Create],
ProjectPermissionSub.ApprovalRequests
);
return rules; return rules;
}; };
@@ -586,6 +592,8 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.PamAccounts ProjectPermissionSub.PamAccounts
); );
can([ProjectPermissionApprovalRequestActions.Create], ProjectPermissionSub.ApprovalRequests);
return rules; return rules;
}; };
@@ -224,6 +224,11 @@ export enum ProjectPermissionPamSessionActions {
// Terminate = "terminate" // Terminate = "terminate"
} }
export enum ProjectPermissionApprovalRequestActions {
Read = "read",
Create = "create"
}
export const isCustomProjectRole = (slug: string) => export const isCustomProjectRole = (slug: string) =>
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
@@ -274,7 +279,8 @@ export enum ProjectPermissionSub {
PamResources = "pam-resources", PamResources = "pam-resources",
PamAccounts = "pam-accounts", PamAccounts = "pam-accounts",
PamSessions = "pam-sessions", PamSessions = "pam-sessions",
CertificateProfiles = "certificate-profiles" CertificateProfiles = "certificate-profiles",
ApprovalRequests = "approval-requests"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -500,7 +506,8 @@ export type ProjectPermissionSet =
| ProjectPermissionSub.CertificateProfiles | ProjectPermissionSub.CertificateProfiles
| (ForcedSubject<ProjectPermissionSub.CertificateProfiles> & CertificateProfileSubjectFields) | (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_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
@@ -1105,6 +1112,12 @@ const GeneralPermissionSchema = [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe(
"Describe what action an entity can take." "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, TCreateRequestDTO,
TUpdatePolicyDTO TUpdatePolicyDTO
} from "./approval-policy-types"; } from "./approval-policy-types";
import { ForbiddenError } from "@casl/ability";
import {
ProjectPermissionActions,
ProjectPermissionApprovalRequestActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
type TApprovalPolicyServiceFactoryDep = { type TApprovalPolicyServiceFactoryDep = {
approvalPolicyDAL: TApprovalPolicyDALFactory; approvalPolicyDAL: TApprovalPolicyDALFactory;
@@ -367,7 +373,19 @@ export const approvalPolicyServiceFactory = ({
}, },
actor: OrgServiceActor 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); const fac = APPROVAL_POLICY_FACTORY_MAP[policyType](policyType);
@@ -466,9 +484,7 @@ export const approvalPolicyServiceFactory = ({
}; };
}; };
const getRequestById = async (requestId: string, _actor: OrgServiceActor) => { const getRequestById = async (requestId: string, actor: OrgServiceActor) => {
// TODO(andrey): Perm check
const request = await approvalRequestDAL.findById(requestId); const request = await approvalRequestDAL.findById(requestId);
if (!request) { if (!request) {
throw new ForbiddenRequestError({ message: "Request not found" }); throw new ForbiddenRequestError({ message: "Request not found" });
@@ -476,6 +492,38 @@ export const approvalPolicyServiceFactory = ({
const steps = await approvalRequestDAL.findStepsByRequestId(requestId); 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 { return {
request: { ...request, steps } request: { ...request, steps }
}; };
@@ -670,11 +718,55 @@ export const approvalPolicyServiceFactory = ({
}; };
const listRequests = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { 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); 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) => { const cancelRequest = async (requestId: string, actor: OrgServiceActor) => {
@@ -228,6 +228,11 @@ export enum ProjectPermissionPamSessionActions {
// Terminate = "terminate" // Terminate = "terminate"
} }
export enum ProjectPermissionApprovalRequestActions {
Read = "read",
Create = "create"
}
export type IdentityManagementSubjectFields = { export type IdentityManagementSubjectFields = {
identityId: string; identityId: string;
}; };
@@ -339,7 +344,8 @@ export enum ProjectPermissionSub {
PamFolders = "pam-folders", PamFolders = "pam-folders",
PamResources = "pam-resources", PamResources = "pam-resources",
PamAccounts = "pam-accounts", PamAccounts = "pam-accounts",
PamSessions = "pam-sessions" PamSessions = "pam-sessions",
ApprovalRequests = "approval-requests"
} }
export type SecretSubjectFields = { export type SecretSubjectFields = {
@@ -577,6 +583,7 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields) | (ForcedSubject<ProjectPermissionSub.PamAccounts> & PamAccountSubjectFields)
) )
] ]
| [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]
| [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests];
export type TProjectPermission = MongoAbility<ProjectPermissionSet>; export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
@@ -15,6 +15,7 @@ import {
import { import {
PermissionConditionOperators, PermissionConditionOperators,
ProjectPermissionAppConnectionActions, ProjectPermissionAppConnectionActions,
ProjectPermissionApprovalRequestActions,
ProjectPermissionAuditLogsActions, ProjectPermissionAuditLogsActions,
ProjectPermissionCommitsActions, ProjectPermissionCommitsActions,
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
@@ -258,6 +259,11 @@ const PamSessionPolicyActionSchema = z.object({
[ProjectPermissionPamSessionActions.Read]: z.boolean().optional() [ProjectPermissionPamSessionActions.Read]: z.boolean().optional()
}); });
const ApprovalRequestPolicyActionSchema = z.object({
[ProjectPermissionApprovalRequestActions.Read]: z.boolean().optional(),
[ProjectPermissionApprovalRequestActions.Create]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({ const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(), read: z.boolean().optional(),
create: z.boolean().optional() create: z.boolean().optional()
@@ -468,7 +474,8 @@ export const projectRoleFormSchema = z.object({
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([]) [ProjectPermissionSub.PamSessions]: PamSessionPolicyActionSchema.array().default([]),
[ProjectPermissionSub.ApprovalRequests]: ApprovalRequestPolicyActionSchema.array().default([])
}) })
.partial() .partial()
.optional() .optional()
@@ -1289,6 +1296,16 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
// Map actions to the keys defined in ApprovalPolicyActionSchema // Map actions to the keys defined in ApprovalPolicyActionSchema
if (canRead) formVal[subject]![0][ProjectPermissionPamAccountActions.Read] = true; 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; return formVal;
@@ -1958,6 +1975,13 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.PamSessions]: { [ProjectPermissionSub.PamSessions]: {
title: "Sessions", title: "Sessions",
actions: [{ label: "Read", value: ProjectPermissionPamSessionActions.Read }] 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.Identity]: true,
[ProjectPermissionSub.Project]: true, [ProjectPermissionSub.Project]: true,
[ProjectPermissionSub.Role]: true, [ProjectPermissionSub.Role]: true,
[ProjectPermissionSub.Settings]: true [ProjectPermissionSub.Settings]: true,
[ProjectPermissionSub.ApprovalRequests]: true
}; };
const SecretsManagerPermissionSubjects = (enabled = false) => ({ const SecretsManagerPermissionSubjects = (enabled = false) => ({