@aooth/arbac 0.1.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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.cjs +476 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.mts +282 -0
- package/dist/index.mjs +455 -0
- package/package.json +56 -0
- package/scripts/codegen.mjs +157 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { TArbacRole, TArbacRule } from "@aooth/arbac-core";
|
|
2
|
+
export * from "@aooth/arbac-core";
|
|
3
|
+
|
|
4
|
+
//#region src/define-role.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A privilege function produces an array of rules when called.
|
|
7
|
+
* Created by `definePrivilege()` or manually.
|
|
8
|
+
*/
|
|
9
|
+
type TPrivilegeFunction<TUserAttrs, TScope> = () => TArbacRule<TUserAttrs, TScope>[];
|
|
10
|
+
/**
|
|
11
|
+
* Fluent builder produced by {@link defineRole}.
|
|
12
|
+
*
|
|
13
|
+
* All methods return `this` so they can be chained. Rules are emitted in the order
|
|
14
|
+
* the corresponding `.allow()`, `.deny()`, and `.use()` calls were made — this matters
|
|
15
|
+
* for engines that respect rule order (deny-first / first-match-wins variants).
|
|
16
|
+
*/
|
|
17
|
+
interface RoleBuilder<TUserAttrs, TScope> {
|
|
18
|
+
/**
|
|
19
|
+
* Set the role id (required). Calling `.id()` more than once overwrites the
|
|
20
|
+
* previous value — the last call wins.
|
|
21
|
+
*/
|
|
22
|
+
id(id: string): RoleBuilder<TUserAttrs, TScope>;
|
|
23
|
+
/** Set the human-readable role name. Optional. */
|
|
24
|
+
name(name: string): RoleBuilder<TUserAttrs, TScope>;
|
|
25
|
+
/** Set the role description. Optional. */
|
|
26
|
+
describe(description: string): RoleBuilder<TUserAttrs, TScope>;
|
|
27
|
+
/**
|
|
28
|
+
* Append an allow rule for a (resource, action) pair, with an optional scope
|
|
29
|
+
* function that derives a scope object from user attrs.
|
|
30
|
+
*/
|
|
31
|
+
allow(resource: string, action: string, scope?: (attrs: TUserAttrs, userId: string) => TScope): RoleBuilder<TUserAttrs, TScope>;
|
|
32
|
+
/** Append a deny rule (`effect: 'deny'`) for a (resource, action) pair. */
|
|
33
|
+
deny(resource: string, action: string): RoleBuilder<TUserAttrs, TScope>;
|
|
34
|
+
/**
|
|
35
|
+
* Splice in rules from one or more privilege functions (see
|
|
36
|
+
* {@link definePrivilege}). Privileges are expanded inline in call order.
|
|
37
|
+
*
|
|
38
|
+
* Two overloads:
|
|
39
|
+
* 1. **Strict** — all privileges share the role's `TScope`. Preserves
|
|
40
|
+
* bidirectional inference of `TUserAttrs` and `TScope` into bare-generic
|
|
41
|
+
* privilege calls like `allowTableWrite("tasks", { scope: (attrs) => … })`.
|
|
42
|
+
* 2. **Variadic tuple** — privileges may carry independent per-arg scope
|
|
43
|
+
* shapes (e.g. `ArbacDbScope<Task>` next to `ArbacDbScope<Comment>`).
|
|
44
|
+
* Used when typed-table privileges don't structurally match the role
|
|
45
|
+
* pin. Runtime storage type-erases through a cast.
|
|
46
|
+
*/
|
|
47
|
+
use(...privileges: TPrivilegeFunction<TUserAttrs, TScope>[]): RoleBuilder<TUserAttrs, TScope>;
|
|
48
|
+
use<TScopes extends readonly unknown[]>(...privileges: { [K in keyof TScopes]: TPrivilegeFunction<TUserAttrs, TScopes[K]> }): RoleBuilder<TUserAttrs, TScope>;
|
|
49
|
+
/**
|
|
50
|
+
* Finalize and return a plain {@link TArbacRole} object suitable for
|
|
51
|
+
* `Arbac.registerRole`. Throws if `.id()` was never called.
|
|
52
|
+
*/
|
|
53
|
+
build(): TArbacRole<TUserAttrs, TScope>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Start building a new role.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const editor = defineRole<MyAttrs, MyScope>()
|
|
60
|
+
* .id("editor")
|
|
61
|
+
* .name("Editor")
|
|
62
|
+
* .use(allowTableWrite("articles"))
|
|
63
|
+
* .deny("articles", "publish")
|
|
64
|
+
* .build();
|
|
65
|
+
*/
|
|
66
|
+
declare function defineRole<TUserAttrs extends object = object, TScope extends object = object>(): RoleBuilder<TUserAttrs, TScope>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/define-privilege.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* Define a reusable privilege factory.
|
|
71
|
+
*
|
|
72
|
+
* Double-call pattern: first call pins TUserAttrs/TScope generics,
|
|
73
|
+
* second call infers TArgs from the factory function.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const canManageUsers = definePrivilege<MyAttrs, MyScope>()(
|
|
77
|
+
* (scope: (attrs: MyAttrs, userId: string) => MyScope) => [
|
|
78
|
+
* { resource: "users", action: "read", scope },
|
|
79
|
+
* { resource: "users", action: "update", scope },
|
|
80
|
+
* ]
|
|
81
|
+
* );
|
|
82
|
+
*
|
|
83
|
+
* defineRole<MyAttrs, MyScope>()
|
|
84
|
+
* .id("manager")
|
|
85
|
+
* .use(canManageUsers((attrs) => ({ dept: attrs.dept })))
|
|
86
|
+
* .build();
|
|
87
|
+
*/
|
|
88
|
+
declare function definePrivilege<TUserAttrs extends object = object, TScope extends object = object>(): <TArgs extends unknown[]>(factory: (...args: TArgs) => TArbacRule<TUserAttrs, TScope>[]) => ((...args: TArgs) => TPrivilegeFunction<TUserAttrs, TScope>);
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/db-privileges.d.ts
|
|
91
|
+
type ScopeFn<TUserAttrs, TScope> = (attrs: TUserAttrs, userId: string) => TScope;
|
|
92
|
+
interface ScopeOpts<TUserAttrs, TScope> {
|
|
93
|
+
scope?: ScopeFn<TUserAttrs, TScope>;
|
|
94
|
+
}
|
|
95
|
+
/** Read-side actions on an `AsDbController` table: `query`, `pages`, `getOne`, `getOneComposite`, `meta`, `metaForm`. */
|
|
96
|
+
declare function allowTableRead<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
97
|
+
/** All actions on an `AsDbController` table: read + `insert`, `update`, `replace`, `remove`, `removeComposite`. */
|
|
98
|
+
declare function allowTableWrite<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
99
|
+
/** One or more declarative actions on the same table, sharing a scope. */
|
|
100
|
+
declare function allowTableAction<TUserAttrs extends object = object, TScope extends object = object>(resource: string, name: string | string[], opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/scope/types.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* Mongo-style field projection: `{ field: 0 | 1 }`.
|
|
105
|
+
* - All 1s = inclusion mode (only listed fields allowed)
|
|
106
|
+
* - All 0s = exclusion mode (listed fields denied, rest allowed)
|
|
107
|
+
* - Mixing 0 and 1 in a single projection is invalid.
|
|
108
|
+
*
|
|
109
|
+
* Structurally compatible with @uniqu/core SelectExpr in object form.
|
|
110
|
+
*/
|
|
111
|
+
type TProjection = Record<string, 0 | 1>;
|
|
112
|
+
/**
|
|
113
|
+
* A filter expression compatible with @uniqu/core FilterExpr.
|
|
114
|
+
* Comparison leaf: `{ field: value }` or `{ field: { $op: value } }`
|
|
115
|
+
* Logical branch: `{ $or: [...] }` | `{ $and: [...] }` | `{ $not: ... }`
|
|
116
|
+
*/
|
|
117
|
+
type TScopeFilter = Record<string, unknown>;
|
|
118
|
+
/**
|
|
119
|
+
* Per-control policy in a resource scope.
|
|
120
|
+
*
|
|
121
|
+
* Used inside `ArbacDbScope.controls` (in `@aooth/arbac-moost`) to gate
|
|
122
|
+
* Uniquery URL controls (`$with`, `$groupBy`, `$having`, …) on a per-role basis.
|
|
123
|
+
*
|
|
124
|
+
* Semantics:
|
|
125
|
+
* - **silence** (key absent / `undefined`) — no opinion → allow.
|
|
126
|
+
* - `true` — explicit allow (same effect as silence; useful for documentation).
|
|
127
|
+
* - `false` — explicit deny; using the control fails 403.
|
|
128
|
+
* - `readonly string[]` — whitelist mode; only the listed values are allowed.
|
|
129
|
+
* E.g., `{ $with: ['comments'] }` permits `?$with=comments` but rejects
|
|
130
|
+
* `?$with=tasks` with 403.
|
|
131
|
+
*
|
|
132
|
+
* Whitelist arrays are supported only for controls whose value is a list of
|
|
133
|
+
* named entities (notably `$with` for relation names and `$groupBy` for
|
|
134
|
+
* column names). For other controls (e.g. `$having`) only boolean gates are
|
|
135
|
+
* supported in v1; supplying a whitelist throws at scope-evaluation time.
|
|
136
|
+
*/
|
|
137
|
+
type ControlGate = boolean | readonly string[];
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/scope/projection.d.ts
|
|
140
|
+
type TProjectionMode = "include" | "exclude" | "empty";
|
|
141
|
+
/**
|
|
142
|
+
* Determine whether a projection is in inclusion mode (all 1s),
|
|
143
|
+
* exclusion mode (all 0s), or empty (`{}`, no restriction).
|
|
144
|
+
*
|
|
145
|
+
* @throws when a single projection mixes 0 and 1 values
|
|
146
|
+
*/
|
|
147
|
+
declare function getProjectionMode(proj: TProjection): TProjectionMode;
|
|
148
|
+
/**
|
|
149
|
+
* Check whether a dot-path field is allowed by a projection.
|
|
150
|
+
*
|
|
151
|
+
* Inclusion mode: a field is allowed if it, any of its parents, or any of its children
|
|
152
|
+
* is explicitly listed.
|
|
153
|
+
* Exclusion mode: a field is allowed unless it or any of its parents is excluded.
|
|
154
|
+
* Empty projection: every field is allowed.
|
|
155
|
+
*/
|
|
156
|
+
declare function isFieldAllowed(field: string, projection: TProjection): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Combines projections from multiple RBAC role grants under additive semantics:
|
|
159
|
+
* more roles = broader access. Each projection represents a set of allowed fields:
|
|
160
|
+
* include-mode `{a:1}` allows `{a}`; exclude-mode `{a:0}` allows `universe \ {a}`;
|
|
161
|
+
* empty `{}` allows the universe.
|
|
162
|
+
*
|
|
163
|
+
* A field is effectively allowed if any input grants it. Equivalently, a field
|
|
164
|
+
* stays excluded only if (a) no include-mode role grants it explicitly, AND (b)
|
|
165
|
+
* every exclude-mode role excludes it.
|
|
166
|
+
*
|
|
167
|
+
* Output mode:
|
|
168
|
+
* - All-include input → include-mode result (union of include keys)
|
|
169
|
+
* - At least one exclude-mode → exclude-mode result (or `{}` if no fields excluded)
|
|
170
|
+
* - Empty input or any universal grant `{}` → `{}` (universe)
|
|
171
|
+
*
|
|
172
|
+
* Within a single projection, mixing 1 and 0 keys is an error (call sites should
|
|
173
|
+
* normalize first). Across projections, mixing modes is supported and resolves
|
|
174
|
+
* via the additive rule above.
|
|
175
|
+
*
|
|
176
|
+
* @throws when a single projection mixes 1 and 0 keys, or contains an invalid value
|
|
177
|
+
*/
|
|
178
|
+
declare function unionProjections(...projections: TProjection[]): TProjection;
|
|
179
|
+
/**
|
|
180
|
+
* Restrict a desired projection to only fields allowed by an access-control projection.
|
|
181
|
+
*
|
|
182
|
+
* The result is the intersection of the two projections: only fields that pass both
|
|
183
|
+
* `desired` and `accessControl` survive. Either side may be empty (unrestricted), in
|
|
184
|
+
* which case the other side is returned. Mixed include/exclude modes are normalized
|
|
185
|
+
* to a single result projection.
|
|
186
|
+
*
|
|
187
|
+
* @param desired - the projection the caller asked for
|
|
188
|
+
* @param accessControl - the projection allowed by RBAC
|
|
189
|
+
*/
|
|
190
|
+
declare function restrictProjection(desired: TProjection, accessControl: TProjection): TProjection;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/scope/filter.d.ts
|
|
193
|
+
/**
|
|
194
|
+
* Merge multiple scope filters into a single filter using `$or` semantics.
|
|
195
|
+
*
|
|
196
|
+
* In RBAC, if multiple roles grant access with different filters,
|
|
197
|
+
* the user can see records matching ANY of them — hence `$or`.
|
|
198
|
+
*
|
|
199
|
+
* Behaviour:
|
|
200
|
+
* - Empty input → `undefined` (no filter)
|
|
201
|
+
* - Any empty filter → `undefined` (one role grants unrestricted access)
|
|
202
|
+
* - Single filter → returned as-is
|
|
203
|
+
* - All filters single-keyed on the same primitive field → `{ field: { $in: [...] } }`
|
|
204
|
+
* - Otherwise → `{ $or: [...] }`
|
|
205
|
+
*
|
|
206
|
+
* @returns the merged filter, or `undefined` for unrestricted access
|
|
207
|
+
*/
|
|
208
|
+
declare function mergeScopeFilters(scopes: TScopeFilter[]): TScopeFilter | undefined;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/scope/controls.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Union per-role `controls` policies. Additive RBAC semantics: more roles =
|
|
213
|
+
* broader access, mirroring how `mergeScopeFilters` and `unionProjections`
|
|
214
|
+
* combine other parts of an `ArbacDbScope`.
|
|
215
|
+
*
|
|
216
|
+
* Resolution per control key:
|
|
217
|
+
* 1. Collect each scope's value for the key (including `undefined` for
|
|
218
|
+
* silent scopes — i.e., scopes that have a `controls` map but no entry
|
|
219
|
+
* for this key).
|
|
220
|
+
* 2. If any value is `undefined` or `true` → return `true` (full allow).
|
|
221
|
+
* Silence and explicit allow both mean "this role has no objection".
|
|
222
|
+
* 3. Else (every scope is `false` or `string[]`):
|
|
223
|
+
* - If all are `false` → return `false` (full deny).
|
|
224
|
+
* - Else collect every `string[]` into a single sorted, de-duplicated
|
|
225
|
+
* array; return that array. (Roles that say `false` don't contribute
|
|
226
|
+
* positively; roles with whitelists do.)
|
|
227
|
+
*
|
|
228
|
+
* Scopes that omit the `controls` map entirely are treated as silent on
|
|
229
|
+
* EVERY key — i.e., they grant unrestricted control usage. As a result, if
|
|
230
|
+
* even a single scope in the input lacks a `controls` map, the result is
|
|
231
|
+
* always `{}` (no restrictions).
|
|
232
|
+
*
|
|
233
|
+
* Whitelist support is restricted to {@link WHITELISTABLE_CONTROLS}. Passing
|
|
234
|
+
* a whitelist for any other control throws — the gate cannot be enforced
|
|
235
|
+
* meaningfully in v1, so the misuse is rejected loudly at scope-merge time.
|
|
236
|
+
*
|
|
237
|
+
* @returns merged policy keyed by control name. Keys absent from the result
|
|
238
|
+
* mean "no opinion → fully allowed". The caller should treat a
|
|
239
|
+
* missing key the same as `true`.
|
|
240
|
+
*
|
|
241
|
+
* @throws when a non-whitelistable control is given a `readonly string[]` gate.
|
|
242
|
+
*/
|
|
243
|
+
declare function unionControlsPolicy(scopes: ReadonlyArray<{
|
|
244
|
+
controls?: Record<string, ControlGate>;
|
|
245
|
+
}>): Record<string, ControlGate>;
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/codegen/extract.d.ts
|
|
248
|
+
interface TResourceActionMap {
|
|
249
|
+
/** Map of resource string → Set of action strings */
|
|
250
|
+
resources: Map<string, Set<string>>;
|
|
251
|
+
/** All unique resource strings */
|
|
252
|
+
allResources: Set<string>;
|
|
253
|
+
/** All unique action strings */
|
|
254
|
+
allActions: Set<string>;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extract all resource and action strings from role definitions.
|
|
258
|
+
*
|
|
259
|
+
* @param roles - Array of built role definitions
|
|
260
|
+
* @param options.includeWildcards - If false (default), skip entries containing `*` or `**`
|
|
261
|
+
*/
|
|
262
|
+
declare function extractResourceActions(roles: TArbacRole<unknown, unknown>[], options?: {
|
|
263
|
+
includeWildcards?: boolean;
|
|
264
|
+
}): TResourceActionMap;
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/codegen/generate.d.ts
|
|
267
|
+
interface TCodegenOptions {
|
|
268
|
+
/** Name for the resource union type. Default: "Resource" */
|
|
269
|
+
resourceTypeName?: string;
|
|
270
|
+
/** Name for the action union type. Default: "Action" */
|
|
271
|
+
actionTypeName?: string;
|
|
272
|
+
/** Whether to generate a resource-to-actions map type. Default: true */
|
|
273
|
+
resourceActionMap?: boolean;
|
|
274
|
+
/** Header comment. Default: "// Auto-generated by @aooth/arbac codegen" */
|
|
275
|
+
header?: string;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Generate TypeScript source defining union types for resources and actions.
|
|
279
|
+
*/
|
|
280
|
+
declare function generateResourceTypes(map: TResourceActionMap, options?: TCodegenOptions): string;
|
|
281
|
+
//#endregion
|
|
282
|
+
export { type ControlGate, type RoleBuilder, type TCodegenOptions, type TPrivilegeFunction, type TProjection, type TProjectionMode, type TResourceActionMap, type TScopeFilter, allowTableAction, allowTableRead, allowTableWrite, definePrivilege, defineRole, extractResourceActions, generateResourceTypes, getProjectionMode, isFieldAllowed, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { TArbacRole, TArbacRule } from "@aooth/arbac-core";
|
|
2
|
+
export * from "@aooth/arbac-core";
|
|
3
|
+
|
|
4
|
+
//#region src/define-role.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A privilege function produces an array of rules when called.
|
|
7
|
+
* Created by `definePrivilege()` or manually.
|
|
8
|
+
*/
|
|
9
|
+
type TPrivilegeFunction<TUserAttrs, TScope> = () => TArbacRule<TUserAttrs, TScope>[];
|
|
10
|
+
/**
|
|
11
|
+
* Fluent builder produced by {@link defineRole}.
|
|
12
|
+
*
|
|
13
|
+
* All methods return `this` so they can be chained. Rules are emitted in the order
|
|
14
|
+
* the corresponding `.allow()`, `.deny()`, and `.use()` calls were made — this matters
|
|
15
|
+
* for engines that respect rule order (deny-first / first-match-wins variants).
|
|
16
|
+
*/
|
|
17
|
+
interface RoleBuilder<TUserAttrs, TScope> {
|
|
18
|
+
/**
|
|
19
|
+
* Set the role id (required). Calling `.id()` more than once overwrites the
|
|
20
|
+
* previous value — the last call wins.
|
|
21
|
+
*/
|
|
22
|
+
id(id: string): RoleBuilder<TUserAttrs, TScope>;
|
|
23
|
+
/** Set the human-readable role name. Optional. */
|
|
24
|
+
name(name: string): RoleBuilder<TUserAttrs, TScope>;
|
|
25
|
+
/** Set the role description. Optional. */
|
|
26
|
+
describe(description: string): RoleBuilder<TUserAttrs, TScope>;
|
|
27
|
+
/**
|
|
28
|
+
* Append an allow rule for a (resource, action) pair, with an optional scope
|
|
29
|
+
* function that derives a scope object from user attrs.
|
|
30
|
+
*/
|
|
31
|
+
allow(resource: string, action: string, scope?: (attrs: TUserAttrs, userId: string) => TScope): RoleBuilder<TUserAttrs, TScope>;
|
|
32
|
+
/** Append a deny rule (`effect: 'deny'`) for a (resource, action) pair. */
|
|
33
|
+
deny(resource: string, action: string): RoleBuilder<TUserAttrs, TScope>;
|
|
34
|
+
/**
|
|
35
|
+
* Splice in rules from one or more privilege functions (see
|
|
36
|
+
* {@link definePrivilege}). Privileges are expanded inline in call order.
|
|
37
|
+
*
|
|
38
|
+
* Two overloads:
|
|
39
|
+
* 1. **Strict** — all privileges share the role's `TScope`. Preserves
|
|
40
|
+
* bidirectional inference of `TUserAttrs` and `TScope` into bare-generic
|
|
41
|
+
* privilege calls like `allowTableWrite("tasks", { scope: (attrs) => … })`.
|
|
42
|
+
* 2. **Variadic tuple** — privileges may carry independent per-arg scope
|
|
43
|
+
* shapes (e.g. `ArbacDbScope<Task>` next to `ArbacDbScope<Comment>`).
|
|
44
|
+
* Used when typed-table privileges don't structurally match the role
|
|
45
|
+
* pin. Runtime storage type-erases through a cast.
|
|
46
|
+
*/
|
|
47
|
+
use(...privileges: TPrivilegeFunction<TUserAttrs, TScope>[]): RoleBuilder<TUserAttrs, TScope>;
|
|
48
|
+
use<TScopes extends readonly unknown[]>(...privileges: { [K in keyof TScopes]: TPrivilegeFunction<TUserAttrs, TScopes[K]> }): RoleBuilder<TUserAttrs, TScope>;
|
|
49
|
+
/**
|
|
50
|
+
* Finalize and return a plain {@link TArbacRole} object suitable for
|
|
51
|
+
* `Arbac.registerRole`. Throws if `.id()` was never called.
|
|
52
|
+
*/
|
|
53
|
+
build(): TArbacRole<TUserAttrs, TScope>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Start building a new role.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const editor = defineRole<MyAttrs, MyScope>()
|
|
60
|
+
* .id("editor")
|
|
61
|
+
* .name("Editor")
|
|
62
|
+
* .use(allowTableWrite("articles"))
|
|
63
|
+
* .deny("articles", "publish")
|
|
64
|
+
* .build();
|
|
65
|
+
*/
|
|
66
|
+
declare function defineRole<TUserAttrs extends object = object, TScope extends object = object>(): RoleBuilder<TUserAttrs, TScope>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/define-privilege.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* Define a reusable privilege factory.
|
|
71
|
+
*
|
|
72
|
+
* Double-call pattern: first call pins TUserAttrs/TScope generics,
|
|
73
|
+
* second call infers TArgs from the factory function.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const canManageUsers = definePrivilege<MyAttrs, MyScope>()(
|
|
77
|
+
* (scope: (attrs: MyAttrs, userId: string) => MyScope) => [
|
|
78
|
+
* { resource: "users", action: "read", scope },
|
|
79
|
+
* { resource: "users", action: "update", scope },
|
|
80
|
+
* ]
|
|
81
|
+
* );
|
|
82
|
+
*
|
|
83
|
+
* defineRole<MyAttrs, MyScope>()
|
|
84
|
+
* .id("manager")
|
|
85
|
+
* .use(canManageUsers((attrs) => ({ dept: attrs.dept })))
|
|
86
|
+
* .build();
|
|
87
|
+
*/
|
|
88
|
+
declare function definePrivilege<TUserAttrs extends object = object, TScope extends object = object>(): <TArgs extends unknown[]>(factory: (...args: TArgs) => TArbacRule<TUserAttrs, TScope>[]) => ((...args: TArgs) => TPrivilegeFunction<TUserAttrs, TScope>);
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/db-privileges.d.ts
|
|
91
|
+
type ScopeFn<TUserAttrs, TScope> = (attrs: TUserAttrs, userId: string) => TScope;
|
|
92
|
+
interface ScopeOpts<TUserAttrs, TScope> {
|
|
93
|
+
scope?: ScopeFn<TUserAttrs, TScope>;
|
|
94
|
+
}
|
|
95
|
+
/** Read-side actions on an `AsDbController` table: `query`, `pages`, `getOne`, `getOneComposite`, `meta`, `metaForm`. */
|
|
96
|
+
declare function allowTableRead<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
97
|
+
/** All actions on an `AsDbController` table: read + `insert`, `update`, `replace`, `remove`, `removeComposite`. */
|
|
98
|
+
declare function allowTableWrite<TUserAttrs extends object = object, TScope extends object = object>(resource: string, opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
99
|
+
/** One or more declarative actions on the same table, sharing a scope. */
|
|
100
|
+
declare function allowTableAction<TUserAttrs extends object = object, TScope extends object = object>(resource: string, name: string | string[], opts?: ScopeOpts<TUserAttrs, TScope>): TPrivilegeFunction<TUserAttrs, TScope>;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/scope/types.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* Mongo-style field projection: `{ field: 0 | 1 }`.
|
|
105
|
+
* - All 1s = inclusion mode (only listed fields allowed)
|
|
106
|
+
* - All 0s = exclusion mode (listed fields denied, rest allowed)
|
|
107
|
+
* - Mixing 0 and 1 in a single projection is invalid.
|
|
108
|
+
*
|
|
109
|
+
* Structurally compatible with @uniqu/core SelectExpr in object form.
|
|
110
|
+
*/
|
|
111
|
+
type TProjection = Record<string, 0 | 1>;
|
|
112
|
+
/**
|
|
113
|
+
* A filter expression compatible with @uniqu/core FilterExpr.
|
|
114
|
+
* Comparison leaf: `{ field: value }` or `{ field: { $op: value } }`
|
|
115
|
+
* Logical branch: `{ $or: [...] }` | `{ $and: [...] }` | `{ $not: ... }`
|
|
116
|
+
*/
|
|
117
|
+
type TScopeFilter = Record<string, unknown>;
|
|
118
|
+
/**
|
|
119
|
+
* Per-control policy in a resource scope.
|
|
120
|
+
*
|
|
121
|
+
* Used inside `ArbacDbScope.controls` (in `@aooth/arbac-moost`) to gate
|
|
122
|
+
* Uniquery URL controls (`$with`, `$groupBy`, `$having`, …) on a per-role basis.
|
|
123
|
+
*
|
|
124
|
+
* Semantics:
|
|
125
|
+
* - **silence** (key absent / `undefined`) — no opinion → allow.
|
|
126
|
+
* - `true` — explicit allow (same effect as silence; useful for documentation).
|
|
127
|
+
* - `false` — explicit deny; using the control fails 403.
|
|
128
|
+
* - `readonly string[]` — whitelist mode; only the listed values are allowed.
|
|
129
|
+
* E.g., `{ $with: ['comments'] }` permits `?$with=comments` but rejects
|
|
130
|
+
* `?$with=tasks` with 403.
|
|
131
|
+
*
|
|
132
|
+
* Whitelist arrays are supported only for controls whose value is a list of
|
|
133
|
+
* named entities (notably `$with` for relation names and `$groupBy` for
|
|
134
|
+
* column names). For other controls (e.g. `$having`) only boolean gates are
|
|
135
|
+
* supported in v1; supplying a whitelist throws at scope-evaluation time.
|
|
136
|
+
*/
|
|
137
|
+
type ControlGate = boolean | readonly string[];
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/scope/projection.d.ts
|
|
140
|
+
type TProjectionMode = "include" | "exclude" | "empty";
|
|
141
|
+
/**
|
|
142
|
+
* Determine whether a projection is in inclusion mode (all 1s),
|
|
143
|
+
* exclusion mode (all 0s), or empty (`{}`, no restriction).
|
|
144
|
+
*
|
|
145
|
+
* @throws when a single projection mixes 0 and 1 values
|
|
146
|
+
*/
|
|
147
|
+
declare function getProjectionMode(proj: TProjection): TProjectionMode;
|
|
148
|
+
/**
|
|
149
|
+
* Check whether a dot-path field is allowed by a projection.
|
|
150
|
+
*
|
|
151
|
+
* Inclusion mode: a field is allowed if it, any of its parents, or any of its children
|
|
152
|
+
* is explicitly listed.
|
|
153
|
+
* Exclusion mode: a field is allowed unless it or any of its parents is excluded.
|
|
154
|
+
* Empty projection: every field is allowed.
|
|
155
|
+
*/
|
|
156
|
+
declare function isFieldAllowed(field: string, projection: TProjection): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Combines projections from multiple RBAC role grants under additive semantics:
|
|
159
|
+
* more roles = broader access. Each projection represents a set of allowed fields:
|
|
160
|
+
* include-mode `{a:1}` allows `{a}`; exclude-mode `{a:0}` allows `universe \ {a}`;
|
|
161
|
+
* empty `{}` allows the universe.
|
|
162
|
+
*
|
|
163
|
+
* A field is effectively allowed if any input grants it. Equivalently, a field
|
|
164
|
+
* stays excluded only if (a) no include-mode role grants it explicitly, AND (b)
|
|
165
|
+
* every exclude-mode role excludes it.
|
|
166
|
+
*
|
|
167
|
+
* Output mode:
|
|
168
|
+
* - All-include input → include-mode result (union of include keys)
|
|
169
|
+
* - At least one exclude-mode → exclude-mode result (or `{}` if no fields excluded)
|
|
170
|
+
* - Empty input or any universal grant `{}` → `{}` (universe)
|
|
171
|
+
*
|
|
172
|
+
* Within a single projection, mixing 1 and 0 keys is an error (call sites should
|
|
173
|
+
* normalize first). Across projections, mixing modes is supported and resolves
|
|
174
|
+
* via the additive rule above.
|
|
175
|
+
*
|
|
176
|
+
* @throws when a single projection mixes 1 and 0 keys, or contains an invalid value
|
|
177
|
+
*/
|
|
178
|
+
declare function unionProjections(...projections: TProjection[]): TProjection;
|
|
179
|
+
/**
|
|
180
|
+
* Restrict a desired projection to only fields allowed by an access-control projection.
|
|
181
|
+
*
|
|
182
|
+
* The result is the intersection of the two projections: only fields that pass both
|
|
183
|
+
* `desired` and `accessControl` survive. Either side may be empty (unrestricted), in
|
|
184
|
+
* which case the other side is returned. Mixed include/exclude modes are normalized
|
|
185
|
+
* to a single result projection.
|
|
186
|
+
*
|
|
187
|
+
* @param desired - the projection the caller asked for
|
|
188
|
+
* @param accessControl - the projection allowed by RBAC
|
|
189
|
+
*/
|
|
190
|
+
declare function restrictProjection(desired: TProjection, accessControl: TProjection): TProjection;
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/scope/filter.d.ts
|
|
193
|
+
/**
|
|
194
|
+
* Merge multiple scope filters into a single filter using `$or` semantics.
|
|
195
|
+
*
|
|
196
|
+
* In RBAC, if multiple roles grant access with different filters,
|
|
197
|
+
* the user can see records matching ANY of them — hence `$or`.
|
|
198
|
+
*
|
|
199
|
+
* Behaviour:
|
|
200
|
+
* - Empty input → `undefined` (no filter)
|
|
201
|
+
* - Any empty filter → `undefined` (one role grants unrestricted access)
|
|
202
|
+
* - Single filter → returned as-is
|
|
203
|
+
* - All filters single-keyed on the same primitive field → `{ field: { $in: [...] } }`
|
|
204
|
+
* - Otherwise → `{ $or: [...] }`
|
|
205
|
+
*
|
|
206
|
+
* @returns the merged filter, or `undefined` for unrestricted access
|
|
207
|
+
*/
|
|
208
|
+
declare function mergeScopeFilters(scopes: TScopeFilter[]): TScopeFilter | undefined;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/scope/controls.d.ts
|
|
211
|
+
/**
|
|
212
|
+
* Union per-role `controls` policies. Additive RBAC semantics: more roles =
|
|
213
|
+
* broader access, mirroring how `mergeScopeFilters` and `unionProjections`
|
|
214
|
+
* combine other parts of an `ArbacDbScope`.
|
|
215
|
+
*
|
|
216
|
+
* Resolution per control key:
|
|
217
|
+
* 1. Collect each scope's value for the key (including `undefined` for
|
|
218
|
+
* silent scopes — i.e., scopes that have a `controls` map but no entry
|
|
219
|
+
* for this key).
|
|
220
|
+
* 2. If any value is `undefined` or `true` → return `true` (full allow).
|
|
221
|
+
* Silence and explicit allow both mean "this role has no objection".
|
|
222
|
+
* 3. Else (every scope is `false` or `string[]`):
|
|
223
|
+
* - If all are `false` → return `false` (full deny).
|
|
224
|
+
* - Else collect every `string[]` into a single sorted, de-duplicated
|
|
225
|
+
* array; return that array. (Roles that say `false` don't contribute
|
|
226
|
+
* positively; roles with whitelists do.)
|
|
227
|
+
*
|
|
228
|
+
* Scopes that omit the `controls` map entirely are treated as silent on
|
|
229
|
+
* EVERY key — i.e., they grant unrestricted control usage. As a result, if
|
|
230
|
+
* even a single scope in the input lacks a `controls` map, the result is
|
|
231
|
+
* always `{}` (no restrictions).
|
|
232
|
+
*
|
|
233
|
+
* Whitelist support is restricted to {@link WHITELISTABLE_CONTROLS}. Passing
|
|
234
|
+
* a whitelist for any other control throws — the gate cannot be enforced
|
|
235
|
+
* meaningfully in v1, so the misuse is rejected loudly at scope-merge time.
|
|
236
|
+
*
|
|
237
|
+
* @returns merged policy keyed by control name. Keys absent from the result
|
|
238
|
+
* mean "no opinion → fully allowed". The caller should treat a
|
|
239
|
+
* missing key the same as `true`.
|
|
240
|
+
*
|
|
241
|
+
* @throws when a non-whitelistable control is given a `readonly string[]` gate.
|
|
242
|
+
*/
|
|
243
|
+
declare function unionControlsPolicy(scopes: ReadonlyArray<{
|
|
244
|
+
controls?: Record<string, ControlGate>;
|
|
245
|
+
}>): Record<string, ControlGate>;
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/codegen/extract.d.ts
|
|
248
|
+
interface TResourceActionMap {
|
|
249
|
+
/** Map of resource string → Set of action strings */
|
|
250
|
+
resources: Map<string, Set<string>>;
|
|
251
|
+
/** All unique resource strings */
|
|
252
|
+
allResources: Set<string>;
|
|
253
|
+
/** All unique action strings */
|
|
254
|
+
allActions: Set<string>;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extract all resource and action strings from role definitions.
|
|
258
|
+
*
|
|
259
|
+
* @param roles - Array of built role definitions
|
|
260
|
+
* @param options.includeWildcards - If false (default), skip entries containing `*` or `**`
|
|
261
|
+
*/
|
|
262
|
+
declare function extractResourceActions(roles: TArbacRole<unknown, unknown>[], options?: {
|
|
263
|
+
includeWildcards?: boolean;
|
|
264
|
+
}): TResourceActionMap;
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/codegen/generate.d.ts
|
|
267
|
+
interface TCodegenOptions {
|
|
268
|
+
/** Name for the resource union type. Default: "Resource" */
|
|
269
|
+
resourceTypeName?: string;
|
|
270
|
+
/** Name for the action union type. Default: "Action" */
|
|
271
|
+
actionTypeName?: string;
|
|
272
|
+
/** Whether to generate a resource-to-actions map type. Default: true */
|
|
273
|
+
resourceActionMap?: boolean;
|
|
274
|
+
/** Header comment. Default: "// Auto-generated by @aooth/arbac codegen" */
|
|
275
|
+
header?: string;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Generate TypeScript source defining union types for resources and actions.
|
|
279
|
+
*/
|
|
280
|
+
declare function generateResourceTypes(map: TResourceActionMap, options?: TCodegenOptions): string;
|
|
281
|
+
//#endregion
|
|
282
|
+
export { type ControlGate, type RoleBuilder, type TCodegenOptions, type TPrivilegeFunction, type TProjection, type TProjectionMode, type TResourceActionMap, type TScopeFilter, allowTableAction, allowTableRead, allowTableWrite, definePrivilege, defineRole, extractResourceActions, generateResourceTypes, getProjectionMode, isFieldAllowed, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections };
|