vona-module-a-rbac 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/.metadata/index.d.ts +110 -0
  3. package/dist/.metadata/index.d.ts.map +1 -0
  4. package/dist/.metadata/this.d.ts +3 -0
  5. package/dist/.metadata/this.d.ts.map +1 -0
  6. package/dist/bean/bean.rbacCatalog.d.ts +13 -0
  7. package/dist/bean/bean.rbacCatalog.d.ts.map +1 -0
  8. package/dist/bean/bean.rbacScope.d.ts +19 -0
  9. package/dist/bean/bean.rbacScope.d.ts.map +1 -0
  10. package/dist/bean/event.policyInvalidated.d.ts +11 -0
  11. package/dist/bean/event.policyInvalidated.d.ts.map +1 -0
  12. package/dist/bean/event.resolvePolicy.d.ts +7 -0
  13. package/dist/bean/event.resolvePolicy.d.ts.map +1 -0
  14. package/dist/bean/guard.rbac.d.ts +17 -0
  15. package/dist/bean/guard.rbac.d.ts.map +1 -0
  16. package/dist/bean/index.d.ts +6 -0
  17. package/dist/bean/index.d.ts.map +1 -0
  18. package/dist/config/config.d.ts +8 -0
  19. package/dist/config/config.d.ts.map +1 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +365 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/lib/index.d.ts +2 -0
  25. package/dist/lib/index.d.ts.map +1 -0
  26. package/dist/lib/rbac.d.ts +12 -0
  27. package/dist/lib/rbac.d.ts.map +1 -0
  28. package/dist/types/index.d.ts +3 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/rbac.d.ts +37 -0
  31. package/dist/types/rbac.d.ts.map +1 -0
  32. package/dist/types/scope.d.ts +21 -0
  33. package/dist/types/scope.d.ts.map +1 -0
  34. package/package.json +46 -0
  35. package/src/.metadata/index.ts +147 -0
  36. package/src/.metadata/this.ts +2 -0
  37. package/src/bean/bean.rbacCatalog.ts +95 -0
  38. package/src/bean/bean.rbacScope.ts +218 -0
  39. package/src/bean/event.policyInvalidated.ts +17 -0
  40. package/src/bean/event.resolvePolicy.ts +13 -0
  41. package/src/bean/guard.rbac.ts +102 -0
  42. package/src/bean/index.ts +5 -0
  43. package/src/config/config.ts +10 -0
  44. package/src/index.ts +3 -0
  45. package/src/lib/index.ts +1 -0
  46. package/src/lib/rbac.ts +46 -0
  47. package/src/types/index.ts +2 -0
  48. package/src/types/rbac.ts +46 -0
  49. package/src/types/scope.ts +27 -0
