enhance Guard and permission handling with new test cases

- Updated the `Guard` class to improve context validation and permission checks, ensuring clearer error messages for unmet conditions.
- Refactored the `Policy` and `RolePermission` classes to support default effects and better handle conditions and filters.
- Enhanced tests in `authorize.spec.ts` and `permissions.spec.ts` to cover new permission scenarios, including guest and member role behaviors.
- Added new tests for context validation in permission middleware, ensuring robust error handling for invalid contexts.
- Improved utility functions for better integration with the updated permission structure.
This commit is contained in:
dswbx
2025-10-13 21:03:49 +02:00
parent 2f88c2216c
commit 7e5c28d621
9 changed files with 317 additions and 52 deletions

View File

@@ -160,7 +160,13 @@ export class Guard {
if (!this.isEnabled()) {
return;
}
const { ctx, user, exists, role, rolePermission } = this.collect(permission, c, context);
const { ctx: _ctx, exists, role, rolePermission } = this.collect(permission, c, context);
// validate context
let ctx = Object.assign({}, _ctx);
if (permission.context) {
ctx = permission.parseContext(ctx);
}
$console.debug("guard: checking permission", {
name: permission.name,
@@ -187,32 +193,37 @@ export class Guard {
throw new GuardPermissionsException(
permission,
undefined,
"Role does not have required permission",
`Role "${role.name}" does not have required permission`,
);
}
// validate context
let ctx2 = Object.assign({}, ctx);
if (permission.context) {
ctx2 = permission.parseContext(ctx2);
}
if (rolePermission?.policies.length > 0) {
$console.debug("guard: rolePermission has policies, checking");
// set the default effect of the role permission
let allowed = rolePermission.effect === "allow";
for (const policy of rolePermission.policies) {
// skip filter policies
if (policy.content.effect === "filter") continue;
// if condition unmet or effect is deny, throw
const meets = policy.meetsCondition(ctx2);
if (!meets || (meets && policy.content.effect === "deny")) {
throw new GuardPermissionsException(
permission,
policy,
"Policy does not meet condition",
);
// if condition is met, check the effect
const meets = policy.meetsCondition(ctx);
if (meets) {
// if deny, then break early
if (policy.content.effect === "deny") {
allowed = false;
break;
// if allow, set allow but continue checking
} else if (policy.content.effect === "allow") {
allowed = true;
}
}
}
if (!allowed) {
throw new GuardPermissionsException(permission, undefined, "Policy condition unmet");
}
}
$console.debug("guard allowing", {
@@ -235,20 +246,24 @@ export class Guard {
c: GuardContext,
context?: PermissionContext<P>,
): PolicySchema["filter"] | undefined {
if (!permission.isFilterable()) return;
if (!permission.isFilterable()) {
$console.debug("getPolicyFilter: permission is not filterable, returning undefined");
return;
}
const { ctx, exists, role, rolePermission } = this.collect(permission, c, context);
const { ctx: _ctx, exists, role, rolePermission } = this.collect(permission, c, context);
// validate context
let ctx2 = Object.assign({}, ctx);
let ctx = Object.assign({}, _ctx);
if (permission.context) {
ctx2 = permission.parseContext(ctx2);
ctx = permission.parseContext(ctx);
}
if (exists && role && rolePermission && rolePermission.policies.length > 0) {
for (const policy of rolePermission.policies) {
if (policy.content.effect === "filter") {
return policy.meetsFilter(ctx2) ? policy.content.filter : undefined;
const meets = policy.meetsCondition(ctx);
return meets ? policy.content.filter : undefined;
}
}
}