mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
refactor: enhance permission handling and introduce new Permission and Policy classes
- Updated the `Guard` class to improve permission checking by utilizing the new `Permission` class. - Refactored tests in `authorize.spec.ts` to use `Permission` instances instead of strings for better type safety. - Introduced a new `permissions.spec.ts` file to test the functionality of the `Permission` and `Policy` classes. - Enhanced the `recursivelyReplacePlaceholders` utility function to support various object structures and types. - Updated middleware and controller files to align with the new permission handling structure.
This commit is contained in:
@@ -1,11 +1,95 @@
|
||||
export class Permission<Name extends string = string> {
|
||||
constructor(public name: Name) {
|
||||
this.name = name;
|
||||
import {
|
||||
s,
|
||||
type ParseOptions,
|
||||
parse,
|
||||
InvalidSchemaError,
|
||||
recursivelyReplacePlaceholders,
|
||||
} from "bknd/utils";
|
||||
import * as query from "core/object/query/object-query";
|
||||
|
||||
export const permissionOptionsSchema = s
|
||||
.strictObject({
|
||||
description: s.string(),
|
||||
filterable: s.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export type PermissionOptions = s.Static<typeof permissionOptionsSchema>;
|
||||
|
||||
export class InvalidPermissionContextError extends InvalidSchemaError {
|
||||
override name = "InvalidPermissionContextError";
|
||||
|
||||
static from(e: InvalidSchemaError) {
|
||||
return new InvalidPermissionContextError(e.schema, e.value, e.errors);
|
||||
}
|
||||
}
|
||||
|
||||
export class Permission<
|
||||
Name extends string = string,
|
||||
Options extends PermissionOptions = {},
|
||||
Context extends s.ObjectSchema = s.ObjectSchema,
|
||||
> {
|
||||
constructor(
|
||||
public name: Name,
|
||||
public options: Options = {} as Options,
|
||||
public context: Context = s.object({}) as Context,
|
||||
) {}
|
||||
|
||||
parseContext(ctx: s.Static<Context>, opts?: ParseOptions) {
|
||||
try {
|
||||
return parse(this.context, ctx, opts);
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidSchemaError) {
|
||||
throw InvalidPermissionContextError.from(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
...this.options,
|
||||
context: this.context,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const policySchema = s
|
||||
.strictObject({
|
||||
description: s.string(),
|
||||
condition: s.object({}, { default: {} }) as s.Schema<{}, query.ObjectQuery>,
|
||||
effect: s.string({ enum: ["allow", "deny", "filter"], default: "deny" }),
|
||||
filter: s.object({}, { default: {} }) as s.Schema<{}, query.ObjectQuery>,
|
||||
})
|
||||
.partial();
|
||||
export type PolicySchema = s.Static<typeof policySchema>;
|
||||
|
||||
export class Policy<Schema extends PolicySchema> {
|
||||
public content: Schema;
|
||||
|
||||
constructor(content?: Schema) {
|
||||
this.content = parse(policySchema, content ?? {}) as Schema;
|
||||
}
|
||||
|
||||
replace(context: object, vars?: Record<string, any>) {
|
||||
return vars ? recursivelyReplacePlaceholders(context, /^@([a-zA-Z_\.]+)$/, vars) : context;
|
||||
}
|
||||
|
||||
meetsCondition(context: object, vars?: Record<string, any>) {
|
||||
return query.validate(this.replace(this.content.condition!, vars), context);
|
||||
}
|
||||
|
||||
meetsFilter(subject: object, vars?: Record<string, any>) {
|
||||
return query.validate(this.replace(this.content.filter!, vars), subject);
|
||||
}
|
||||
|
||||
getFiltered<Given extends any[]>(given: Given): Given {
|
||||
return given.filter((item) => this.meetsFilter(item)) as Given;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,3 +512,38 @@ export function convertNumberedObjectToArray(obj: object): any[] | object {
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function recursivelyReplacePlaceholders(
|
||||
obj: any,
|
||||
pattern: RegExp,
|
||||
variables: Record<string, any>,
|
||||
) {
|
||||
if (typeof obj === "string") {
|
||||
// check if the entire string matches the pattern
|
||||
const match = obj.match(pattern);
|
||||
if (match && match[0] === obj && match[1]) {
|
||||
// full string match - replace with the actual value (preserving type)
|
||||
const key = match[1];
|
||||
const value = getPath(variables, key);
|
||||
return value !== undefined ? value : obj;
|
||||
}
|
||||
// partial match - use string replacement
|
||||
if (pattern.test(obj)) {
|
||||
return obj.replace(pattern, (match, key) => {
|
||||
const value = getPath(variables, key);
|
||||
// convert to string for partial replacements
|
||||
return value !== undefined ? String(value) : match;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => recursivelyReplacePlaceholders(item, pattern, variables));
|
||||
}
|
||||
if (obj && typeof obj === "object") {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
acc[key] = recursivelyReplacePlaceholders(value, pattern, variables);
|
||||
return acc;
|
||||
}, {} as object);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ export const stringIdentifier = s.string({
|
||||
});
|
||||
|
||||
export class InvalidSchemaError extends Error {
|
||||
override name = "InvalidSchemaError";
|
||||
|
||||
constructor(
|
||||
public schema: s.Schema,
|
||||
public value: unknown,
|
||||
|
||||
Reference in New Issue
Block a user