RBAC Improvements

Caching improvements
Maker-Checker Implementation
This commit is contained in:
2026-10-21 07:27:47 +02:00
parent dee9098379
commit 5d7e2f74ce
25 changed files with 1269 additions and 309 deletions
+54 -5
View File
@@ -142,12 +142,14 @@ export function resolveClaimActionSet(
actions.add('add');
actions.add('update');
actions.add('export');
} else if (val === 'view') {
} else if (val === 'view' || val === 'read') {
actions.add('view');
} else if (val === 'add') {
} else if (val === 'add' || val === 'edit' || val === 'write' || val === 'create') {
// 'add' encompasses create and non-approval edits
actions.add('view');
actions.add('add');
} else if (val === 'update') {
} else if (val === 'update' || val === 'approve' || val === 'verify') {
// 'update' strictly grants approval capability (+ view and add)
actions.add('view');
actions.add('add');
actions.add('update');
@@ -173,10 +175,13 @@ export function resolveClaimActionSet(
actions.add('update');
actions.add('export');
} else if (role === 'supervisor') {
// Supervisor performs operational review and verification/approval of milestones & evidence
actions.add('view');
actions.add('add');
actions.add('update');
actions.add('export');
} else if (role === 'contributor') {
// Contributor is strictly working-level execution: creating activities, submitting evidence, etc. (NO approve/update)
actions.add('view');
actions.add('add');
} else {
@@ -294,11 +299,36 @@ export function evaluateUserPermission(
return false;
}
/**
* Evaluates Maker-Checker separation of duties constraint.
* In PFM governance, the user who created (or initiated) a record ("Maker")
* is strictly barred from approving ("Checking") their own record.
*
* @param createdBy The user identifier of the record creator (Guid, sub, email)
* @param currentUserId Current logged-in user ID (user.id or sub)
* @param currentUserSub Current logged-in user subject claim
* @param currentUserEmail Current logged-in user email
* @returns boolean true if user is the creator (Maker), false otherwise
*/
export function isRecordMaker(
createdBy?: string | null,
currentUserId?: string | null,
currentUserSub?: string | null,
currentUserEmail?: string | null
): boolean {
if (!createdBy) return false;
const creator = createdBy.trim().toLowerCase();
if (currentUserId && creator === currentUserId.trim().toLowerCase()) return true;
if (currentUserSub && creator === currentUserSub.trim().toLowerCase()) return true;
if (currentUserEmail && creator === currentUserEmail.trim().toLowerCase()) return true;
return false;
}
/**
* React hook exposing RBAC evaluation helpers for components.
*/
export function useRbac() {
const { roles, claims } = useAuth();
const { roles, claims, user, sub } = useAuth();
const permissions = useMemo(() => {
const isUnrestricted =
@@ -315,6 +345,22 @@ export function useRbac() {
const canApprove = (claim: RoleClaim): boolean => canAccess(claim, 'update');
const canExport = (claim: RoleClaim): boolean => canAccess(claim, 'export');
const isMaker = (createdBy?: string | null): boolean => {
return isRecordMaker(createdBy, user?.id, sub, user?.email);
};
const canApproveRecord = (claim: RoleClaim, createdBy?: string | null): boolean => {
if (!canApprove(claim)) return false;
// Maker-Checker policy: record creator cannot approve their own record
return !isMaker(createdBy);
};
const getApprovalRestrictionReason = (claim: RoleClaim, createdBy?: string | null): string | null => {
if (!canApprove(claim)) return 'Requires update/approval permission';
if (isMaker(createdBy)) return 'Maker-Checker policy: Record creators cannot approve their own records';
return null;
};
return {
isUnrestricted,
canAccess,
@@ -322,8 +368,11 @@ export function useRbac() {
canAdd,
canApprove,
canExport,
isMaker,
canApproveRecord,
getApprovalRestrictionReason,
};
}, [roles, claims]);
}, [roles, claims, user, sub]);
return permissions;
}