@aooth/arbac-moost 0.1.67 → 0.1.69
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.
|
@@ -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;
|
|
@@ -156,7 +165,10 @@ declare function collectWithGrantNames(scopes: ArbacDbScope[]): ReadonlySet<stri
|
|
|
156
165
|
* (or per `/meta` overlay) and threaded through the pruning walk.
|
|
157
166
|
*/
|
|
158
167
|
interface MetaVisibility {
|
|
159
|
-
/**
|
|
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
|
+
*/
|
|
160
172
|
allowed: TProjection;
|
|
161
173
|
/**
|
|
162
174
|
* Identifier fields reads ALWAYS return regardless of projection (the read
|
|
@@ -166,6 +178,25 @@ interface MetaVisibility {
|
|
|
166
178
|
alwaysVisible: ReadonlySet<string>;
|
|
167
179
|
/** Relation names granted via `with.<name>` (see {@link collectWithGrantNames}). */
|
|
168
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;
|
|
169
200
|
/**
|
|
170
201
|
* Fields the principal's WRITE scopes allow (union of `allowedFields`, or
|
|
171
202
|
* `"all"` for an unrestricted write grant). A field that is writable but not
|
|
@@ -184,10 +215,12 @@ interface MetaVisibility {
|
|
|
184
215
|
*/
|
|
185
216
|
declare function collectWritableFields(scopes: ArbacDbScope[], unrestricted: boolean): ReadonlySet<string> | "all" | undefined;
|
|
186
217
|
/**
|
|
187
|
-
* Whether a flattened dot-path field exists for this principal. A
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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).
|
|
191
224
|
*/
|
|
192
225
|
declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
193
226
|
/**
|
|
@@ -198,8 +231,9 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
|
198
231
|
*
|
|
199
232
|
* - `fields` — the flat capability map (sortable/filterable flags).
|
|
200
233
|
* - `type` — the serialized annotated type (what dynamic clients build
|
|
201
|
-
* tables/forms from); nested object props prune by dot-path
|
|
202
|
-
*
|
|
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`.
|
|
203
237
|
* - `relations` — entries neither projected nor `with`-granted.
|
|
204
238
|
* - `versionColumn` — dropped when the OCC column itself is hidden.
|
|
205
239
|
*
|
|
@@ -207,19 +241,59 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
|
207
241
|
* (`applyMetaOverlay` contract); every pruned branch is a fresh object.
|
|
208
242
|
*/
|
|
209
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;
|
|
210
280
|
/**
|
|
211
281
|
* Shared body of both ARBAC controllers' `hasField` overrides (BUG-3 twin of
|
|
212
|
-
* the `/meta` pruning): a field outside the read-scope projection union
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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
|
|
215
289
|
*/
|
|
216
|
-
declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string,
|
|
290
|
+
declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
|
|
217
291
|
/**
|
|
218
292
|
* PK + `preferredId` for a controller's table/readable — the
|
|
219
|
-
* {@link MetaVisibility.alwaysVisible} set
|
|
220
|
-
* `
|
|
293
|
+
* {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
|
|
294
|
+
* controllers now pass `this.readable` and it is derived there.
|
|
221
295
|
*/
|
|
222
|
-
declare function metaAlwaysVisibleFields(
|
|
296
|
+
declare function metaAlwaysVisibleFields(_controller: object, source: {
|
|
223
297
|
primaryKeys: readonly string[];
|
|
224
298
|
preferredId: readonly string[];
|
|
225
299
|
}): ReadonlySet<string>;
|
|
@@ -239,11 +313,14 @@ declare function metaAlwaysVisibleFields(controller: object, source: {
|
|
|
239
313
|
* universe, no pruning (unchanged behavior for unscoped roles). No read op
|
|
240
314
|
* allowed → the projection union is empty → no pruning: `crud` already
|
|
241
315
|
* advertises no read surface, and write-only principals still need `type`
|
|
242
|
-
* for their insert/update forms.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
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
|
|
245
322
|
*/
|
|
246
|
-
declare function applyArbacMetaOverlay(meta: TMetaResponse,
|
|
323
|
+
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>): Promise<TMetaResponse>;
|
|
247
324
|
/**
|
|
248
325
|
* Test-friendly internal helper — exported for unit tests and helper
|
|
249
326
|
* composition; regular consumers should not call this directly.
|
|
@@ -272,4 +349,4 @@ declare function collectMethodNames(instance: object): string[];
|
|
|
272
349
|
*/
|
|
273
350
|
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
|
|
274
351
|
//#endregion
|
|
275
|
-
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
|
|
@@ -730,21 +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
904
|
* Scope-aware field visibility (BUG-3 twin of the `/meta` pruning above): a
|
|
740
905
|
* field outside the read-scope projection union answers `false`, which
|
|
741
906
|
* moost-db's visibility hook turns into the same `Unknown field "x"` 400 a
|
|
742
907
|
* nonexistent field gets at every gated query position — see the docs'
|
|
743
|
-
* "Column-scope security floor". Identifiers stay visible
|
|
744
|
-
* `with`-granted relation
|
|
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.
|
|
745
913
|
*/
|
|
746
914
|
hasField(path) {
|
|
747
|
-
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path,
|
|
915
|
+
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path, this.readable);
|
|
748
916
|
}
|
|
749
917
|
async onWrite(action, data) {
|
|
750
918
|
const scopes = readCachedScopes();
|
|
@@ -759,7 +927,7 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
759
927
|
const scopeFilter = mergeScopeFilters(scopes.map((s) => s.filter ?? {}));
|
|
760
928
|
if (!scopeFilter) return;
|
|
761
929
|
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
762
|
-
const idFilters = ids.map((id) => this.readable.resolveIdFilter(id));
|
|
930
|
+
const idFilters = ids.map((id) => this.readable.resolveIdFilter(id, this._idOpts));
|
|
763
931
|
if (idFilters.some((f) => !f)) throw new HttpError(404, "Not found");
|
|
764
932
|
const idFilter = idFilters.length === 1 ? idFilters[0] : { $or: idFilters };
|
|
765
933
|
if (await this.readable.count({ filter: { $and: [idFilter, scopeFilter] } }) < ids.length) throw new HttpError(404, "Not found");
|
|
@@ -908,14 +1076,14 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
908
1076
|
return transformArbacFilter(filter);
|
|
909
1077
|
}
|
|
910
1078
|
transformProjection(projection) {
|
|
911
|
-
return applyArbacProjection(projection, readCachedScopes());
|
|
1079
|
+
return applyArbacProjection(projection, readCachedScopes(), this.readable);
|
|
912
1080
|
}
|
|
913
1081
|
validateControls(controls, type) {
|
|
914
1082
|
const baseErr = super.validateControls(controls, type);
|
|
915
1083
|
if (baseErr) return baseErr;
|
|
916
1084
|
const scopes = readCachedScopes();
|
|
917
1085
|
applyArbacControls(controls, scopes);
|
|
918
|
-
applyArbacRelationScopes(controls, scopes);
|
|
1086
|
+
applyArbacRelationScopes(controls, scopes, this.readable);
|
|
919
1087
|
}
|
|
920
1088
|
/**
|
|
921
1089
|
* Same ARBAC `/meta` overlay as the writable controller (actions/crud
|
|
@@ -923,16 +1091,16 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
923
1091
|
* union) — view-style metas leak hidden field names identically.
|
|
924
1092
|
*/
|
|
925
1093
|
applyMetaOverlay(meta) {
|
|
926
|
-
return applyArbacMetaOverlay(meta,
|
|
1094
|
+
return applyArbacMetaOverlay(meta, this.readable);
|
|
927
1095
|
}
|
|
928
1096
|
/**
|
|
929
1097
|
* Scope-aware field visibility — same contract as
|
|
930
1098
|
* {@link AsArbacDbController.hasField}.
|
|
931
1099
|
*/
|
|
932
1100
|
hasField(path) {
|
|
933
|
-
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path,
|
|
1101
|
+
return super.hasField(path) && isScopedFieldVisible(readCachedScopes(), path, this.readable);
|
|
934
1102
|
}
|
|
935
1103
|
};
|
|
936
1104
|
AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
|
|
937
1105
|
//#endregion
|
|
938
|
-
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:
|
|
@@ -167,8 +167,11 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
167
167
|
* field outside the read-scope projection union answers `false`, which
|
|
168
168
|
* moost-db's visibility hook turns into the same `Unknown field "x"` 400 a
|
|
169
169
|
* nonexistent field gets at every gated query position — see the docs'
|
|
170
|
-
* "Column-scope security floor". Identifiers stay visible
|
|
171
|
-
* `with`-granted relation
|
|
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.
|
|
172
175
|
*/
|
|
173
176
|
protected hasField(path: string): boolean;
|
|
174
177
|
protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
|
|
@@ -256,15 +259,24 @@ interface AoothArbacClaims {
|
|
|
256
259
|
*
|
|
257
260
|
* Each side is first UNIONed with the existing additive helpers (today's
|
|
258
261
|
* machinery), then the two RESULTS are CONJOINED with the dedicated combiners
|
|
259
|
-
* ({@link conjoinScopeFilters} `$and`, {@link
|
|
262
|
+
* ({@link conjoinScopeFilters} `$and`, {@link intersectProjections} field ∩,
|
|
260
263
|
* {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
|
|
261
264
|
* additive union helpers, which would silently widen.
|
|
262
265
|
*
|
|
263
266
|
* Returned as a single-element list so every downstream scope-application
|
|
264
267
|
* site (which UNIONs the cached scope list per facet) sees the identity of a
|
|
265
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.
|
|
266
278
|
*/
|
|
267
|
-
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[]): ArbacDbScope[];
|
|
279
|
+
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[], childrenOf?: TProjectionChildren): ArbacDbScope[];
|
|
268
280
|
//#endregion
|
|
269
281
|
//#region src/user.provider.d.ts
|
|
270
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.69",
|
|
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/user": "0.1.
|
|
66
|
-
"@aooth/arbac-core": "0.1.
|
|
63
|
+
"@uniqu/core": "^0.1.11",
|
|
64
|
+
"@aooth/arbac": "0.1.69",
|
|
65
|
+
"@aooth/user": "0.1.69",
|
|
66
|
+
"@aooth/arbac-core": "0.1.69"
|
|
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.136",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.136",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.136",
|
|
73
|
+
"@atscript/moost-db": "^0.1.136",
|
|
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.136",
|
|
82
|
+
"@atscript/moost-db": "^0.1.136",
|
|
83
83
|
"@atscript/typescript": "^0.1.93",
|
|
84
84
|
"@moostjs/event-http": "^0.6.37",
|
|
85
85
|
"@wooksjs/event-core": "^0.7.23",
|