Update permissions handling and enhance Guard functionality

- Bump `jsonv-ts` dependency to 0.8.6.
- Refactor permission checks in the `Guard` class to improve context validation and error handling.
- Update tests to reflect changes in permission handling, ensuring robust coverage for new scenarios.
- Introduce new test cases for data permissions, enhancing overall test coverage and reliability.
This commit is contained in:
dswbx
2025-10-21 16:44:08 +02:00
parent 0347efa592
commit 38902ebcba
20 changed files with 859 additions and 153 deletions

View File

@@ -1,5 +1,5 @@
import { Exception } from "core/errors";
import { $console, type s } from "bknd/utils";
import { $console, mergeObject, type s } from "bknd/utils";
import type { Permission, PermissionContext } from "auth/authorize/Permission";
import type { Context } from "hono";
import type { ServerEnv } from "modules/Controller";
@@ -232,41 +232,85 @@ export class Guard {
});
}
getPolicyFilter<P extends Permission<any, any, any, any>>(
filters<P extends Permission<any, any, any, any>>(
permission: P,
c: GuardContext,
context: PermissionContext<P>,
): PolicySchema["filter"] | undefined;
getPolicyFilter<P extends Permission<any, any, undefined, any>>(
permission: P,
c: GuardContext,
): PolicySchema["filter"] | undefined;
getPolicyFilter<P extends Permission<any, any, any, any>>(
);
filters<P extends Permission<any, any, undefined, any>>(permission: P, c: GuardContext);
filters<P extends Permission<any, any, any, any>>(
permission: P,
c: GuardContext,
context?: PermissionContext<P>,
): PolicySchema["filter"] | undefined {
) {
if (!permission.isFilterable()) {
$console.debug("getPolicyFilter: permission is not filterable, returning undefined");
return;
throw new GuardPermissionsException(permission, undefined, "Permission is not filterable");
}
const { ctx: _ctx, exists, role, rolePermission } = this.collect(permission, c, context);
const {
ctx: _ctx,
exists,
role,
user,
rolePermission,
} = this.collect(permission, c, context);
// validate context
let ctx = Object.assign({}, _ctx);
let ctx = Object.assign(
{
user,
},
_ctx,
);
if (permission.context) {
ctx = permission.parseContext(ctx);
ctx = permission.parseContext(ctx, {
coerceDropUnknown: false,
});
}
const filters: PolicySchema["filter"][] = [];
const policies: Policy[] = [];
if (exists && role && rolePermission && rolePermission.policies.length > 0) {
for (const policy of rolePermission.policies) {
if (policy.content.effect === "filter") {
const meets = policy.meetsCondition(ctx);
return meets ? policy.content.filter : undefined;
if (meets) {
policies.push(policy);
filters.push(policy.getReplacedFilter(ctx));
}
}
}
}
return;
const filter = filters.length > 0 ? mergeObject({}, ...filters) : undefined;
return {
filters,
filter,
policies,
merge: (givenFilter: object | undefined) => {
return mergeObject(givenFilter ?? {}, filter ?? {});
},
matches: (subject: object | object[], opts?: { throwOnError?: boolean }) => {
const subjects = Array.isArray(subject) ? subject : [subject];
if (policies.length > 0) {
for (const policy of policies) {
for (const subject of subjects) {
if (!policy.meetsFilter(subject, ctx)) {
if (opts?.throwOnError) {
throw new GuardPermissionsException(
permission,
policy,
"Policy filter not met",
);
}
return false;
}
}
}
}
return true;
},
};
}
}

View File

@@ -54,6 +54,8 @@ export class Permission<
}
parseContext(ctx: ContextValue, opts?: ParseOptions) {
// @todo: allow additional properties
if (!this.context) return ctx;
try {
return this.context ? parse(this.context!, ctx, opts) : undefined;
} catch (e) {

View File

@@ -21,8 +21,15 @@ export class Policy<Schema extends PolicySchema = PolicySchema> {
}) as Schema;
}
replace(context: object, vars?: Record<string, any>) {
return vars ? recursivelyReplacePlaceholders(context, /^@([a-zA-Z_\.]+)$/, vars) : context;
replace(context: object, vars?: Record<string, any>, fallback?: any) {
return vars
? recursivelyReplacePlaceholders(context, /^@([a-zA-Z_\.]+)$/, vars, fallback)
: context;
}
getReplacedFilter(context: object, fallback?: any) {
if (!this.content.filter) return context;
return this.replace(this.content.filter!, context, fallback);
}
meetsCondition(context: object, vars?: Record<string, any>) {