@@ -0,0 +1,17 @@
1
+ import { BeanEventBase, Event } from 'vona-module-a-event';
2
+
3
+ export type TypeRbacPolicyInvalidationKind = 'policy' | 'role' | 'scopeTopology';
4
+
5
+ export interface TypeEventPolicyInvalidatedData {
6
+ kind: TypeRbacPolicyInvalidationKind;
7
+ removedRoleIds?: string[];
8
+ removedScopeIds?: string[];
9
+ }
10
+
11
+ export type TypeEventPolicyInvalidatedResult = void;
12
+
13
+ @Event()
14
+ export class EventPolicyInvalidated extends BeanEventBase<
15
+ TypeEventPolicyInvalidatedData,
16
+ TypeEventPolicyInvalidatedResult
17
+ > {}
@@ -0,0 +1,13 @@
1
+ import { BeanEventBase, Event } from 'vona-module-a-event';
2
+
3
+ import type { IRbacPolicyDecision, IRbacPolicyRequest } from '../types/rbac.ts';
4
+
5
+ export type TypeEventResolvePolicyData = IRbacPolicyRequest;
6
+
7
+ export type TypeEventResolvePolicyResult = IRbacPolicyDecision | undefined;
8
+
9
+ @Event()
10
+ export class EventResolvePolicy extends BeanEventBase<
11
+ TypeEventResolvePolicyData,
12
+ TypeEventResolvePolicyResult
13
+ > {}
@@ -0,0 +1,102 @@
1
+ import type { IDecoratorGuardOptions } from 'vona-module-a-aspect';
2
+
3
+ import { Guard, GuardBase } from 'vona-module-a-aspect';
4
+
5
+ import type {
6
+ IRbacActionDescriptor,
7
+ IRbacPolicyDecision,
8
+ IRbacScopeTerm,
9
+ TypeRbacDataScope,
10
+ } from '../types/rbac.ts';
11
+
12
+ import { clearRbacDecision, createRbacAllScopeDecision, setRbacDecision } from '../lib/rbac.ts';
13
+
14
+ export interface IGuardOptionsRbac extends IDecoratorGuardOptions {
15
+ dataScope?: boolean;
16
+ dataScopeField?: string;
17
+ dataScopeMineField?: string;
18
+ actionInherit?: string;
19
+ }
20
+
21
+ @Guard<IGuardOptionsRbac>({
22
+ dataScopeField: 'departmentId',
23
+ dataScopeMineField: 'userIdOwner',
24
+ })
25
+ export class GuardRbac extends GuardBase {
26
+ async check(options: IGuardOptionsRbac): Promise<boolean> {
27
+ clearRbacDecision(this.ctx);
28
+ const action = this.bean.rbacCatalog.getAction(this.ctx.route);
29
+ if (!action) return false;
30
+ const effectiveAction = { ...action, options: { ...action.options, ...options } };
31
+ if (await this.bean.rbacScope.isUnrestricted()) {
32
+ setRbacDecision(this.ctx, createRbacAllScopeDecision(effectiveAction));
33
+ return true;
34
+ }
35
+
36
+ const decision = await this.scope.event.resolvePolicy.emit(
37
+ {
38
+ action: effectiveAction,
39
+ policyActionKey: action.actionInheritKey ?? action.actionKey,
40
+ },
41
+ async () => undefined,
42
+ );
43
+ if (!this.isValidDecision(decision, effectiveAction) || !decision.allowed) return false;
44
+ setRbacDecision(this.ctx, { ...decision, action: effectiveAction });
45
+ return true;
46
+ }
47
+
48
+ private isValidDecision(
49
+ decision: unknown,
50
+ action: IRbacActionDescriptor,
51
+ ): decision is IRbacPolicyDecision {
52
+ if (!decision || typeof decision !== 'object') return false;
53
+ const value = decision as Partial<IRbacPolicyDecision>;
54
+ if (typeof value.allowed !== 'boolean' || value.actionKey !== action.actionKey) return false;
55
+ if (!this.isValidAction(value.action, action)) return false;
56
+ if (value.revision !== undefined && typeof value.revision !== 'string') return false;
57
+ if (value.terms !== undefined && !this.isValidTerms(value.terms)) return false;
58
+ return !value.allowed || !action.options.dataScope || this.isValidTerms(value.terms);
59
+ }
60
+
61
+ private isValidAction(value: unknown, action: IRbacActionDescriptor): boolean {
62
+ if (!value || typeof value !== 'object') return false;
63
+ const descriptor = value as Partial<IRbacActionDescriptor>;
64
+ return (
65
+ descriptor.actionKey === action.actionKey &&
66
+ descriptor.actionInheritKey === action.actionInheritKey &&
67
+ descriptor.controllerBeanFullName === action.controllerBeanFullName &&
68
+ descriptor.action === action.action
69
+ );
70
+ }
71
+
72
+ private isValidTerms(value: unknown): value is IRbacScopeTerm[] {
73
+ return Array.isArray(value) && value.length > 0 && value.every(term => this.isValidTerm(term));
74
+ }
75
+
76
+ private isValidTerm(value: unknown): value is IRbacScopeTerm {
77
+ if (!value || typeof value !== 'object') return false;
78
+ const term = value as {
79
+ dataScope?: unknown;
80
+ departmentIds?: unknown;
81
+ ownerId?: unknown;
82
+ };
83
+ if (term.dataScope === 'all') return true;
84
+ if (term.dataScope === 'mine') {
85
+ return typeof term.ownerId === 'string' && term.ownerId.length > 0;
86
+ }
87
+ if (!this.isDepartmentDataScope(term.dataScope)) return false;
88
+ return (
89
+ Array.isArray(term.departmentIds) &&
90
+ term.departmentIds.length > 0 &&
91
+ term.departmentIds.every(id => typeof id === 'string' && id.length > 0)
92
+ );
93
+ }
94
+
95
+ private isDepartmentDataScope(
96
+ value: unknown,
97
+ ): value is Exclude<TypeRbacDataScope, 'all' | 'mine'> {
98
+ return ['customDepartments', 'ownDepartment', 'ownDepartmentAndDescendants'].includes(
99
+ value as Exclude<TypeRbacDataScope, 'all' | 'mine'>,
100
+ );
101
+ }
102
+ }
@@ -0,0 +1,5 @@
1
+ export * from './bean.rbacCatalog.ts';
2
+ export * from './bean.rbacScope.ts';
3
+ export * from './event.policyInvalidated.ts';
4
+ export * from './event.resolvePolicy.ts';
5
+ export * from './guard.rbac.ts';
@@ -0,0 +1,10 @@
1
+ import type { VonaApplication } from 'vona';
2
+ import type { IServiceRecord } from 'vona-module-a-bean';
3
+
4
+ export function config(_app: VonaApplication) {
5
+ return {
6
+ adapter: {
7
+ rbacScope: 'admin-rbac:rbacScopeAdapter' as keyof IServiceRecord,
8
+ },
9
+ };
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './lib/index.ts';
2
+ export * from './types/index.ts';
3
+ export * from './.metadata/index.ts';
@@ -0,0 +1 @@
1
+ export * from './rbac.ts';
@@ -0,0 +1,46 @@
1
+ import type { VonaContext } from 'vona';
2
+
3
+ import type { IRbacCapability, IRbacPolicyDecision } from '../types/rbac.ts';
4
+
5
+ export const SymbolRbacDecision = Symbol('SymbolRbacDecision');
6
+
7
+ export function createRbacAllScopeDecision(
8
+ action: IRbacPolicyDecision['action'],
9
+ ): IRbacPolicyDecision {
10
+ return { allowed: true, actionKey: action.actionKey, action, terms: [{ dataScope: 'all' }] };
11
+ }
12
+
13
+ export function createRbacCapability(key: string, allowed: boolean): IRbacCapability {
14
+ return { key, allowed };
15
+ }
16
+
17
+ export function isRbacCapability(value: unknown): value is IRbacCapability {
18
+ if (!value || typeof value !== 'object') return false;
19
+ const capability = value as Partial<IRbacCapability>;
20
+ return (
21
+ Object.keys(capability).every(key => key === 'key' || key === 'allowed') &&
22
+ typeof capability.key === 'string' &&
23
+ capability.key.length > 0 &&
24
+ typeof capability.allowed === 'boolean'
25
+ );
26
+ }
27
+
28
+ export function hasRbacAllScope(decision: IRbacPolicyDecision): boolean {
29
+ return decision.terms?.some(term => term.dataScope === 'all') === true;
30
+ }
31
+
32
+ export function rbacActionKey(controllerBeanFullName: string, action: string): string {
33
+ return `${controllerBeanFullName}#${action}`;
34
+ }
35
+
36
+ export function getRbacDecision(ctx: VonaContext): IRbacPolicyDecision | undefined {
37
+ return ctx[SymbolRbacDecision];
38
+ }
39
+
40
+ export function setRbacDecision(ctx: VonaContext, decision: IRbacPolicyDecision): void {
41
+ ctx[SymbolRbacDecision] = decision;
42
+ }
43
+
44
+ export function clearRbacDecision(ctx: VonaContext): void {
45
+ delete ctx[SymbolRbacDecision];
46
+ }
@@ -0,0 +1,2 @@
1
+ export * from './rbac.ts';
2
+ export * from './scope.ts';
@@ -0,0 +1,46 @@
1
+ import type { ContextRoute } from 'vona-module-a-web';
2
+
3
+ import type { IGuardOptionsRbac } from '../bean/guard.rbac.ts';
4
+
5
+ export type TypeRbacDataScope =
6
+ | 'all'
7
+ | 'customDepartments'
8
+ | 'ownDepartment'
9
+ | 'ownDepartmentAndDescendants'
10
+ | 'mine';
11
+
12
+ export interface IRbacActionDescriptor {
13
+ actionKey: string;
14
+ controllerBeanFullName: string;
15
+ action: string;
16
+ actionInheritKey?: string;
17
+ route: ContextRoute;
18
+ options: IGuardOptionsRbac;
19
+ }
20
+
21
+ export interface IRbacPolicyRequest {
22
+ action: IRbacActionDescriptor;
23
+ policyActionKey: string;
24
+ }
25
+
26
+ export type IRbacScopeTerm =
27
+ | { dataScope: 'all' }
28
+ | {
29
+ dataScope: 'customDepartments' | 'ownDepartment' | 'ownDepartmentAndDescendants';
30
+ departmentIds: string[];
31
+ }
32
+ | { dataScope: 'mine'; ownerId: string };
33
+
34
+ export interface IRbacPolicyDecision {
35
+ allowed: boolean;
36
+ actionKey: string;
37
+ action: IRbacActionDescriptor;
38
+ terms?: IRbacScopeTerm[];
39
+ revision?: string;
40
+ }
41
+
42
+ /** A server-derived UX hint; it never grants backend authority. */
43
+ export interface IRbacCapability {
44
+ key: string;
45
+ allowed: boolean;
46
+ }
@@ -0,0 +1,27 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IOpenapiPermissionActionRbac } from 'vona-module-a-openapi';
3
+ import type { TypeModelWhere } from 'vona-module-a-orm';
4
+
5
+ import type { IRbacActionDescriptor, IRbacPolicyDecision } from './rbac.ts';
6
+
7
+ export type IRbacScopeOwnerValues = Record<string, TableIdentity | null | undefined>;
8
+
9
+ export interface IRbacScopeAdapter {
10
+ isUnrestricted(): Promise<boolean>;
11
+ ownerValues(
12
+ action: IRbacActionDescriptor,
13
+ decision: IRbacPolicyDecision,
14
+ ): Promise<IRbacScopeOwnerValues>;
15
+ }
16
+
17
+ export interface IRbacScopeAccess {
18
+ action: IRbacActionDescriptor;
19
+ decision: IRbacPolicyDecision;
20
+ unrestricted: boolean;
21
+ ownerValues(): Record<string, TableIdentity | null | undefined>;
22
+ where(callerWhere?: TypeModelWhere<any>): TypeModelWhere<any> | undefined;
23
+ checkEntry(entry: object | undefined): void;
24
+ checkEntries(entries: readonly object[]): void;
25
+ /** A safe permission projection for Resource permission consumers. */
26
+ permissionProjection(): IOpenapiPermissionActionRbac;
27
+ }