@aooth/arbac-moost 0.1.66 → 0.1.68
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/dist/atscript/index.d.mts +1 -1
- package/dist/index.d.mts +100 -26
- package/dist/index.mjs +293 -134
- package/dist/{user.provider-B1f6ZI45.d.mts → user.provider-C5BrFNxg.d.mts} +25 -19
- package/package.json +11 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-
|
|
1
|
+
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-C5BrFNxg.mjs";
|
|
2
2
|
import { TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap, TValidatorOptions, Validator } from "@atscript/typescript/utils";
|
|
3
3
|
import { AoothUserCredentials } from "@aooth/user/atscript-db/model.as";
|
|
4
4
|
//#region src/atscript/auto-provider.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
import { a as ArbacDbScope, c as enforceControlsPolicy, d as NavRelationKey, f as NavTarget, i as conjoinArbacDbScopes, l as extractUsedControlValues, m as ProjectionOf, n as ArbacUserProviderToken, o as AsArbacDbController, p as OwnFieldKey, r as AoothArbacClaims, s as applyAllowedFieldsAndSet, t as ArbacUserProvider, u as ControlsOf } from "./user.provider-
|
|
1
|
+
import { a as ArbacDbScope, c as enforceControlsPolicy, d as NavRelationKey, f as NavTarget, i as conjoinArbacDbScopes, l as extractUsedControlValues, m as ProjectionOf, n as ArbacUserProviderToken, o as AsArbacDbController, p as OwnFieldKey, r as AoothArbacClaims, s as applyAllowedFieldsAndSet, t as ArbacUserProvider, u as ControlsOf } from "./user.provider-C5BrFNxg.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
4
4
|
import { EventContext } from "@wooksjs/event-core";
|
|
5
5
|
import { Mate, TMateParamMeta, TMoostMetadata } from "moost";
|
|
6
6
|
import { ControlGate, TProjection } from "@aooth/arbac";
|
|
7
|
+
import { TMetaResponse } from "@atscript/db";
|
|
7
8
|
import { AsDbReadableController } from "@atscript/moost-db";
|
|
8
9
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
9
|
-
import { TMetaResponse } from "@atscript/db";
|
|
10
10
|
//#region src/arbac.composables.d.ts
|
|
11
|
+
/**
|
|
12
|
+
* The scopes cached for the current event (`undefined` before the authorize
|
|
13
|
+
* interceptor / `setScopes` ran). A direct slot read — no controller
|
|
14
|
+
* metadata resolution — for per-field hot paths like `hasField`; use
|
|
15
|
+
* {@link useArbac} for `evaluate` / `setScopes`.
|
|
16
|
+
*
|
|
17
|
+
* `TScope` is a caller-side type witness (see `evaluate` below).
|
|
18
|
+
*/
|
|
19
|
+
declare function getArbacScopes<TScope extends object>(ctx?: EventContext): TScope[] | undefined;
|
|
11
20
|
interface ArbacBindings {
|
|
12
21
|
getScopes: <TScope extends object>() => TScope[] | undefined;
|
|
13
22
|
setScopes: <TScope extends object>(scope: TScope[] | undefined) => void;
|
|
@@ -129,11 +138,8 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
129
138
|
*/
|
|
130
139
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
131
140
|
/**
|
|
132
|
-
* Scope-aware field
|
|
133
|
-
* {@link AsArbacDbController.hasField}
|
|
134
|
-
* read-scope projection union answers `false`, so `validateInsights` rejects
|
|
135
|
-
* `$select` / filter / sort references to it with the identical
|
|
136
|
-
* `Unknown field "x"` 400 a nonexistent field gets.
|
|
141
|
+
* Scope-aware field visibility — same contract as
|
|
142
|
+
* {@link AsArbacDbController.hasField}.
|
|
137
143
|
*/
|
|
138
144
|
protected hasField(path: string): boolean;
|
|
139
145
|
}
|
|
@@ -159,7 +165,10 @@ declare function collectWithGrantNames(scopes: ArbacDbScope[]): ReadonlySet<stri
|
|
|
159
165
|
* (or per `/meta` overlay) and threaded through the pruning walk.
|
|
160
166
|
*/
|
|
161
167
|
interface MetaVisibility {
|
|
162
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Projection union for this level's own fields. `{}` (the universe) means
|
|
170
|
+
* unrestricted — possible when only a `with` sub-scope restricts anything.
|
|
171
|
+
*/
|
|
163
172
|
allowed: TProjection;
|
|
164
173
|
/**
|
|
165
174
|
* Identifier fields reads ALWAYS return regardless of projection (the read
|
|
@@ -169,6 +178,25 @@ interface MetaVisibility {
|
|
|
169
178
|
alwaysVisible: ReadonlySet<string>;
|
|
170
179
|
/** Relation names granted via `with.<name>` (see {@link collectWithGrantNames}). */
|
|
171
180
|
withGrants: ReadonlySet<string>;
|
|
181
|
+
/**
|
|
182
|
+
* Visibility of a `with`-granted relation's JOINED fields, built from the
|
|
183
|
+
* union of its `with.<name>` sub-scopes (with the related table's own
|
|
184
|
+
* identifiers always visible). A path `rel.x.y` is checked as `x.y`
|
|
185
|
+
* against it, recursively for nested relations. The scope-driven builders
|
|
186
|
+
* ({@link buildScopeVisibility}) always supply it; a hand-built visibility
|
|
187
|
+
* without it lets granted paths through unchecked (legacy behavior).
|
|
188
|
+
*/
|
|
189
|
+
relation?: (name: string) => MetaVisibility | undefined;
|
|
190
|
+
/**
|
|
191
|
+
* Precompiled `isFieldAllowed(path, allowed)`. Supplied by
|
|
192
|
+
* {@link buildScopeVisibility}; a hand-built visibility falls back to
|
|
193
|
+
* `isFieldAllowed`.
|
|
194
|
+
*/
|
|
195
|
+
isAllowed?: (path: string) => boolean;
|
|
196
|
+
/** The scopes this level was built from (set by {@link buildScopeVisibility}). */
|
|
197
|
+
scopes?: ArbacDbScope[];
|
|
198
|
+
/** The table this level describes, when known (set by {@link buildScopeVisibility}). */
|
|
199
|
+
table?: VisibilityTableSource;
|
|
172
200
|
/**
|
|
173
201
|
* Fields the principal's WRITE scopes allow (union of `allowedFields`, or
|
|
174
202
|
* `"all"` for an unrestricted write grant). A field that is writable but not
|
|
@@ -187,10 +215,12 @@ interface MetaVisibility {
|
|
|
187
215
|
*/
|
|
188
216
|
declare function collectWritableFields(scopes: ArbacDbScope[], unrestricted: boolean): ReadonlySet<string> | "all" | undefined;
|
|
189
217
|
/**
|
|
190
|
-
* Whether a flattened dot-path field exists for this principal. A
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
218
|
+
* Whether a flattened dot-path field exists for this principal. A
|
|
219
|
+
* `with`-granted relation itself is always visible (the grant implies it);
|
|
220
|
+
* a path UNDER it (`rel.x.y`) is checked as `x.y` against the relation's
|
|
221
|
+
* sub-scope visibility ({@link MetaVisibility.relation}) — so a field the
|
|
222
|
+
* sub-scope hides is as unknown in a `$with` sub-query as a top-level hidden
|
|
223
|
+
* column is in the main query (no filter/sort value oracle).
|
|
194
224
|
*/
|
|
195
225
|
declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
196
226
|
/**
|
|
@@ -201,8 +231,9 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
|
201
231
|
*
|
|
202
232
|
* - `fields` — the flat capability map (sortable/filterable flags).
|
|
203
233
|
* - `type` — the serialized annotated type (what dynamic clients build
|
|
204
|
-
* tables/forms from); nested object props prune by dot-path
|
|
205
|
-
*
|
|
234
|
+
* tables/forms from); nested object props prune by dot-path. A relation
|
|
235
|
+
* prop visible through the projection survives whole; a `with`-granted
|
|
236
|
+
* one is pruned by its sub-scope (recursively), matching `hasField`.
|
|
206
237
|
* - `relations` — entries neither projected nor `with`-granted.
|
|
207
238
|
* - `versionColumn` — dropped when the OCC column itself is hidden.
|
|
208
239
|
*
|
|
@@ -210,19 +241,59 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
|
210
241
|
* (`applyMetaOverlay` contract); every pruned branch is a fresh object.
|
|
211
242
|
*/
|
|
212
243
|
declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility): TMetaResponse;
|
|
244
|
+
/**
|
|
245
|
+
* The slice of an atscript-db readable (table / view) the visibility walk
|
|
246
|
+
* reads — a moost-db controller's `this.readable` satisfies it: identifiers
|
|
247
|
+
* (always visible), nav relations (to reach the joined table) and the
|
|
248
|
+
* flattened schema (to split `$select` parents by scope).
|
|
249
|
+
*/
|
|
250
|
+
interface VisibilityTableSource {
|
|
251
|
+
primaryKeys: readonly string[];
|
|
252
|
+
preferredId: readonly string[];
|
|
253
|
+
relations?: ReadonlyMap<string, unknown>;
|
|
254
|
+
/**
|
|
255
|
+
* The target table of nav relation `navField` (atscript-db's
|
|
256
|
+
* `readable.relatedTable`); absent / `undefined` → the joined table's
|
|
257
|
+
* identifiers are not exempted.
|
|
258
|
+
*/
|
|
259
|
+
relatedTable?(navField: string): VisibilityTableSource | undefined;
|
|
260
|
+
/** Flattened schema (dot-paths) — used to split `$select` parents by scope. */
|
|
261
|
+
flatMap?: ReadonlyMap<string, unknown>;
|
|
262
|
+
/** Navigation field paths — their joined descendants are not own columns. */
|
|
263
|
+
navFields?: ReadonlySet<string>;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Build the {@link MetaVisibility} a set of read scopes implies — the single
|
|
267
|
+
* structure behind `hasField`, `/meta` pruning, `$select` value stripping
|
|
268
|
+
* and the `$with` overlay, so they cannot drift. Own fields: the projection
|
|
269
|
+
* union (`{}` when unrestricted), precompiled into `isAllowed`. A
|
|
270
|
+
* `with`-granted relation: a lazily built (memoized) child visibility over
|
|
271
|
+
* the union of its `with.<name>` sub-scopes — the same sub-scopes joined rows
|
|
272
|
+
* are stripped with — whose `alwaysVisible` is the related table's own PK /
|
|
273
|
+
* `preferredId` (reached through `table.relatedTable`; without it, no
|
|
274
|
+
* related identifier is exempt).
|
|
275
|
+
*
|
|
276
|
+
* @param alwaysVisible - identifiers exempt from the projection; defaults to
|
|
277
|
+
* the table's PK + `preferredId`
|
|
278
|
+
*/
|
|
279
|
+
declare function buildScopeVisibility(scopes: ArbacDbScope[], table: VisibilityTableSource | undefined, alwaysVisible?: ReadonlySet<string>): MetaVisibility;
|
|
213
280
|
/**
|
|
214
281
|
* Shared body of both ARBAC controllers' `hasField` overrides (BUG-3 twin of
|
|
215
|
-
* the `/meta` pruning): a field outside the read-scope projection union
|
|
216
|
-
*
|
|
217
|
-
*
|
|
282
|
+
* the `/meta` pruning): a field outside the read-scope projection union —
|
|
283
|
+
* or, for a `rel.x` path under a `with`-granted relation, outside that
|
|
284
|
+
* relation's sub-scope union — must be indistinguishable from a field that
|
|
285
|
+
* does not exist. Returns `true` when there are no scopes (unscoped grant).
|
|
286
|
+
*
|
|
287
|
+
* @param source - the controller's `this.readable`; an identifier set (the
|
|
288
|
+
* 0.1.67 signature) still works, without related-table identifiers
|
|
218
289
|
*/
|
|
219
|
-
declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string,
|
|
290
|
+
declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
|
|
220
291
|
/**
|
|
221
292
|
* PK + `preferredId` for a controller's table/readable — the
|
|
222
|
-
* {@link MetaVisibility.alwaysVisible} set
|
|
223
|
-
* `
|
|
293
|
+
* {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
|
|
294
|
+
* controllers now pass `this.readable` and it is derived there.
|
|
224
295
|
*/
|
|
225
|
-
declare function metaAlwaysVisibleFields(
|
|
296
|
+
declare function metaAlwaysVisibleFields(_controller: object, source: {
|
|
226
297
|
primaryKeys: readonly string[];
|
|
227
298
|
preferredId: readonly string[];
|
|
228
299
|
}): ReadonlySet<string>;
|
|
@@ -242,11 +313,14 @@ declare function metaAlwaysVisibleFields(controller: object, source: {
|
|
|
242
313
|
* universe, no pruning (unchanged behavior for unscoped roles). No read op
|
|
243
314
|
* allowed → the projection union is empty → no pruning: `crud` already
|
|
244
315
|
* advertises no read surface, and write-only principals still need `type`
|
|
245
|
-
* for their insert/update forms.
|
|
246
|
-
*
|
|
247
|
-
*
|
|
316
|
+
* for their insert/update forms. PK + `preferredId` are never pruned —
|
|
317
|
+
* reads always return those fields (projection widening / id addressing).
|
|
318
|
+
*
|
|
319
|
+
* @param source - the controller's `this.readable` (identifiers and related
|
|
320
|
+
* tables are derived from it); an identifier set (the 0.1.67 signature)
|
|
321
|
+
* still works, without related-table identifiers
|
|
248
322
|
*/
|
|
249
|
-
declare function applyArbacMetaOverlay(meta: TMetaResponse,
|
|
323
|
+
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>): Promise<TMetaResponse>;
|
|
250
324
|
/**
|
|
251
325
|
* Test-friendly internal helper — exported for unit tests and helper
|
|
252
326
|
* composition; regular consumers should not call this directly.
|
|
@@ -275,4 +349,4 @@ declare function collectMethodNames(instance: object): string[];
|
|
|
275
349
|
*/
|
|
276
350
|
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
|
|
277
351
|
//#endregion
|
|
278
|
-
export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
|
|
352
|
+
export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, buildScopeVisibility, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core"
|
|
|
3
3
|
import { Authenticate, HttpError } from "@moostjs/event-http";
|
|
4
4
|
import { current, key } from "@wooksjs/event-core";
|
|
5
5
|
import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getMoostMate, useControllerContext, useLogger } from "moost";
|
|
6
|
-
import { conjoinScopeFilters, intersectControlsPolicy, isFieldAllowed, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
|
|
6
|
+
import { DENY_FILTER, conjoinScopeFilters, expandExcludeToLeaves, getProjectionMode, intersectControlsPolicy, intersectProjections, isFieldAllowed, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
|
|
7
|
+
import { findAncestorInSet } from "@atscript/db";
|
|
7
8
|
import { AsDbController, AsDbReadableController } from "@atscript/moost-db";
|
|
8
9
|
import { groupByFields } from "@uniqu/core";
|
|
9
10
|
//#region src/attenuation.ts
|
|
@@ -15,17 +16,29 @@ import { groupByFields } from "@uniqu/core";
|
|
|
15
16
|
*
|
|
16
17
|
* Each side is first UNIONed with the existing additive helpers (today's
|
|
17
18
|
* machinery), then the two RESULTS are CONJOINED with the dedicated combiners
|
|
18
|
-
* ({@link conjoinScopeFilters} `$and`, {@link
|
|
19
|
+
* ({@link conjoinScopeFilters} `$and`, {@link intersectProjections} field ∩,
|
|
19
20
|
* {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
|
|
20
21
|
* additive union helpers, which would silently widen.
|
|
21
22
|
*
|
|
22
23
|
* Returned as a single-element list so every downstream scope-application
|
|
23
24
|
* site (which UNIONs the cached scope list per facet) sees the identity of a
|
|
24
25
|
* one-element union — i.e. the conjunction — with no change to those sites.
|
|
26
|
+
*
|
|
27
|
+
* The projection conjunction is path-wise and never wider than either side
|
|
28
|
+
* (`{a:1}` ∩ `{"a.b":1}` → `{"a.b":1}`). A nested exclusion under an
|
|
29
|
+
* included parent (`{a:1}` ∩ `{"a.c":0}`) is subtracted exactly via
|
|
30
|
+
* `childrenOf` (the table schema); without it the parent is dropped (fail
|
|
31
|
+
* closed). When NO field survives (`{a:1}` ∩ `{a:0}`, disjoint whitelists),
|
|
32
|
+
* the composite keeps the user's projection and gets a match-nothing filter —
|
|
33
|
+
* an empty field set must never read as the unrestricted `{}`. `with`
|
|
34
|
+
* sub-scopes are conjoined without a schema.
|
|
25
35
|
*/
|
|
26
|
-
function conjoinArbacDbScopes(userScopes, credScopes) {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
36
|
+
function conjoinArbacDbScopes(userScopes, credScopes, childrenOf) {
|
|
37
|
+
const userProjection = unionProjections(...userScopes.map((s) => s.projection ?? {}));
|
|
38
|
+
const intersected = intersectProjections(userProjection, unionProjections(...credScopes.map((s) => s.projection ?? {})), childrenOf);
|
|
39
|
+
const projection = intersected ?? userProjection;
|
|
40
|
+
const credFilter = mergeScopeFilters(credScopes.map((s) => s.filter ?? {}));
|
|
41
|
+
const filter = conjoinScopeFilters(mergeScopeFilters(userScopes.map((s) => s.filter ?? {})), intersected ? credFilter : conjoinScopeFilters(credFilter, DENY_FILTER));
|
|
29
42
|
const controls = intersectControlsPolicy(unionControlsPolicy(userScopes), unionControlsPolicy(credScopes));
|
|
30
43
|
const allowedFields = intersectAllowedFields(userScopes, credScopes);
|
|
31
44
|
const set = combineSet(userScopes, credScopes);
|
|
@@ -98,6 +111,34 @@ function conjoinWith(userScopes, credScopes) {
|
|
|
98
111
|
return out;
|
|
99
112
|
}
|
|
100
113
|
//#endregion
|
|
114
|
+
//#region src/db/field-children.ts
|
|
115
|
+
const fieldChildrenCache = /* @__PURE__ */ new WeakMap();
|
|
116
|
+
/**
|
|
117
|
+
* The `TProjectionChildren` schema lookup for a table (its own `flatMap` paths —
|
|
118
|
+
* navigation descendants excluded, they are joined rows, not columns);
|
|
119
|
+
* `undefined` when the table exposes no flattened schema.
|
|
120
|
+
*/
|
|
121
|
+
function fieldChildrenOf(table) {
|
|
122
|
+
if (!table?.flatMap) return void 0;
|
|
123
|
+
let lookup = fieldChildrenCache.get(table);
|
|
124
|
+
if (!lookup) {
|
|
125
|
+
const nav = table.navFields ?? new Set(table.relations?.keys());
|
|
126
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const path of table.flatMap.keys()) {
|
|
128
|
+
const dot = path.lastIndexOf(".");
|
|
129
|
+
if (dot === -1) continue;
|
|
130
|
+
const parent = path.slice(0, dot);
|
|
131
|
+
if (nav.has(parent) || findAncestorInSet(parent, nav) !== void 0) continue;
|
|
132
|
+
const list = byParent.get(parent);
|
|
133
|
+
if (list) list.push(path);
|
|
134
|
+
else byParent.set(parent, [path]);
|
|
135
|
+
}
|
|
136
|
+
lookup = (path) => byParent.get(path) ?? [];
|
|
137
|
+
fieldChildrenCache.set(table, lookup);
|
|
138
|
+
}
|
|
139
|
+
return lookup;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
101
142
|
//#region src/moost-arbac.ts
|
|
102
143
|
let MoostArbac = class MoostArbac extends Arbac$1 {};
|
|
103
144
|
MoostArbac = __decorate([Injectable()], MoostArbac);
|
|
@@ -112,6 +153,18 @@ MoostArbac = __decorate([Injectable()], MoostArbac);
|
|
|
112
153
|
*/
|
|
113
154
|
const arbacScopesKey = key("arbac.scopes");
|
|
114
155
|
/**
|
|
156
|
+
* The scopes cached for the current event (`undefined` before the authorize
|
|
157
|
+
* interceptor / `setScopes` ran). A direct slot read — no controller
|
|
158
|
+
* metadata resolution — for per-field hot paths like `hasField`; use
|
|
159
|
+
* {@link useArbac} for `evaluate` / `setScopes`.
|
|
160
|
+
*
|
|
161
|
+
* `TScope` is a caller-side type witness (see `evaluate` below).
|
|
162
|
+
*/
|
|
163
|
+
function getArbacScopes(ctx) {
|
|
164
|
+
const c = ctx ?? current();
|
|
165
|
+
return c.has(arbacScopesKey) ? c.get(arbacScopesKey) : void 0;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
115
168
|
* Composable for ARBAC utilities within Moost handlers and interceptors.
|
|
116
169
|
*
|
|
117
170
|
* Exposes scope read/write, lazy `evaluate`, and the resolved
|
|
@@ -129,7 +182,7 @@ const arbacScopesKey = key("arbac.scopes");
|
|
|
129
182
|
const useArbac = (_ctx) => {
|
|
130
183
|
const ctx = _ctx ?? current();
|
|
131
184
|
const cc = useControllerContext(ctx);
|
|
132
|
-
const getScopes = () =>
|
|
185
|
+
const getScopes = () => getArbacScopes(ctx);
|
|
133
186
|
const setScopes = (scope) => {
|
|
134
187
|
ctx.set(arbacScopesKey, scope);
|
|
135
188
|
};
|
|
@@ -161,7 +214,7 @@ const useArbac = (_ctx) => {
|
|
|
161
214
|
});
|
|
162
215
|
if (result.allowed && result.credScopes !== void 0) return {
|
|
163
216
|
allowed: true,
|
|
164
|
-
scopes: conjoinArbacDbScopes(result.scopes ?? [], result.credScopes),
|
|
217
|
+
scopes: conjoinArbacDbScopes(result.scopes ?? [], result.credScopes, fieldChildrenOf(cc.getController()?.readable)),
|
|
165
218
|
userId
|
|
166
219
|
};
|
|
167
220
|
return {
|
|
@@ -267,11 +320,7 @@ function collectWithGrantNames(scopes) {
|
|
|
267
320
|
/** Exact-or-ancestor membership: `credit.credentials.user` matches a `credit.credentials` grant. */
|
|
268
321
|
function isPathWritable(path, writable) {
|
|
269
322
|
if (!writable) return false;
|
|
270
|
-
|
|
271
|
-
if (writable.has(path)) return true;
|
|
272
|
-
let pos = path.length;
|
|
273
|
-
while ((pos = path.lastIndexOf(".", pos - 1)) !== -1) if (writable.has(path.slice(0, pos))) return true;
|
|
274
|
-
return false;
|
|
323
|
+
return writable === "all" || hasSelfOrAncestor(writable, path);
|
|
275
324
|
}
|
|
276
325
|
/**
|
|
277
326
|
* Union of write-scope `allowedFields`; `"all"` for field-unrestricted write
|
|
@@ -292,17 +341,23 @@ function collectWritableFields(scopes, unrestricted) {
|
|
|
292
341
|
return out.size > 0 ? out : void 0;
|
|
293
342
|
}
|
|
294
343
|
/**
|
|
295
|
-
* Whether a flattened dot-path field exists for this principal. A
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
344
|
+
* Whether a flattened dot-path field exists for this principal. A
|
|
345
|
+
* `with`-granted relation itself is always visible (the grant implies it);
|
|
346
|
+
* a path UNDER it (`rel.x.y`) is checked as `x.y` against the relation's
|
|
347
|
+
* sub-scope visibility ({@link MetaVisibility.relation}) — so a field the
|
|
348
|
+
* sub-scope hides is as unknown in a `$with` sub-query as a top-level hidden
|
|
349
|
+
* column is in the main query (no filter/sort value oracle).
|
|
299
350
|
*/
|
|
300
351
|
function isMetaFieldVisible(path, vis) {
|
|
301
352
|
if (vis.alwaysVisible.has(path)) return true;
|
|
302
353
|
const dot = path.indexOf(".");
|
|
303
354
|
const head = dot === -1 ? path : path.slice(0, dot);
|
|
304
|
-
if (vis.withGrants.has(head))
|
|
305
|
-
|
|
355
|
+
if (vis.withGrants.has(head)) {
|
|
356
|
+
if (dot === -1) return true;
|
|
357
|
+
const sub = vis.relation?.(head);
|
|
358
|
+
return sub ? isMetaFieldVisible(path.slice(dot + 1), sub) : true;
|
|
359
|
+
}
|
|
360
|
+
return vis.isAllowed ? vis.isAllowed(path) : isFieldAllowed(path, vis.allowed);
|
|
306
361
|
}
|
|
307
362
|
/**
|
|
308
363
|
* Prune a `/meta` envelope down to the fields the principal's read scopes can
|
|
@@ -312,8 +367,9 @@ function isMetaFieldVisible(path, vis) {
|
|
|
312
367
|
*
|
|
313
368
|
* - `fields` — the flat capability map (sortable/filterable flags).
|
|
314
369
|
* - `type` — the serialized annotated type (what dynamic clients build
|
|
315
|
-
* tables/forms from); nested object props prune by dot-path
|
|
316
|
-
*
|
|
370
|
+
* tables/forms from); nested object props prune by dot-path. A relation
|
|
371
|
+
* prop visible through the projection survives whole; a `with`-granted
|
|
372
|
+
* one is pruned by its sub-scope (recursively), matching `hasField`.
|
|
317
373
|
* - `relations` — entries neither projected nor `with`-granted.
|
|
318
374
|
* - `versionColumn` — dropped when the OCC column itself is hidden.
|
|
319
375
|
*
|
|
@@ -342,12 +398,13 @@ function pruneMetaByVisibility(meta, vis) {
|
|
|
342
398
|
}
|
|
343
399
|
/**
|
|
344
400
|
* Copy-on-prune walk over a serialized type node. `basePath` is the flattened
|
|
345
|
-
* dot-path prefix ("" at the root).
|
|
346
|
-
*
|
|
347
|
-
* joined
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* `hash`
|
|
401
|
+
* dot-path prefix ("" at the root). Relation props (nav props named in
|
|
402
|
+
* `meta.relations` at the root, or carrying a `db.rel.*` annotation inside a
|
|
403
|
+
* joined type) are dropped when invisible; a `with`-granted one is pruned by
|
|
404
|
+
* its sub-scope visibility (the same one `hasField` checks `rel.x` paths
|
|
405
|
+
* against), any other visible one survives whole. Own-field subtrees prune
|
|
406
|
+
* recursively so an include-mode union like `{ "password.hash": 1 }` keeps
|
|
407
|
+
* `password` with only `hash` inside.
|
|
351
408
|
*/
|
|
352
409
|
function pruneSerializedType(node, basePath, vis, relationNames) {
|
|
353
410
|
const def = node.type;
|
|
@@ -355,8 +412,10 @@ function pruneSerializedType(node, basePath, vis, relationNames) {
|
|
|
355
412
|
const props = {};
|
|
356
413
|
for (const [name, prop] of Object.entries(def.props)) {
|
|
357
414
|
const path = basePath ? `${basePath}.${name}` : name;
|
|
358
|
-
if (basePath === "" && relationNames.has(name)) {
|
|
359
|
-
if (isMetaFieldVisible(name, vis))
|
|
415
|
+
if (basePath === "" && (relationNames.has(name) || isNavProp(prop))) {
|
|
416
|
+
if (!isMetaFieldVisible(name, vis)) continue;
|
|
417
|
+
const sub = vis.withGrants.has(name) ? vis.relation?.(name) : void 0;
|
|
418
|
+
props[name] = sub ? pruneSerializedType(prop, "", sub, NO_NAMES) : prop;
|
|
360
419
|
continue;
|
|
361
420
|
}
|
|
362
421
|
if (!isMetaFieldVisible(path, vis)) {
|
|
@@ -395,52 +454,137 @@ function pruneSerializedType(node, basePath, vis, relationNames) {
|
|
|
395
454
|
};
|
|
396
455
|
return node;
|
|
397
456
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
457
|
+
const NO_NAMES = /* @__PURE__ */ new Set();
|
|
458
|
+
/** A nav prop inside a joined type: atscript-db stamps `db.rel.to` / `from` / `via`. */
|
|
459
|
+
function isNavProp(prop) {
|
|
460
|
+
const meta = prop.metadata;
|
|
461
|
+
return !!meta && ("db.rel.to" in meta || "db.rel.from" in meta || "db.rel.via" in meta);
|
|
462
|
+
}
|
|
463
|
+
const identifierSetCache = /* @__PURE__ */ new WeakMap();
|
|
464
|
+
/** PK + `preferredId` of a table — the identifiers reads always return. */
|
|
465
|
+
function identifierSet(table) {
|
|
466
|
+
if (!table) return NO_NAMES;
|
|
467
|
+
let set = identifierSetCache.get(table);
|
|
468
|
+
if (!set) {
|
|
469
|
+
set = /* @__PURE__ */ new Set([...table.primaryKeys, ...table.preferredId]);
|
|
470
|
+
identifierSetCache.set(table, set);
|
|
471
|
+
}
|
|
472
|
+
return set;
|
|
473
|
+
}
|
|
474
|
+
/** Pick `scopes[i].with?.[name]`, dropping undefined (silence wins when empty). */
|
|
475
|
+
function collectSubScopes(scopes, name) {
|
|
476
|
+
const out = [];
|
|
477
|
+
for (const s of scopes) {
|
|
478
|
+
const sub = s.with?.[name];
|
|
479
|
+
if (sub) out.push(sub);
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
483
|
+
/** `path` or one of its ancestors is in `set`. */
|
|
484
|
+
function hasSelfOrAncestor(set, path) {
|
|
485
|
+
return set.has(path) || findAncestorInSet(path, set) !== void 0;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Precompiled {@link isFieldAllowed} for one projection: `hasField` asks it
|
|
489
|
+
* for every referenced path, so the mode and key sets are computed once.
|
|
490
|
+
* Inclusion: the path, an ancestor, or a descendant is listed. Exclusion: no
|
|
491
|
+
* listed key is the path or an ancestor.
|
|
492
|
+
*/
|
|
493
|
+
function compileProjection(projection) {
|
|
494
|
+
const keys = Object.keys(projection);
|
|
495
|
+
if (keys.length === 0) return () => true;
|
|
496
|
+
const listed = new Set(keys);
|
|
497
|
+
if (getProjectionMode(projection) === "exclude") return (path) => !hasSelfOrAncestor(listed, path);
|
|
498
|
+
const parents = /* @__PURE__ */ new Set();
|
|
499
|
+
for (const key of keys) {
|
|
500
|
+
let pos = key.length;
|
|
501
|
+
while ((pos = key.lastIndexOf(".", pos - 1)) !== -1) parents.add(key.slice(0, pos));
|
|
502
|
+
}
|
|
503
|
+
return (path) => parents.has(path) || hasSelfOrAncestor(listed, path);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Build the {@link MetaVisibility} a set of read scopes implies — the single
|
|
507
|
+
* structure behind `hasField`, `/meta` pruning, `$select` value stripping
|
|
508
|
+
* and the `$with` overlay, so they cannot drift. Own fields: the projection
|
|
509
|
+
* union (`{}` when unrestricted), precompiled into `isAllowed`. A
|
|
510
|
+
* `with`-granted relation: a lazily built (memoized) child visibility over
|
|
511
|
+
* the union of its `with.<name>` sub-scopes — the same sub-scopes joined rows
|
|
512
|
+
* are stripped with — whose `alwaysVisible` is the related table's own PK /
|
|
513
|
+
* `preferredId` (reached through `table.relatedTable`; without it, no
|
|
514
|
+
* related identifier is exempt).
|
|
515
|
+
*
|
|
516
|
+
* @param alwaysVisible - identifiers exempt from the projection; defaults to
|
|
517
|
+
* the table's PK + `preferredId`
|
|
518
|
+
*/
|
|
519
|
+
function buildScopeVisibility(scopes, table, alwaysVisible = identifierSet(table)) {
|
|
520
|
+
const withGrants = collectWithGrantNames(scopes);
|
|
521
|
+
const allowed = unionScopeProjection(scopes) ?? {};
|
|
522
|
+
const children = /* @__PURE__ */ new Map();
|
|
523
|
+
return {
|
|
524
|
+
allowed,
|
|
525
|
+
alwaysVisible,
|
|
526
|
+
withGrants,
|
|
527
|
+
scopes,
|
|
528
|
+
table,
|
|
529
|
+
isAllowed: compileProjection(allowed),
|
|
530
|
+
relation(name) {
|
|
531
|
+
if (!withGrants.has(name)) return void 0;
|
|
532
|
+
let child = children.get(name);
|
|
533
|
+
if (!child) {
|
|
534
|
+
child = buildScopeVisibility(collectSubScopes(scopes, name), table?.relatedTable?.(name));
|
|
535
|
+
children.set(name, child);
|
|
536
|
+
}
|
|
537
|
+
return child;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
402
541
|
const scopeVisibilityCache = /* @__PURE__ */ new WeakMap();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
542
|
+
const NO_TABLE = {
|
|
543
|
+
primaryKeys: [],
|
|
544
|
+
preferredId: []
|
|
545
|
+
};
|
|
546
|
+
/** `source` is an identifier set (the 0.1.67 signatures), not a readable. */
|
|
547
|
+
function isIdentifierSet(source) {
|
|
548
|
+
return !("primaryKeys" in source);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* The event-cached {@link MetaVisibility} for `scopes` over `source` (a
|
|
552
|
+
* readable, or an `alwaysVisible` identifier set).
|
|
553
|
+
*/
|
|
554
|
+
function scopeVisibility(scopes, source = NO_TABLE) {
|
|
555
|
+
let perSource = scopeVisibilityCache.get(scopes);
|
|
556
|
+
if (!perSource) {
|
|
557
|
+
perSource = /* @__PURE__ */ new WeakMap();
|
|
558
|
+
scopeVisibilityCache.set(scopes, perSource);
|
|
559
|
+
}
|
|
560
|
+
let vis = perSource.get(source);
|
|
406
561
|
if (!vis) {
|
|
407
|
-
vis =
|
|
408
|
-
|
|
409
|
-
withGrants: collectWithGrantNames(scopes)
|
|
410
|
-
};
|
|
411
|
-
scopeVisibilityCache.set(scopes, vis);
|
|
562
|
+
vis = isIdentifierSet(source) ? buildScopeVisibility(scopes, void 0, source) : buildScopeVisibility(scopes, source === NO_TABLE ? void 0 : source);
|
|
563
|
+
perSource.set(source, vis);
|
|
412
564
|
}
|
|
413
565
|
return vis;
|
|
414
566
|
}
|
|
415
567
|
/**
|
|
416
568
|
* Shared body of both ARBAC controllers' `hasField` overrides (BUG-3 twin of
|
|
417
|
-
* the `/meta` pruning): a field outside the read-scope projection union
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
});
|
|
569
|
+
* the `/meta` pruning): a field outside the read-scope projection union —
|
|
570
|
+
* or, for a `rel.x` path under a `with`-granted relation, outside that
|
|
571
|
+
* relation's sub-scope union — must be indistinguishable from a field that
|
|
572
|
+
* does not exist. Returns `true` when there are no scopes (unscoped grant).
|
|
573
|
+
*
|
|
574
|
+
* @param source - the controller's `this.readable`; an identifier set (the
|
|
575
|
+
* 0.1.67 signature) still works, without related-table identifiers
|
|
576
|
+
*/
|
|
577
|
+
function isScopedFieldVisible(scopes, path, source) {
|
|
578
|
+
if (scopes.length === 0) return true;
|
|
579
|
+
return isMetaFieldVisible(path, scopeVisibility(scopes, source));
|
|
429
580
|
}
|
|
430
|
-
const alwaysVisibleFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
431
581
|
/**
|
|
432
582
|
* PK + `preferredId` for a controller's table/readable — the
|
|
433
|
-
* {@link MetaVisibility.alwaysVisible} set
|
|
434
|
-
* `
|
|
583
|
+
* {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
|
|
584
|
+
* controllers now pass `this.readable` and it is derived there.
|
|
435
585
|
*/
|
|
436
|
-
function metaAlwaysVisibleFields(
|
|
437
|
-
|
|
438
|
-
let set = alwaysVisibleFieldsCache.get(ctor);
|
|
439
|
-
if (!set) {
|
|
440
|
-
set = /* @__PURE__ */ new Set([...source.primaryKeys, ...source.preferredId]);
|
|
441
|
-
alwaysVisibleFieldsCache.set(ctor, set);
|
|
442
|
-
}
|
|
443
|
-
return set;
|
|
586
|
+
function metaAlwaysVisibleFields(_controller, source) {
|
|
587
|
+
return identifierSet(source);
|
|
444
588
|
}
|
|
445
589
|
/**
|
|
446
590
|
* The write CRUD ops. Everything else in `meta.crud` is row-returning read
|
|
@@ -471,11 +615,14 @@ const WRITE_CRUD_OPS = /* @__PURE__ */ new Set([
|
|
|
471
615
|
* universe, no pruning (unchanged behavior for unscoped roles). No read op
|
|
472
616
|
* allowed → the projection union is empty → no pruning: `crud` already
|
|
473
617
|
* advertises no read surface, and write-only principals still need `type`
|
|
474
|
-
* for their insert/update forms.
|
|
475
|
-
*
|
|
476
|
-
*
|
|
618
|
+
* for their insert/update forms. PK + `preferredId` are never pruned —
|
|
619
|
+
* reads always return those fields (projection widening / id addressing).
|
|
620
|
+
*
|
|
621
|
+
* @param source - the controller's `this.readable` (identifiers and related
|
|
622
|
+
* tables are derived from it); an identifier set (the 0.1.67 signature)
|
|
623
|
+
* still works, without related-table identifiers
|
|
477
624
|
*/
|
|
478
|
-
async function applyArbacMetaOverlay(meta,
|
|
625
|
+
async function applyArbacMetaOverlay(meta, source) {
|
|
479
626
|
const arbac = useArbac();
|
|
480
627
|
const actionToMethodMeta = collectActionMetaByName();
|
|
481
628
|
const crudKeys = Object.keys(meta.crud);
|
|
@@ -508,12 +655,10 @@ async function applyArbacMetaOverlay(meta, alwaysVisible) {
|
|
|
508
655
|
if (!scopes || scopes.length === 0) readUnrestricted = true;
|
|
509
656
|
else readScopes.push(...scopes);
|
|
510
657
|
}
|
|
511
|
-
if (!readUnrestricted) {
|
|
512
|
-
const
|
|
513
|
-
if (allowed) overlaid = pruneMetaByVisibility(overlaid, {
|
|
514
|
-
|
|
515
|
-
alwaysVisible,
|
|
516
|
-
withGrants: collectWithGrantNames(readScopes),
|
|
658
|
+
if (!readUnrestricted && readScopes.length > 0) {
|
|
659
|
+
const vis = isIdentifierSet(source) ? buildScopeVisibility(readScopes, void 0, source) : buildScopeVisibility(readScopes, source);
|
|
660
|
+
if (Object.keys(vis.allowed).length > 0 || vis.withGrants.size > 0) overlaid = pruneMetaByVisibility(overlaid, {
|
|
661
|
+
...vis,
|
|
517
662
|
writable: collectWritableFields(writeScopes, writeUnrestricted)
|
|
518
663
|
});
|
|
519
664
|
}
|
|
@@ -568,11 +713,13 @@ function collectMethodNames(instance) {
|
|
|
568
713
|
}
|
|
569
714
|
//#endregion
|
|
570
715
|
//#region src/db/shared-read-helpers.ts
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Single point of access to the per-event scope cache. Reads the slot
|
|
718
|
+
* directly — `hasField` calls this per referenced path, and `useArbac()`
|
|
719
|
+
* would resolve controller metadata on every call.
|
|
720
|
+
*/
|
|
574
721
|
function readCachedScopes() {
|
|
575
|
-
return
|
|
722
|
+
return getArbacScopes() ?? [];
|
|
576
723
|
}
|
|
577
724
|
/**
|
|
578
725
|
* Full body of `transformFilter` for both ARBAC DB controllers: evaluate
|
|
@@ -594,16 +741,34 @@ async function transformArbacFilter(filter) {
|
|
|
594
741
|
return conjoinScopeFilters(mergeScopeFilters((scopes ?? []).map((s) => s.filter ?? {})), filter) ?? {};
|
|
595
742
|
}
|
|
596
743
|
/**
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
744
|
+
* Restrict the user-supplied `$select` to the scopes' projection union.
|
|
745
|
+
* Returns the original projection untouched when no scope declares a
|
|
746
|
+
* projection (so caller sees the unrestricted shape). Reads the same cached
|
|
747
|
+
* visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
|
|
748
|
+
* stripping and name hiding cannot drift.
|
|
749
|
+
*
|
|
750
|
+
* `projection` is the `$select` as moost-db parsed it: an inclusion is an
|
|
751
|
+
* ARRAY of names, an exclusion an object — both are normalized first. A
|
|
752
|
+
* requested parent (`$select=a`) narrows to what the scope shows: to the
|
|
753
|
+
* whitelisted descendants (`a.b: 1`), or — for a hidden descendant
|
|
754
|
+
* (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
|
|
755
|
+
* the parent is dropped). Never the whole object, and never the empty
|
|
756
|
+
* (universe) projection: when nothing survives, the scope's own projection
|
|
757
|
+
* applies (`restrictProjection`'s ceiling fallback).
|
|
758
|
+
*
|
|
759
|
+
* An exclusion result names nested-object parents by their leaves
|
|
760
|
+
* (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
|
|
761
|
+
* exclusion against their leaf columns, so a parent key alone would strip
|
|
762
|
+
* nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
|
|
602
763
|
*/
|
|
603
|
-
function applyArbacProjection(projection, scopes) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
764
|
+
function applyArbacProjection(projection, scopes, readable) {
|
|
765
|
+
return restrictSelect(projection, scopeVisibility(scopes, readable));
|
|
766
|
+
}
|
|
767
|
+
/** {@link applyArbacProjection} over an already-built visibility level. */
|
|
768
|
+
function restrictSelect(projection, vis) {
|
|
769
|
+
if (Object.keys(vis.allowed).length === 0) return projection;
|
|
770
|
+
const childrenOf = fieldChildrenOf(vis.table);
|
|
771
|
+
return expandExcludeToLeaves(restrictProjection(normalizeSelect(projection) ?? {}, vis.allowed, childrenOf), childrenOf);
|
|
607
772
|
}
|
|
608
773
|
/**
|
|
609
774
|
* Enforce the union of per-scope `controls` gates against the parsed
|
|
@@ -625,43 +790,43 @@ function applyArbacControls(controls, scopes) {
|
|
|
625
790
|
* passes through unchanged. The outermost check skips the entire walk when no
|
|
626
791
|
* scope declares `with` at all — this is the common case on every read.
|
|
627
792
|
*/
|
|
628
|
-
function applyArbacRelationScopes(controls, scopes) {
|
|
793
|
+
function applyArbacRelationScopes(controls, scopes, readable) {
|
|
629
794
|
if (scopes.length === 0) return;
|
|
795
|
+
if (!scopes.some((s) => s.with)) return;
|
|
796
|
+
applyRelationLevel(controls, scopeVisibility(scopes, readable));
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* One `$with` level of {@link applyArbacRelationScopes}: each granted
|
|
800
|
+
* relation's child visibility already carries its sub-scopes and target table.
|
|
801
|
+
*/
|
|
802
|
+
function applyRelationLevel(controls, vis) {
|
|
630
803
|
const withArr = controls.$with;
|
|
631
804
|
if (!Array.isArray(withArr) || withArr.length === 0) return;
|
|
632
|
-
if (!scopes.some((s) => s.with)) return;
|
|
633
805
|
for (const raw of withArr) {
|
|
634
806
|
if (!raw || typeof raw !== "object") continue;
|
|
635
807
|
const entry = raw;
|
|
636
808
|
if (typeof entry.name !== "string") continue;
|
|
637
|
-
const
|
|
638
|
-
|
|
809
|
+
const child = vis.relation?.(entry.name);
|
|
810
|
+
const subScopes = child?.scopes ?? [];
|
|
811
|
+
if (!child || subScopes.length === 0) continue;
|
|
639
812
|
const conjoined = conjoinScopeFilters(mergeScopeFilters(subScopes.map((s) => s.filter ?? {})), entry.filter);
|
|
640
813
|
if (conjoined) entry.filter = conjoined;
|
|
641
814
|
const entryControls = entry.controls ?? {};
|
|
642
|
-
const restricted =
|
|
815
|
+
const restricted = restrictSelect(entryControls.$select, child);
|
|
643
816
|
if (restricted !== void 0) {
|
|
644
817
|
entryControls.$select = restricted;
|
|
645
818
|
entry.controls = entryControls;
|
|
646
819
|
}
|
|
647
820
|
enforceControlsPolicy(unionControlsPolicy(subScopes), entryControls);
|
|
648
|
-
|
|
821
|
+
if (subScopes.some((s) => s.with)) applyRelationLevel(entryControls, child);
|
|
649
822
|
}
|
|
650
823
|
}
|
|
651
|
-
/** Pick `scopes[i].with?.[name]`, dropping undefined. */
|
|
652
|
-
function collectSubScopes(scopes, name) {
|
|
653
|
-
const out = [];
|
|
654
|
-
for (const s of scopes) {
|
|
655
|
-
const sub = s.with?.[name];
|
|
656
|
-
if (sub) out.push(sub);
|
|
657
|
-
}
|
|
658
|
-
return out;
|
|
659
|
-
}
|
|
660
824
|
/**
|
|
661
825
|
* `$select` may be an array of field names (uniquery inclusion list) or a
|
|
662
|
-
* `TProjection` object. `
|
|
663
|
-
*
|
|
664
|
-
* undefined if the input is empty (caller
|
|
826
|
+
* `TProjection` object. `restrictProjection` operates on the object form
|
|
827
|
+
* (it would read `["a"]` as an exclusion keyed `"0"`), so normalize the array
|
|
828
|
+
* to `{ field: 1 }` first. Returns undefined if the input is empty (caller
|
|
829
|
+
* treats as "no projection set").
|
|
665
830
|
*/
|
|
666
831
|
function normalizeSelect(value) {
|
|
667
832
|
if (value === void 0 || value === null) return void 0;
|
|
@@ -679,7 +844,7 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
679
844
|
return transformArbacFilter(filter);
|
|
680
845
|
}
|
|
681
846
|
transformProjection(projection) {
|
|
682
|
-
return applyArbacProjection(projection, readCachedScopes());
|
|
847
|
+
return applyArbacProjection(projection, readCachedScopes(), this.readable);
|
|
683
848
|
}
|
|
684
849
|
/**
|
|
685
850
|
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
@@ -717,9 +882,9 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
717
882
|
* controls of a request. Runs after the base validator (which checks the
|
|
718
883
|
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
719
884
|
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
885
|
+
* Reads the scopes the route-level authorize interceptor cached via
|
|
886
|
+
* `arbac.setScopes(...)` — validation runs before `transformFilter`, so
|
|
887
|
+
* this hook never re-evaluates ARBAC.
|
|
723
888
|
*
|
|
724
889
|
* On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
|
|
725
890
|
* `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
|
|
@@ -730,27 +895,24 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
730
895
|
if (baseErr) return baseErr;
|
|
731
896
|
const scopes = readCachedScopes();
|
|
732
897
|
applyArbacControls(controls, scopes);
|
|
733
|
-
applyArbacRelationScopes(controls, scopes);
|
|
898
|
+
applyArbacRelationScopes(controls, scopes, this.readable);
|
|
734
899
|
}
|
|
735
900
|
applyMetaOverlay(meta) {
|
|
736
|
-
return applyArbacMetaOverlay(meta,
|
|
901
|
+
return applyArbacMetaOverlay(meta, this.readable);
|
|
737
902
|
}
|
|
738
903
|
/**
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* enforcement. Scopes were cached by the route-level authorize interceptor
|
|
749
|
-
* before validation runs (`useArbac` setScopes), so the union reflects the
|
|
750
|
-
* exact action being executed.
|
|
904
|
+
* Scope-aware field visibility (BUG-3 twin of the `/meta` pruning above): a
|
|
905
|
+
* field outside the read-scope projection union answers `false`, which
|
|
906
|
+
* moost-db's visibility hook turns into the same `Unknown field "x"` 400 a
|
|
907
|
+
* nonexistent field gets at every gated query position — see the docs'
|
|
908
|
+
* "Column-scope security floor". Identifiers stay visible. A `rel.x` path
|
|
909
|
+
* under a `with`-granted relation (`$with=rel(x>1)`, `$with=rel($sort=x)`,
|
|
910
|
+
* `$with=rel($select=x)`) is checked as `x` against the union of the
|
|
911
|
+
* `with.rel` sub-scopes, recursively; the related table's own PK /
|
|
912
|
+
* `preferredId` stay visible.
|
|
751
913
|
*/
|
|
752
914
|
hasField(path) {
|
|
753
|
-
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path,
|
|
915
|
+
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path, this.readable);
|
|
754
916
|
}
|
|
755
917
|
async onWrite(action, data) {
|
|
756
918
|
const scopes = readCachedScopes();
|
|
@@ -765,7 +927,7 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
765
927
|
const scopeFilter = mergeScopeFilters(scopes.map((s) => s.filter ?? {}));
|
|
766
928
|
if (!scopeFilter) return;
|
|
767
929
|
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
768
|
-
const idFilters = ids.map((id) => this.readable.resolveIdFilter(id));
|
|
930
|
+
const idFilters = ids.map((id) => this.readable.resolveIdFilter(id, this._idOpts));
|
|
769
931
|
if (idFilters.some((f) => !f)) throw new HttpError(404, "Not found");
|
|
770
932
|
const idFilter = idFilters.length === 1 ? idFilters[0] : { $or: idFilters };
|
|
771
933
|
if (await this.readable.count({ filter: { $and: [idFilter, scopeFilter] } }) < ids.length) throw new HttpError(404, "Not found");
|
|
@@ -914,14 +1076,14 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
914
1076
|
return transformArbacFilter(filter);
|
|
915
1077
|
}
|
|
916
1078
|
transformProjection(projection) {
|
|
917
|
-
return applyArbacProjection(projection, readCachedScopes());
|
|
1079
|
+
return applyArbacProjection(projection, readCachedScopes(), this.readable);
|
|
918
1080
|
}
|
|
919
1081
|
validateControls(controls, type) {
|
|
920
1082
|
const baseErr = super.validateControls(controls, type);
|
|
921
1083
|
if (baseErr) return baseErr;
|
|
922
1084
|
const scopes = readCachedScopes();
|
|
923
1085
|
applyArbacControls(controls, scopes);
|
|
924
|
-
applyArbacRelationScopes(controls, scopes);
|
|
1086
|
+
applyArbacRelationScopes(controls, scopes, this.readable);
|
|
925
1087
|
}
|
|
926
1088
|
/**
|
|
927
1089
|
* Same ARBAC `/meta` overlay as the writable controller (actions/crud
|
|
@@ -929,19 +1091,16 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
929
1091
|
* union) — view-style metas leak hidden field names identically.
|
|
930
1092
|
*/
|
|
931
1093
|
applyMetaOverlay(meta) {
|
|
932
|
-
return applyArbacMetaOverlay(meta,
|
|
1094
|
+
return applyArbacMetaOverlay(meta, this.readable);
|
|
933
1095
|
}
|
|
934
1096
|
/**
|
|
935
|
-
* Scope-aware field
|
|
936
|
-
* {@link AsArbacDbController.hasField}
|
|
937
|
-
* read-scope projection union answers `false`, so `validateInsights` rejects
|
|
938
|
-
* `$select` / filter / sort references to it with the identical
|
|
939
|
-
* `Unknown field "x"` 400 a nonexistent field gets.
|
|
1097
|
+
* Scope-aware field visibility — same contract as
|
|
1098
|
+
* {@link AsArbacDbController.hasField}.
|
|
940
1099
|
*/
|
|
941
1100
|
hasField(path) {
|
|
942
|
-
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path,
|
|
1101
|
+
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path, this.readable);
|
|
943
1102
|
}
|
|
944
1103
|
};
|
|
945
1104
|
AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
|
|
946
1105
|
//#endregion
|
|
947
|
-
export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
|
|
1106
|
+
export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, buildScopeVisibility, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { TClassConstructor } from "moost";
|
|
2
|
-
import { ControlGate, TProjection, TScopeFilter } from "@aooth/arbac";
|
|
2
|
+
import { ControlGate, TProjection, TProjectionChildren, TScopeFilter } from "@aooth/arbac";
|
|
3
|
+
import { TMetaResponse } from "@atscript/db";
|
|
3
4
|
import { AsDbController } from "@atscript/moost-db";
|
|
4
5
|
import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
5
|
-
import { TMetaResponse } from "@atscript/db";
|
|
6
6
|
//#region src/db/scope-types.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Unwrap a `NavPropsOf<T>[K]` value to its target model type:
|
|
@@ -152,9 +152,9 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
152
152
|
* controls of a request. Runs after the base validator (which checks the
|
|
153
153
|
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
154
154
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* Reads the scopes the route-level authorize interceptor cached via
|
|
156
|
+
* `arbac.setScopes(...)` — validation runs before `transformFilter`, so
|
|
157
|
+
* this hook never re-evaluates ARBAC.
|
|
158
158
|
*
|
|
159
159
|
* On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
|
|
160
160
|
* `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
|
|
@@ -163,18 +163,15 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
163
163
|
protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
|
|
164
164
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* enforcement. Scopes were cached by the route-level authorize interceptor
|
|
176
|
-
* before validation runs (`useArbac` setScopes), so the union reflects the
|
|
177
|
-
* exact action being executed.
|
|
166
|
+
* Scope-aware field visibility (BUG-3 twin of the `/meta` pruning above): a
|
|
167
|
+
* field outside the read-scope projection union answers `false`, which
|
|
168
|
+
* moost-db's visibility hook turns into the same `Unknown field "x"` 400 a
|
|
169
|
+
* nonexistent field gets at every gated query position — see the docs'
|
|
170
|
+
* "Column-scope security floor". Identifiers stay visible. A `rel.x` path
|
|
171
|
+
* under a `with`-granted relation (`$with=rel(x>1)`, `$with=rel($sort=x)`,
|
|
172
|
+
* `$with=rel($select=x)`) is checked as `x` against the union of the
|
|
173
|
+
* `with.rel` sub-scopes, recursively; the related table's own PK /
|
|
174
|
+
* `preferredId` stay visible.
|
|
178
175
|
*/
|
|
179
176
|
protected hasField(path: string): boolean;
|
|
180
177
|
protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
|
|
@@ -262,15 +259,24 @@ interface AoothArbacClaims {
|
|
|
262
259
|
*
|
|
263
260
|
* Each side is first UNIONed with the existing additive helpers (today's
|
|
264
261
|
* machinery), then the two RESULTS are CONJOINED with the dedicated combiners
|
|
265
|
-
* ({@link conjoinScopeFilters} `$and`, {@link
|
|
262
|
+
* ({@link conjoinScopeFilters} `$and`, {@link intersectProjections} field ∩,
|
|
266
263
|
* {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
|
|
267
264
|
* additive union helpers, which would silently widen.
|
|
268
265
|
*
|
|
269
266
|
* Returned as a single-element list so every downstream scope-application
|
|
270
267
|
* site (which UNIONs the cached scope list per facet) sees the identity of a
|
|
271
268
|
* one-element union — i.e. the conjunction — with no change to those sites.
|
|
269
|
+
*
|
|
270
|
+
* The projection conjunction is path-wise and never wider than either side
|
|
271
|
+
* (`{a:1}` ∩ `{"a.b":1}` → `{"a.b":1}`). A nested exclusion under an
|
|
272
|
+
* included parent (`{a:1}` ∩ `{"a.c":0}`) is subtracted exactly via
|
|
273
|
+
* `childrenOf` (the table schema); without it the parent is dropped (fail
|
|
274
|
+
* closed). When NO field survives (`{a:1}` ∩ `{a:0}`, disjoint whitelists),
|
|
275
|
+
* the composite keeps the user's projection and gets a match-nothing filter —
|
|
276
|
+
* an empty field set must never read as the unrestricted `{}`. `with`
|
|
277
|
+
* sub-scopes are conjoined without a schema.
|
|
272
278
|
*/
|
|
273
|
-
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[]): ArbacDbScope[];
|
|
279
|
+
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[], childrenOf?: TProjectionChildren): ArbacDbScope[];
|
|
274
280
|
//#endregion
|
|
275
281
|
//#region src/user.provider.d.ts
|
|
276
282
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -60,17 +60,17 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@uniqu/core": "^0.1.
|
|
64
|
-
"@aooth/arbac": "0.1.
|
|
65
|
-
"@aooth/
|
|
66
|
-
"@aooth/
|
|
63
|
+
"@uniqu/core": "^0.1.10",
|
|
64
|
+
"@aooth/arbac": "0.1.68",
|
|
65
|
+
"@aooth/arbac-core": "0.1.68",
|
|
66
|
+
"@aooth/user": "0.1.68"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@atscript/core": "^0.1.93",
|
|
70
|
-
"@atscript/db": "^0.1.
|
|
71
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
72
|
-
"@atscript/db-sqlite": "^0.1.
|
|
73
|
-
"@atscript/moost-db": "^0.1.
|
|
70
|
+
"@atscript/db": "^0.1.134",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.134",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.134",
|
|
73
|
+
"@atscript/moost-db": "^0.1.134",
|
|
74
74
|
"@atscript/typescript": "^0.1.93",
|
|
75
75
|
"@moostjs/event-http": "^0.6.37",
|
|
76
76
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"unplugin-atscript": "^0.1.93"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@atscript/db": "^0.1.
|
|
82
|
-
"@atscript/moost-db": "^0.1.
|
|
81
|
+
"@atscript/db": "^0.1.134",
|
|
82
|
+
"@atscript/moost-db": "^0.1.134",
|
|
83
83
|
"@atscript/typescript": "^0.1.93",
|
|
84
84
|
"@moostjs/event-http": "^0.6.37",
|
|
85
85
|
"@wooksjs/event-core": "^0.7.23",
|