@aooth/arbac-moost 0.1.73 → 0.1.74
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-CKjmUhkx.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,10 +1,10 @@
|
|
|
1
|
-
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-
|
|
1
|
+
import { _ as ProjectionOf, a as conjoinArbacDbScopes, b as TArbacMeta, c as ArbacDbScope, d as extractUsedControlValues, f as applyAllowedFieldsAndSet, g as OwnFieldKey, h as NavTarget, i as ConjoinArbacDbScopesOptions, l as AsArbacDbController, m as NavRelationKey, n as ArbacUserProviderToken, o as RefForeignKey, p as ControlsOf, r as AoothArbacClaims, s as RefTableSource, t as ArbacUserProvider, u as enforceControlsPolicy, v as ARBAC_DELEGATED_AUTH, x as getArbacMate, y as ArbacDelegatedAuth } from "./user.provider-CKjmUhkx.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
|
+
import { ControlGate, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
|
|
3
4
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
4
5
|
import { EventContext } from "@wooksjs/event-core";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
|
|
6
|
+
import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions, UniqueryControls } from "@atscript/db";
|
|
7
|
+
import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbActionScopeContext, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
|
|
8
8
|
import { TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
|
|
9
9
|
//#region src/db/visibility.d.ts
|
|
10
10
|
/**
|
|
@@ -54,8 +54,6 @@ interface VisibilityTableSource {
|
|
|
54
54
|
* @since 0.1.72
|
|
55
55
|
*/
|
|
56
56
|
jsonParents?: ReadonlySet<string>;
|
|
57
|
-
/** Whether the adapter searches natively (`/meta` search-surface pruning). @since 0.1.72 */
|
|
58
|
-
isSearchable?(): boolean;
|
|
59
57
|
}
|
|
60
58
|
/** Relation name → the visibility its joined rows obey, or `null` when hidden. */
|
|
61
59
|
type ArbacRelationResolution = ReadonlyMap<string, MetaVisibility | null>;
|
|
@@ -291,6 +289,7 @@ declare const ArbacResource: (name: string) => MethodDecorator & ClassDecorator
|
|
|
291
289
|
declare const ArbacAction: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
|
|
292
290
|
//#endregion
|
|
293
291
|
//#region src/db/as-arbac-db-readable-controller.d.ts
|
|
292
|
+
type TSelect = UniqueryControls["$select"];
|
|
294
293
|
/**
|
|
295
294
|
* Read-only mirror of {@link AsArbacDbController} for view-style controllers
|
|
296
295
|
* built on top of `@atscript/moost-db`'s {@link AsDbReadableController}.
|
|
@@ -303,9 +302,11 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
303
302
|
/** Same contract as {@link AsArbacDbController.prepareRequest}. */
|
|
304
303
|
protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
|
|
305
304
|
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
306
|
-
protected transformFilter(filter: Record<string, unknown> | undefined):
|
|
305
|
+
protected transformFilter(filter: Record<string, unknown> | undefined): FilterExpr | Promise<FilterExpr>;
|
|
306
|
+
/** Same contract as {@link AsArbacDbController.transformRelationFilter}. Since 0.1.74. */
|
|
307
|
+
protected transformRelationFilter(path: string, filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
|
|
307
308
|
/** Same contract as {@link AsArbacDbController.transformProjection}. */
|
|
308
|
-
protected transformProjection(projection?:
|
|
309
|
+
protected transformProjection(projection?: TSelect): TSelect | undefined | Promise<TSelect | undefined>;
|
|
309
310
|
/** Same contract as {@link AsArbacDbController.validateControls}. */
|
|
310
311
|
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
311
312
|
/** Same contract as {@link AsArbacDbController.applyMetaOverlay}. */
|
|
@@ -313,7 +314,7 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
313
314
|
/** Same contract as {@link AsArbacDbController.authorizeForm}. */
|
|
314
315
|
protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
|
|
315
316
|
/** Same contract as {@link AsArbacDbController.actionRowScope}. */
|
|
316
|
-
protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
|
|
317
|
+
protected actionRowScope(name: string, _ctx?: TDbActionScopeContext): Promise<Record<string, unknown> | undefined>;
|
|
317
318
|
/** Same contract as {@link AsArbacDbController.allowedActions}. */
|
|
318
319
|
protected allowedActions(names: readonly string[]): Promise<string[]>;
|
|
319
320
|
/** Same contract as {@link AsArbacDbController.[ARBAC_DELEGATED_AUTH]}. */
|
|
@@ -360,21 +361,16 @@ declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedTy
|
|
|
360
361
|
private _arbacHost?;
|
|
361
362
|
private get arbacHost();
|
|
362
363
|
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
363
|
-
|
|
364
|
-
protected
|
|
364
|
+
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
365
|
+
protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
|
|
366
|
+
protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined | Promise<ValueHelpSelect<DataType> | undefined>;
|
|
365
367
|
protected hasField(path: string): boolean;
|
|
366
368
|
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
367
369
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
368
|
-
runQuery(url: string):
|
|
369
|
-
runPages(url: string):
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
itemsPerPage: number;
|
|
373
|
-
pages: number;
|
|
374
|
-
count: number;
|
|
375
|
-
}>;
|
|
376
|
-
runGetOne(id: string): Promise<DataType | import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody>>;
|
|
377
|
-
runGetOneComposite(query: Record<string, string>): Promise<DataType | import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody>>;
|
|
370
|
+
runQuery(url: string): ReturnType<AsValueHelpController<T, DataType>["runQuery"]>;
|
|
371
|
+
runPages(url: string): ReturnType<AsValueHelpController<T, DataType>["runPages"]>;
|
|
372
|
+
runGetOne(id: string): ReturnType<AsValueHelpController<T, DataType>["runGetOne"]>;
|
|
373
|
+
runGetOneComposite(query: Record<string, string>): ReturnType<AsValueHelpController<T, DataType>["runGetOneComposite"]>;
|
|
378
374
|
}
|
|
379
375
|
/**
|
|
380
376
|
* ARBAC-scoped mirror of `@atscript/moost-db`'s
|
|
@@ -398,21 +394,16 @@ declare class AsArbacJsonValueHelpController<T extends TAtscriptAnnotatedType =
|
|
|
398
394
|
private _arbacHost?;
|
|
399
395
|
private get arbacHost();
|
|
400
396
|
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
401
|
-
|
|
402
|
-
protected
|
|
397
|
+
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
398
|
+
protected transformFilter(filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
|
|
399
|
+
protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined | Promise<ValueHelpSelect<DataType> | undefined>;
|
|
403
400
|
protected hasField(path: string): boolean;
|
|
404
401
|
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
405
402
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
406
|
-
runQuery(url: string):
|
|
407
|
-
runPages(url: string):
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
itemsPerPage: number;
|
|
411
|
-
pages: number;
|
|
412
|
-
count: number;
|
|
413
|
-
}>;
|
|
414
|
-
runGetOne(id: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType>;
|
|
415
|
-
runGetOneComposite(query: Record<string, string>): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType>;
|
|
403
|
+
runQuery(url: string): ReturnType<AsJsonValueHelpController<T, DataType>["runQuery"]>;
|
|
404
|
+
runPages(url: string): ReturnType<AsJsonValueHelpController<T, DataType>["runPages"]>;
|
|
405
|
+
runGetOne(id: string): ReturnType<AsJsonValueHelpController<T, DataType>["runGetOne"]>;
|
|
406
|
+
runGetOneComposite(query: Record<string, string>): ReturnType<AsJsonValueHelpController<T, DataType>["runGetOneComposite"]>;
|
|
416
407
|
}
|
|
417
408
|
//#endregion
|
|
418
409
|
//#region src/db/relation-policy.d.ts
|
|
@@ -488,18 +479,17 @@ declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility,
|
|
|
488
479
|
* `crud` already advertises no read surface, and write-only principals
|
|
489
480
|
* still need `type` for their insert/update forms. PK + `preferredId` are
|
|
490
481
|
* never pruned.
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
482
|
+
*
|
|
483
|
+
* The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
|
|
484
|
+
* `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
|
|
485
|
+
* overridden `hasField`), after this overlay.
|
|
495
486
|
*
|
|
496
487
|
* @param source - the controller's `this.readable` (identifiers, relations
|
|
497
488
|
* and schema rules are derived from it); an identifier set (the 0.1.67
|
|
498
489
|
* signature) still works, without them
|
|
499
|
-
* @param
|
|
500
|
-
* it on the current controller when it has one
|
|
490
|
+
* @param _indexes - @deprecated ignored since 0.1.74 (moost-db prunes the search surface)
|
|
501
491
|
*/
|
|
502
|
-
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>,
|
|
492
|
+
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>, _indexes?: readonly TDbIndexFieldPaths[]): Promise<TMetaResponse>;
|
|
503
493
|
/**
|
|
504
494
|
* Test-friendly internal helper — exported for unit tests and helper
|
|
505
495
|
* composition; regular consumers should not call this directly.
|
|
@@ -566,7 +556,18 @@ declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, sour
|
|
|
566
556
|
* action id / row is read): resolve the request's scopes
|
|
567
557
|
* ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
|
|
568
558
|
* bypass) and, on read endpoints, the `$with` relation policy for the
|
|
569
|
-
* requested relations.
|
|
559
|
+
* requested relations — the `$with` entries and, since 0.1.74, the
|
|
560
|
+
* relations the client filter's relational predicates name (`ctx.filter`):
|
|
561
|
+
* a predicate on a relation is allowed exactly when `$with` of it is (same
|
|
562
|
+
* visibility, same `controls.$with` gates).
|
|
563
|
+
*
|
|
564
|
+
* Delegated endpoints (no grant of their own): `"availableActions"`
|
|
565
|
+
* (`GET /meta/actions…`) — some row-level action grant (or, on a
|
|
566
|
+
* `@DbActionsFrom` controller, none: its own part lists nothing);
|
|
567
|
+
* `"delegatedAction"` (`POST /delegated-actions/:name`, since 0.1.74) — the
|
|
568
|
+
* controller's READ grant, since the route only resolves which of the
|
|
569
|
+
* caller's readable rows match the query; the action itself is authorized
|
|
570
|
+
* by its source controller on every batch.
|
|
570
571
|
*
|
|
571
572
|
* @since 0.1.72
|
|
572
573
|
*/
|
|
@@ -593,12 +594,17 @@ declare function arbacAllowedActions(names: readonly string[]): Promise<string[]
|
|
|
593
594
|
* `@DbAction` `name` may run on — the filter of the caller's grant on the
|
|
594
595
|
* action ({@link actionArbacIds}: every handler method of the action, or a
|
|
595
596
|
* class-level entry's fallback ids), including custom-field row filters and
|
|
596
|
-
* credential attenuation.
|
|
597
|
-
*
|
|
598
|
-
*
|
|
597
|
+
* credential attenuation. Several grants union (`$or`); attenuation is
|
|
598
|
+
* already intersected into each. No grant → a match-nothing filter (fail
|
|
599
|
+
* closed); an unrestricted grant → `undefined`. When the current request IS
|
|
600
|
+
* that action (its resolved scopes are the same evaluation), the row overlay
|
|
599
601
|
* already applies it → `undefined`. Equal filters are returned as ONE
|
|
600
602
|
* object per request.
|
|
601
603
|
*
|
|
604
|
+
* Candidate-free on purpose (evaluated once per action per event) — see
|
|
605
|
+
* `AsArbacDbController.actionRowScope` to bound an action by its candidate
|
|
606
|
+
* rows.
|
|
607
|
+
*
|
|
602
608
|
* @since 0.1.72
|
|
603
609
|
*/
|
|
604
610
|
declare function arbacActionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
|
|
@@ -610,6 +616,20 @@ declare function arbacActionRowScope(name: string): Promise<Record<string, unkno
|
|
|
610
616
|
* @since 0.1.72
|
|
611
617
|
*/
|
|
612
618
|
declare function arbacRowFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
619
|
+
/**
|
|
620
|
+
* Body of the ARBAC DB controllers' `transformRelationFilter` (since 0.1.74):
|
|
621
|
+
* the operand of a CLIENT relational predicate at `path` (`ticket`,
|
|
622
|
+
* `ticket.team`, `tickets.issues` inside `$with=tickets(…)`) CONJOINED
|
|
623
|
+
* with the row filter of the related rows the caller may see there — the
|
|
624
|
+
* same policy `$with` of that relation applies: a declared `with.<rel>`
|
|
625
|
+
* sub-scope, else the caller's own read grant on the related table. So
|
|
626
|
+
* `$some` matches, and `$none` excludes, only on rows the caller could
|
|
627
|
+
* load. A relation the request did not resolve answers like an unknown
|
|
628
|
+
* field (400 — the request gate rejects it first).
|
|
629
|
+
*
|
|
630
|
+
* @since 0.1.74
|
|
631
|
+
*/
|
|
632
|
+
declare function arbacRelationFilter(path: string, filter: FilterExpr, readable: VisibilityTableSource): FilterExpr;
|
|
613
633
|
//#endregion
|
|
614
634
|
//#region src/db/shared-read-helpers.d.ts
|
|
615
635
|
/**
|
|
@@ -848,4 +868,4 @@ declare class MoostArbac<TUserAttrs extends object, TScope extends object> exten
|
|
|
848
868
|
getScopeFields(): TScopeFieldRules<TScope>;
|
|
849
869
|
}
|
|
850
870
|
//#endregion
|
|
851
|
-
export { ARBAC_DELEGATED_AUTH, type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, type ArbacDbScopeHelpers, ArbacDelegatedAuth, ArbacEvaluateOptions, type ArbacGuardedTable, type ArbacRelationResolution, ArbacResource, type ArbacScopedTable, ArbacUserProvider, ArbacUserProviderToken, type ArbacWriteTable, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, type ConjoinArbacDbScopesOptions, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type RefForeignKey, type RefTableSource, type ScopeVisibilityOptions, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
|
|
871
|
+
export { ARBAC_DELEGATED_AUTH, type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, type ArbacDbScopeHelpers, ArbacDelegatedAuth, ArbacEvaluateOptions, type ArbacGuardedTable, type ArbacRelationResolution, ArbacResource, type ArbacScopedTable, ArbacUserProvider, ArbacUserProviderToken, type ArbacWriteTable, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, type ConjoinArbacDbScopesOptions, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type RefForeignKey, type RefTableSource, type ScopeVisibilityOptions, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRelationFilter, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, conjoinScopeFilters, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { i as __decorate, n as ArbacUserProvider, r as ArbacUserProviderToken, t as __decorateMetadata } from "./decorateMetadata-CU_mqYGT.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
|
+
import { DB_SCOPE_KEYS, DENY_FILTER, ScopeFieldConfigError, applyScopeFieldFilters, conjoinScopeFilters, conjoinScopeFilters as conjoinScopeFilters$1, conjoinScopes, effectiveScope, expandExcludeToLeaves, getProjectionMode, intersectEnabled, isFieldAllowed, needsInheritedConjunction, normalizeScopes, restrictProjection, stableKey, unionOutcomes } from "@aooth/arbac";
|
|
3
4
|
import { Authenticate, HttpError } from "@moostjs/event-http";
|
|
4
5
|
import { cached, cachedBy, current, eventTypeKey, key, tryGetCurrent } from "@wooksjs/event-core";
|
|
5
6
|
import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getMoostMate, useControllerContext, useLogger } from "moost";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, discoverRowLevelActions, getDbEndpoint, resolveTerminalRef } from "@atscript/moost-db";
|
|
7
|
+
import { computeInsights, containsRelationFilter, findAncestorInSet, getPath, hasRelationOp, isPlainObject, isRelationOp } from "@atscript/db";
|
|
8
|
+
import { AsDbController, AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, DB_CRUD_HANDLERS, VALUE_HELP_CRUD_HANDLERS, badRequest, discoverRowLevelActions, getDbEndpoint, hasActionDelegations, resolveTerminalRef } from "@atscript/moost-db";
|
|
9
9
|
import { groupByFields, walkFilter } from "@uniqu/core";
|
|
10
10
|
//#region src/db/helpers.ts
|
|
11
11
|
/** `map.get(key)`, created with `create()` and stored on a miss. */
|
|
@@ -110,6 +110,10 @@ function isMetaFieldVisible(path, vis) {
|
|
|
110
110
|
}
|
|
111
111
|
return allowed(path);
|
|
112
112
|
}
|
|
113
|
+
/** The level of relation `name` when it is visible at `vis` (see {@link isMetaFieldVisible}). */
|
|
114
|
+
function visibleRelation(name, vis) {
|
|
115
|
+
return isMetaFieldVisible(name, vis) ? vis.relation?.(name) : void 0;
|
|
116
|
+
}
|
|
113
117
|
const NO_NAMES$1 = /* @__PURE__ */ new Set();
|
|
114
118
|
const identifierSetCache = /* @__PURE__ */ new WeakMap();
|
|
115
119
|
/** PK + `preferredId` of a table — the identifiers reads always return. */
|
|
@@ -358,7 +362,9 @@ async function evaluateTypeRead(type) {
|
|
|
358
362
|
const MAX_WITH_DEPTH = 16;
|
|
359
363
|
/**
|
|
360
364
|
* Relation names a parsed `$with` control reaches, per level. A dotted
|
|
361
|
-
* name (`a.b`) nests like `a($with=b)`.
|
|
365
|
+
* name (`a.b`) nests like `a($with=b)`. The relational predicates of each
|
|
366
|
+
* entry's sub-filter (`$with=a(b=$some(…))`) join the entry's level
|
|
367
|
+
* ({@link relationFilterNameTree}).
|
|
362
368
|
*/
|
|
363
369
|
function withNameTree(withArr, into = /* @__PURE__ */ new Map(), depth = 0) {
|
|
364
370
|
if (!Array.isArray(withArr) || depth >= MAX_WITH_DEPTH) return into;
|
|
@@ -370,7 +376,29 @@ function withNameTree(withArr, into = /* @__PURE__ */ new Map(), depth = 0) {
|
|
|
370
376
|
if (depth + segments.length > MAX_WITH_DEPTH) continue;
|
|
371
377
|
let level = into;
|
|
372
378
|
for (const segment of segments) level = getOrCreate(level, segment, () => /* @__PURE__ */ new Map());
|
|
373
|
-
if (entry && typeof entry === "object")
|
|
379
|
+
if (entry && typeof entry === "object") {
|
|
380
|
+
withNameTree(entry.controls?.$with ?? entry.$with, level, depth + segments.length);
|
|
381
|
+
relationFilterNameTree(entry.filter, level, depth + segments.length);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return into;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Relation names the relational predicates of a filter reach, per level
|
|
388
|
+
* (`ticket=$some(team=$some(…))` → `ticket → { team }`), through
|
|
389
|
+
* `$and` / `$or` / `$not` and nested predicate operands. Only reads
|
|
390
|
+
* `filter` (moost-db hands `prepareRequest` a deep-frozen copy).
|
|
391
|
+
*
|
|
392
|
+
* @since 0.1.74
|
|
393
|
+
*/
|
|
394
|
+
function relationFilterNameTree(filter, into = /* @__PURE__ */ new Map(), depth = 0) {
|
|
395
|
+
if (!filter || typeof filter !== "object" || depth >= MAX_WITH_DEPTH) return into;
|
|
396
|
+
for (const [k, value] of Object.entries(filter)) if (k === "$and" || k === "$or") {
|
|
397
|
+
if (Array.isArray(value)) for (const c of value) relationFilterNameTree(c, into, depth);
|
|
398
|
+
} else if (k === "$not") relationFilterNameTree(value, into, depth);
|
|
399
|
+
else if (!k.startsWith("$") && hasRelationOp(value)) {
|
|
400
|
+
const level = getOrCreate(into, k, () => /* @__PURE__ */ new Map());
|
|
401
|
+
for (const [op, operand] of Object.entries(value)) if (isRelationOp(op)) relationFilterNameTree(operand, level, depth + 1);
|
|
374
402
|
}
|
|
375
403
|
return into;
|
|
376
404
|
}
|
|
@@ -412,19 +440,35 @@ async function resolveRelationTree(scopes, table, names, opts = {}) {
|
|
|
412
440
|
const requestRelationsKey = key("arbac.db.relations");
|
|
413
441
|
/**
|
|
414
442
|
* Per-request `$with` policy for the ARBAC DB controllers' `prepareRequest`:
|
|
415
|
-
* resolves every relation `controls.$with` names (recursively) and
|
|
416
|
-
*
|
|
417
|
-
* `
|
|
443
|
+
* resolves every relation `controls.$with` names (recursively) and — since
|
|
444
|
+
* 0.1.74 — every relation the client `filter`'s relational predicates name
|
|
445
|
+
* (`ticket=$some(…)`, also inside `$with` sub-filters), and stores the
|
|
446
|
+
* resolution the request's visibility reads.
|
|
447
|
+
*
|
|
448
|
+
* @returns the relation names resolved, per level
|
|
418
449
|
*/
|
|
419
|
-
async function resolveRequestRelations(scopes, controls, readable) {
|
|
450
|
+
async function resolveRequestRelations(scopes, controls, readable, filter) {
|
|
420
451
|
const names = withNameTree(controls?.$with);
|
|
421
|
-
|
|
422
|
-
|
|
452
|
+
const listSiblings = names.size > 0;
|
|
453
|
+
relationFilterNameTree(filter, names);
|
|
454
|
+
if (names.size === 0) return names;
|
|
455
|
+
const relations = await resolveRelationTree(scopes, readable, names, { listSiblings });
|
|
456
|
+
current().set(requestRelationsKey, {
|
|
457
|
+
readable,
|
|
458
|
+
relations
|
|
459
|
+
});
|
|
460
|
+
return names;
|
|
423
461
|
}
|
|
424
|
-
/**
|
|
425
|
-
|
|
462
|
+
/**
|
|
463
|
+
* The resolution {@link resolveRequestRelations} stored for the current
|
|
464
|
+
* event, if any — only for the readable it was resolved for: another
|
|
465
|
+
* controller evaluated in the same event (a `@DbActionsFrom` source) never
|
|
466
|
+
* sees the delegating request's `$with` resolution.
|
|
467
|
+
*/
|
|
468
|
+
function requestRelations(readable) {
|
|
426
469
|
const ctx = current();
|
|
427
|
-
|
|
470
|
+
const stored = ctx.has(requestRelationsKey) ? ctx.get(requestRelationsKey) : void 0;
|
|
471
|
+
return stored && stored.readable === readable ? stored.relations : void 0;
|
|
428
472
|
}
|
|
429
473
|
//#endregion
|
|
430
474
|
//#region src/db/write-refs.ts
|
|
@@ -741,6 +785,20 @@ function arbacIdsFromMeta(cMeta, mMeta, controller, methodName) {
|
|
|
741
785
|
action: mMeta?.arbacActionId || mMeta?.atscript_db_action?.name || cMeta?.arbacActionId || mMeta?.id || methodName
|
|
742
786
|
};
|
|
743
787
|
}
|
|
788
|
+
const scopeOwners = /* @__PURE__ */ new WeakMap();
|
|
789
|
+
/** Record that `scopes` were evaluated for `resource` / `action` (see {@link scopesOwnedBy}). */
|
|
790
|
+
function tagScopesOwner(scopes, resource, action) {
|
|
791
|
+
scopeOwners.set(scopes, `${resource}\u0000${action}`);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* `true` when the cached `scopes` were set for `resource` / `action`. An
|
|
795
|
+
* event slot read can fall through to a parent context (moost-db evaluates a
|
|
796
|
+
* `@DbActionsFrom` source in a child of the delegating event), so scopes
|
|
797
|
+
* cached for another handler are never taken as this handler's grant.
|
|
798
|
+
*/
|
|
799
|
+
function scopesOwnedBy(scopes, resource, action) {
|
|
800
|
+
return scopeOwners.get(scopes) === `${resource}\u0000${action}`;
|
|
801
|
+
}
|
|
744
802
|
/** The 403 every ARBAC deny answers. */
|
|
745
803
|
function insufficientPrivileges(resource, action) {
|
|
746
804
|
return new HttpError(403, `Insufficient privileges for action "${action}" on resource "${resource}"`);
|
|
@@ -899,6 +957,7 @@ const useArbac = (_ctx) => {
|
|
|
899
957
|
const cc = useControllerContext(ctx);
|
|
900
958
|
const getScopes = () => getArbacScopes(ctx);
|
|
901
959
|
const setScopes = (scope) => {
|
|
960
|
+
if (scope) tagScopesOwner(scope, resource, action);
|
|
902
961
|
ctx.set(arbacScopesKey, scope);
|
|
903
962
|
};
|
|
904
963
|
const cMeta = cc.getControllerMeta();
|
|
@@ -1222,22 +1281,21 @@ function crudHandlers(op, methods) {
|
|
|
1222
1281
|
* `crud` already advertises no read surface, and write-only principals
|
|
1223
1282
|
* still need `type` for their insert/update forms. PK + `preferredId` are
|
|
1224
1283
|
* never pruned.
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1284
|
+
*
|
|
1285
|
+
* The search surface (`searchIndexes`, `searchable`, `vectorSearchable`,
|
|
1286
|
+
* `geoSearchable`) is narrowed by moost-db itself (≥ 0.1.147, under the
|
|
1287
|
+
* overridden `hasField`), after this overlay.
|
|
1229
1288
|
*
|
|
1230
1289
|
* @param source - the controller's `this.readable` (identifiers, relations
|
|
1231
1290
|
* and schema rules are derived from it); an identifier set (the 0.1.67
|
|
1232
1291
|
* signature) still works, without them
|
|
1233
|
-
* @param
|
|
1234
|
-
* it on the current controller when it has one
|
|
1292
|
+
* @param _indexes - @deprecated ignored since 0.1.74 (moost-db prunes the search surface)
|
|
1235
1293
|
*/
|
|
1236
|
-
async function applyArbacMetaOverlay(meta, source,
|
|
1294
|
+
async function applyArbacMetaOverlay(meta, source, _indexes) {
|
|
1237
1295
|
const instance = useControllerContext().getController();
|
|
1238
1296
|
const { methods } = controllerMethodIndex(instance);
|
|
1239
1297
|
const crudKeys = Object.keys(meta.crud);
|
|
1240
|
-
const [allowedActions, crudScopes] = await Promise.all([grantedActions(instance, meta.actions.map((entry) => entry.name)), Promise.all(crudKeys.map((op) => evaluateHandlers(
|
|
1298
|
+
const [allowedActions, crudScopes] = await Promise.all([grantedActions(instance, meta.actions.map((entry) => entry.name)), Promise.all(crudKeys.map((op) => evaluateHandlers(crudHandlerIds(instance, op, methods))))]);
|
|
1241
1299
|
const allowed = new Set(allowedActions);
|
|
1242
1300
|
const actions = meta.actions.filter((entry) => allowed.has(entry.name));
|
|
1243
1301
|
const crud = {};
|
|
@@ -1267,11 +1325,11 @@ async function applyArbacMetaOverlay(meta, source, indexes) {
|
|
|
1267
1325
|
}
|
|
1268
1326
|
const vis = buildScopeVisibility(readScopes, source, { relations: await resolveRelationTree(readScopes, source, metaRelationNames(meta), { listSiblings: false }) });
|
|
1269
1327
|
const refTargets = await resolveRefTargets(vis, meta);
|
|
1270
|
-
return
|
|
1328
|
+
return pruneMetaByVisibility(overlaid, {
|
|
1271
1329
|
...vis,
|
|
1272
1330
|
writable,
|
|
1273
1331
|
nestedWrites
|
|
1274
|
-
}, refTargets)
|
|
1332
|
+
}, refTargets);
|
|
1275
1333
|
}
|
|
1276
1334
|
const metaNamesCache = /* @__PURE__ */ new WeakMap();
|
|
1277
1335
|
/** The relation names `/meta`'s serialized type reaches: root relations, then nav props of each nav body. */
|
|
@@ -1319,8 +1377,8 @@ async function resolveRefTargets(root, meta) {
|
|
|
1319
1377
|
if (!props || depth >= MAX_NAV_DEPTH) return;
|
|
1320
1378
|
for (const name of /* @__PURE__ */ new Set([...vis.relationNames ?? NO_NAMES, ...vis.withGrants])) {
|
|
1321
1379
|
const prop = props[name];
|
|
1322
|
-
if (!prop
|
|
1323
|
-
const sub =
|
|
1380
|
+
if (!prop) continue;
|
|
1381
|
+
const sub = visibleRelation(name, vis);
|
|
1324
1382
|
if (sub) visit(sub, prop, depth + 1);
|
|
1325
1383
|
}
|
|
1326
1384
|
};
|
|
@@ -1348,59 +1406,6 @@ function foreignRefTypes(table) {
|
|
|
1348
1406
|
});
|
|
1349
1407
|
}
|
|
1350
1408
|
const NO_TYPES = /* @__PURE__ */ new Set();
|
|
1351
|
-
/** Controls a text / vector search needs, dropped from `crud.query` / `crud.pages` when unusable. */
|
|
1352
|
-
const TEXT_SEARCH_CONTROLS = ["search", "fuzzy"];
|
|
1353
|
-
const VECTOR_CONTROLS = ["vector", "threshold"];
|
|
1354
|
-
/**
|
|
1355
|
-
* Drop `searchIndexes` reading a hidden field, recompute the search /
|
|
1356
|
-
* vector / geo flags over what is left (native search, or the visible
|
|
1357
|
-
* `@db.column.searchable` fallback), and remove the controls that became
|
|
1358
|
-
* unusable from the `query` / `pages` control lists (`crud.geo` goes with
|
|
1359
|
-
* the last visible geo index).
|
|
1360
|
-
*/
|
|
1361
|
-
function pruneSearchSurface(meta, original, vis, indexes) {
|
|
1362
|
-
if (!indexes) return meta;
|
|
1363
|
-
const shown = indexes.filter((e) => e.fields.every((f) => isMetaFieldVisible(f, vis)));
|
|
1364
|
-
const shownOf = (type) => shown.some((e) => e.type === type);
|
|
1365
|
-
const hasText = indexes.some((e) => e.type === "text");
|
|
1366
|
-
const native = vis.table?.isSearchable?.() ?? hasText;
|
|
1367
|
-
const fallback = searchFallbackVisible(meta, vis.table);
|
|
1368
|
-
const searchable = original.searchable && (native && (!hasText || shownOf("text")) || fallback);
|
|
1369
|
-
const vectorSearchable = original.vectorSearchable && shownOf("vector");
|
|
1370
|
-
const geoSearchable = original.geoSearchable === true ? shownOf("geo") : original.geoSearchable;
|
|
1371
|
-
const out = {
|
|
1372
|
-
...meta,
|
|
1373
|
-
searchIndexes: original.searchIndexes.filter((info) => shown.some((e) => e.name === info.name && e.type === (info.type ?? "text"))),
|
|
1374
|
-
searchable,
|
|
1375
|
-
vectorSearchable
|
|
1376
|
-
};
|
|
1377
|
-
if (geoSearchable !== void 0) out.geoSearchable = geoSearchable;
|
|
1378
|
-
const drop = /* @__PURE__ */ new Set();
|
|
1379
|
-
if (original.searchable && !searchable && !vectorSearchable) for (const c of TEXT_SEARCH_CONTROLS) drop.add(c);
|
|
1380
|
-
if (original.vectorSearchable && !vectorSearchable) for (const c of VECTOR_CONTROLS) drop.add(c);
|
|
1381
|
-
if (hasText && !shownOf("text")) drop.add("index");
|
|
1382
|
-
if (drop.size > 0 || original.geoSearchable && !geoSearchable) {
|
|
1383
|
-
const crud = { ...meta.crud };
|
|
1384
|
-
for (const op of ["query", "pages"]) {
|
|
1385
|
-
const list = crud[op];
|
|
1386
|
-
if (list && drop.size > 0) crud[op] = list.filter((c) => !drop.has(c));
|
|
1387
|
-
}
|
|
1388
|
-
if (original.geoSearchable && !geoSearchable) delete crud.geo;
|
|
1389
|
-
out.crud = crud;
|
|
1390
|
-
}
|
|
1391
|
-
return out;
|
|
1392
|
-
}
|
|
1393
|
-
/** A `@db.column.searchable` field is still listed (visible) in the pruned `fields`. */
|
|
1394
|
-
function searchFallbackVisible(meta, table) {
|
|
1395
|
-
if (!table?.flatMap) return false;
|
|
1396
|
-
for (const path of Object.keys(meta.fields)) if (table.flatMap.get(path)?.metadata?.has?.("db.column.searchable") && !meta.fields[path].writeOnly) return true;
|
|
1397
|
-
return false;
|
|
1398
|
-
}
|
|
1399
|
-
/** The current controller's `indexFieldPaths()` (moost-db 0.1.143 DB readables), if it has one. */
|
|
1400
|
-
function controllerIndexes(instance) {
|
|
1401
|
-
const c = instance;
|
|
1402
|
-
return typeof c.indexFieldPaths === "function" ? c.indexFieldPaths() : void 0;
|
|
1403
|
-
}
|
|
1404
1409
|
const methodIndexCache = /* @__PURE__ */ new WeakMap();
|
|
1405
1410
|
/**
|
|
1406
1411
|
* THE ARBAC ids an action is authorized as — used by the `/meta` overlay,
|
|
@@ -1432,14 +1437,20 @@ async function grantedActions(instance, names) {
|
|
|
1432
1437
|
function controllerArbacResource(instance) {
|
|
1433
1438
|
return arbacIdsFromMeta(getMoostMate().read(instance), void 0, instance, "").resource;
|
|
1434
1439
|
}
|
|
1435
|
-
/**
|
|
1440
|
+
/**
|
|
1441
|
+
* A controller class's methods and `@DbAction` name → methods map (memoized
|
|
1442
|
+
* per class). The method metadata is read from `instance` itself — never from
|
|
1443
|
+
* the event's current controller, which differs whenever another
|
|
1444
|
+
* controller's actions are evaluated in this event (a view's
|
|
1445
|
+
* `@DbActionsFrom` delegation).
|
|
1446
|
+
*/
|
|
1436
1447
|
function controllerMethodIndex(instance) {
|
|
1437
1448
|
return getOrCreate(methodIndexCache, getConstructor(instance), () => {
|
|
1438
|
-
const
|
|
1449
|
+
const mate = getMoostMate();
|
|
1439
1450
|
const methods = new Set(collectMethodNames(instance));
|
|
1440
1451
|
const actionMethods = /* @__PURE__ */ new Map();
|
|
1441
1452
|
for (const methodName of methods) {
|
|
1442
|
-
const name =
|
|
1453
|
+
const name = mate.read(instance, methodName)?.atscript_db_action?.name;
|
|
1443
1454
|
if (name) getOrCreate(actionMethods, name, () => []).push(methodName);
|
|
1444
1455
|
}
|
|
1445
1456
|
return {
|
|
@@ -1448,6 +1459,18 @@ function controllerMethodIndex(instance) {
|
|
|
1448
1459
|
};
|
|
1449
1460
|
});
|
|
1450
1461
|
}
|
|
1462
|
+
/** The ARBAC ids of the handler method(s) serving crud op `op` on `instance` (`useArbac()` precedence). */
|
|
1463
|
+
function crudHandlerIds(instance, op, methods = controllerMethodIndex(instance).methods) {
|
|
1464
|
+
return crudHandlers(op, methods).map((m) => resolveHandlerArbacIds(instance, m));
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* The ARBAC ids of `instance`'s READ handler — the `query` crud op's
|
|
1468
|
+
* handler method(s), exactly what the `/meta` overlay evaluates for
|
|
1469
|
+
* `crud.query`.
|
|
1470
|
+
*/
|
|
1471
|
+
function readHandlerIds(instance) {
|
|
1472
|
+
return crudHandlerIds(instance, "query");
|
|
1473
|
+
}
|
|
1451
1474
|
/**
|
|
1452
1475
|
* Test-friendly internal helper — exported for unit tests and helper
|
|
1453
1476
|
* composition; regular consumers should not call this directly.
|
|
@@ -1474,6 +1497,124 @@ function collectMethodNames(instance) {
|
|
|
1474
1497
|
return [...names];
|
|
1475
1498
|
}
|
|
1476
1499
|
//#endregion
|
|
1500
|
+
//#region src/db/shared-read-helpers.ts
|
|
1501
|
+
/**
|
|
1502
|
+
* Restrict the user-supplied `$select` to the scopes' projection union.
|
|
1503
|
+
* Returns the original projection untouched when no scope declares a
|
|
1504
|
+
* projection (so caller sees the unrestricted shape). Reads the same cached
|
|
1505
|
+
* visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
|
|
1506
|
+
* stripping and name hiding cannot drift.
|
|
1507
|
+
*
|
|
1508
|
+
* `projection` is the `$select` as moost-db parsed it: an inclusion is an
|
|
1509
|
+
* ARRAY of names, an exclusion an object — both are normalized first. A
|
|
1510
|
+
* requested parent (`$select=a`) narrows to what the scope shows: to the
|
|
1511
|
+
* whitelisted descendants (`a.b: 1`), or — for a hidden descendant
|
|
1512
|
+
* (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
|
|
1513
|
+
* the parent is dropped). Never the whole object, and never the empty
|
|
1514
|
+
* (universe) projection: when nothing survives, the scope's own projection
|
|
1515
|
+
* applies (`restrictProjection`'s ceiling fallback).
|
|
1516
|
+
*
|
|
1517
|
+
* An exclusion result names nested-object parents by their leaves
|
|
1518
|
+
* (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
|
|
1519
|
+
* exclusion against their leaf columns, so a parent key alone would strip
|
|
1520
|
+
* nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
|
|
1521
|
+
*
|
|
1522
|
+
* @since 0.1.72
|
|
1523
|
+
*/
|
|
1524
|
+
function applyArbacProjection(projection, scopes, readable) {
|
|
1525
|
+
return restrictSelect(projection, visibilityFor(scopes, readable));
|
|
1526
|
+
}
|
|
1527
|
+
/** {@link applyArbacProjection} over an already-built visibility level. */
|
|
1528
|
+
function restrictSelect(projection, vis) {
|
|
1529
|
+
if (Object.keys(vis.allowed).length === 0) return projection;
|
|
1530
|
+
const childrenOf = fieldChildrenOf(vis.table);
|
|
1531
|
+
return expandExcludeToLeaves(restrictProjection(normalizeSelect(projection) ?? {}, vis.allowed, childrenOf), childrenOf);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Enforce the union of per-scope `controls` gates against the parsed
|
|
1535
|
+
* Uniquery controls. Throws `HttpError(403)` on the first violation.
|
|
1536
|
+
*
|
|
1537
|
+
* @since 0.1.72
|
|
1538
|
+
*/
|
|
1539
|
+
function applyArbacControls(controls, scopes) {
|
|
1540
|
+
enforceControlsPolicy(effectiveScope(scopes).controls, controls);
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* The per-role `controls.$with` gates over every relation a read reaches —
|
|
1544
|
+
* its `$with` entries and its client relational predicates alike (see
|
|
1545
|
+
* `prepareArbacRequest`) — each level against that level's scopes.
|
|
1546
|
+
* Overlaps `validateControls`' gate for the `$with` entries on purpose: one
|
|
1547
|
+
* rule for both. Throws `HttpError(403)` on the first violation; a hidden
|
|
1548
|
+
* relation's nested names are left to moost-db (`Unknown field`).
|
|
1549
|
+
*
|
|
1550
|
+
* @since 0.1.74
|
|
1551
|
+
*/
|
|
1552
|
+
function enforceRelationGates(names, vis) {
|
|
1553
|
+
if (!vis?.scopes || names.size === 0) return;
|
|
1554
|
+
enforceControlsPolicy(effectiveScope(vis.scopes).controls, { $with: [...names.keys()] });
|
|
1555
|
+
for (const [name, nested] of names) if (nested.size > 0) enforceRelationGates(nested, visibleRelation(name, vis));
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Walk the user-supplied `$with` items in `controls` and overlay each joined
|
|
1559
|
+
* relation with the policy of its rows (the `$with` inherit-target policy):
|
|
1560
|
+
* a relation some scope declares `with.<name>` for gets the union of those
|
|
1561
|
+
* sub-scopes (parent authority — silent roles contribute nothing); any other
|
|
1562
|
+
* gets the caller's OWN read scopes on the related table, as resolved in
|
|
1563
|
+
* `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
|
|
1564
|
+
* restricted, per-relation `controls` gates enforced, recursively.
|
|
1565
|
+
*
|
|
1566
|
+
* Mutates `controls` in place (matches the `validateControls` contract).
|
|
1567
|
+
* Throws `HttpError(403)` if a per-relation control policy is violated. A
|
|
1568
|
+
* hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
|
|
1569
|
+
* level through `hasField`, byte-for-byte like a nonexistent one.
|
|
1570
|
+
*
|
|
1571
|
+
* @since 0.1.72
|
|
1572
|
+
*/
|
|
1573
|
+
function applyArbacRelationScopes(controls, scopes, readable) {
|
|
1574
|
+
const withArr = controls.$with;
|
|
1575
|
+
if (!Array.isArray(withArr) || withArr.length === 0) return;
|
|
1576
|
+
applyRelationLevel(controls, visibilityFor(scopes, readable));
|
|
1577
|
+
}
|
|
1578
|
+
/** One `$with` level of {@link applyArbacRelationScopes}. */
|
|
1579
|
+
function applyRelationLevel(controls, vis) {
|
|
1580
|
+
const withArr = controls.$with;
|
|
1581
|
+
if (!Array.isArray(withArr) || withArr.length === 0) return;
|
|
1582
|
+
for (const raw of withArr) {
|
|
1583
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1584
|
+
const entry = raw;
|
|
1585
|
+
if (typeof entry.name !== "string" || entry.name.includes(".")) continue;
|
|
1586
|
+
const level = (!vis.relationNames || vis.relationNames.has(entry.name)) && isMetaFieldVisible(entry.name, vis) ? vis.relation?.(entry.name) : void 0;
|
|
1587
|
+
if (!level) continue;
|
|
1588
|
+
const sub = effectiveScope(level.scopes ?? []);
|
|
1589
|
+
const conjoined = conjoinScopeFilters$1(sub.filter, entry.filter);
|
|
1590
|
+
if (conjoined) entry.filter = conjoined;
|
|
1591
|
+
const entryControls = entry.controls ?? {};
|
|
1592
|
+
const restricted = restrictSelect(entryControls.$select, level);
|
|
1593
|
+
if (restricted !== void 0) {
|
|
1594
|
+
entryControls.$select = restricted;
|
|
1595
|
+
entry.controls = entryControls;
|
|
1596
|
+
}
|
|
1597
|
+
enforceControlsPolicy(sub.controls, entryControls);
|
|
1598
|
+
applyRelationLevel(entryControls, level);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* `$select` may be an array of field names (uniquery inclusion list) or a
|
|
1603
|
+
* `TProjection` object. `restrictProjection` operates on the object form
|
|
1604
|
+
* (it would read `["a"]` as an exclusion keyed `"0"`), so normalize the array
|
|
1605
|
+
* to `{ field: 1 }` first. Returns undefined if the input is empty (caller
|
|
1606
|
+
* treats as "no projection set").
|
|
1607
|
+
*/
|
|
1608
|
+
function normalizeSelect(value) {
|
|
1609
|
+
if (value === void 0 || value === null) return void 0;
|
|
1610
|
+
if (Array.isArray(value)) {
|
|
1611
|
+
const out = {};
|
|
1612
|
+
for (const item of value) if (typeof item === "string") out[item] = 1;
|
|
1613
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1614
|
+
}
|
|
1615
|
+
if (typeof value === "object") return value;
|
|
1616
|
+
}
|
|
1617
|
+
//#endregion
|
|
1477
1618
|
//#region src/db/request-scopes.ts
|
|
1478
1619
|
/** Endpoints whose `prepareRequest` carries parsed Uniquery controls. */
|
|
1479
1620
|
const READ_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
@@ -1519,7 +1660,8 @@ function cachedRequestScopes() {
|
|
|
1519
1660
|
async function resolveRequestScopes() {
|
|
1520
1661
|
const ctx = current();
|
|
1521
1662
|
const arbac = useArbac(ctx);
|
|
1522
|
-
const
|
|
1663
|
+
const cachedScopes = arbac.getScopes();
|
|
1664
|
+
const fromInterceptor = cachedScopes && scopesOwnedBy(cachedScopes, arbac.resource, arbac.action) ? cachedScopes : void 0;
|
|
1523
1665
|
const prior = resolved();
|
|
1524
1666
|
if (prior && prior.resource === arbac.resource && prior.action === arbac.action && prior.source === fromInterceptor) return prior.scopes;
|
|
1525
1667
|
const scopes = normalizeScopes(fromInterceptor !== void 0 ? {
|
|
@@ -1551,7 +1693,7 @@ function requireRequestScopes() {
|
|
|
1551
1693
|
*/
|
|
1552
1694
|
function visibilityFor(scopes, source) {
|
|
1553
1695
|
const request = resolved();
|
|
1554
|
-
if (source && !isIdentifierSet(source) && request?.scopes === scopes) return getOrCreate(request.visibility, source, () => buildScopeVisibility(scopes, source, { relations: requestRelations() }));
|
|
1696
|
+
if (source && !isIdentifierSet(source) && request?.scopes === scopes) return getOrCreate(request.visibility, source, () => buildScopeVisibility(scopes, source, { relations: requestRelations(source) }));
|
|
1555
1697
|
return scopeVisibility(scopes, source);
|
|
1556
1698
|
}
|
|
1557
1699
|
/**
|
|
@@ -1585,7 +1727,18 @@ function isScopedFieldVisible(scopes, path, source) {
|
|
|
1585
1727
|
* action id / row is read): resolve the request's scopes
|
|
1586
1728
|
* ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
|
|
1587
1729
|
* bypass) and, on read endpoints, the `$with` relation policy for the
|
|
1588
|
-
* requested relations.
|
|
1730
|
+
* requested relations — the `$with` entries and, since 0.1.74, the
|
|
1731
|
+
* relations the client filter's relational predicates name (`ctx.filter`):
|
|
1732
|
+
* a predicate on a relation is allowed exactly when `$with` of it is (same
|
|
1733
|
+
* visibility, same `controls.$with` gates).
|
|
1734
|
+
*
|
|
1735
|
+
* Delegated endpoints (no grant of their own): `"availableActions"`
|
|
1736
|
+
* (`GET /meta/actions…`) — some row-level action grant (or, on a
|
|
1737
|
+
* `@DbActionsFrom` controller, none: its own part lists nothing);
|
|
1738
|
+
* `"delegatedAction"` (`POST /delegated-actions/:name`, since 0.1.74) — the
|
|
1739
|
+
* controller's READ grant, since the route only resolves which of the
|
|
1740
|
+
* caller's readable rows match the query; the action itself is authorized
|
|
1741
|
+
* by its source controller on every batch.
|
|
1589
1742
|
*
|
|
1590
1743
|
* @since 0.1.72
|
|
1591
1744
|
*/
|
|
@@ -1594,8 +1747,12 @@ async function prepareArbacRequest(ctx, readable) {
|
|
|
1594
1747
|
await authorizeAvailableActions();
|
|
1595
1748
|
return;
|
|
1596
1749
|
}
|
|
1750
|
+
if (ctx.endpoint === "delegatedAction") {
|
|
1751
|
+
await authorizeDelegatedQueryTarget();
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1597
1754
|
const scopes = await resolveRequestScopes();
|
|
1598
|
-
if (
|
|
1755
|
+
if (READ_ENDPOINTS.has(ctx.endpoint)) enforceRelationGates(await resolveRequestRelations(scopes, ctx.controls, readable, ctx.filter), visibilityFor(scopes, readable));
|
|
1599
1756
|
}
|
|
1600
1757
|
/**
|
|
1601
1758
|
* `authorizeForm` for the ARBAC DB controllers: a form is served iff the
|
|
@@ -1621,14 +1778,42 @@ function actionsIds(names) {
|
|
|
1621
1778
|
* overlay stays unrestricted — each action's rows come from
|
|
1622
1779
|
* `actionRowScope` ({@link arbacActionRowScope}) — while field visibility
|
|
1623
1780
|
* is never wider than the widest granted action.
|
|
1781
|
+
*
|
|
1782
|
+
* A `@DbActionsFrom` controller without an own row-level grant is served
|
|
1783
|
+
* too (since 0.1.74): its own part lists nothing (a match-nothing row
|
|
1784
|
+
* overlay), and each delegated action is answered by its source controller,
|
|
1785
|
+
* under the caller's grants THERE.
|
|
1624
1786
|
*/
|
|
1625
1787
|
async function authorizeAvailableActions() {
|
|
1626
1788
|
const ctx = current();
|
|
1627
1789
|
const instance = useControllerContext(ctx).getController();
|
|
1628
|
-
const
|
|
1790
|
+
const ctor = getConstructor(instance);
|
|
1791
|
+
const granted = await evaluateHandlers(actionsIds(discoverRowLevelActions(ctor, instance.app, instance.logger).map((e) => e.info.name)));
|
|
1629
1792
|
const arbac = useArbac(ctx);
|
|
1630
|
-
if (
|
|
1631
|
-
storeResolved(ctx, arbac, void 0,
|
|
1793
|
+
if (granted) storeResolved(ctx, arbac, void 0, granted.map(withoutRowPolicy));
|
|
1794
|
+
else if (hasActionDelegations(ctor)) storeResolved(ctx, arbac, void 0, NO_OWN_ACTIONS);
|
|
1795
|
+
else throw insufficientPrivileges(arbac.resource, arbac.action);
|
|
1796
|
+
}
|
|
1797
|
+
/** The own scopes of a delegating controller's `GET /meta/actions` without an own grant. */
|
|
1798
|
+
const NO_OWN_ACTIONS = [{
|
|
1799
|
+
filter: DENY_FILTER,
|
|
1800
|
+
check: DENY_FILTER
|
|
1801
|
+
}];
|
|
1802
|
+
/**
|
|
1803
|
+
* `POST /delegated-actions/:name` (endpoint `"delegatedAction"`): the route
|
|
1804
|
+
* resolves which rows of THIS controller match the query — a read, so it
|
|
1805
|
+
* is served under the caller's read grant (its `query` handler; none →
|
|
1806
|
+
* 403), stored as the request's scopes (`transformFilter`, `hasField` and
|
|
1807
|
+
* moost-db's default `queryTargetScope` read them). It grants nothing on
|
|
1808
|
+
* the action: the source controller's route re-authorizes every batch.
|
|
1809
|
+
*/
|
|
1810
|
+
async function authorizeDelegatedQueryTarget() {
|
|
1811
|
+
const ctx = current();
|
|
1812
|
+
const arbac = useArbac(ctx);
|
|
1813
|
+
const ids = readHandlerIds(useControllerContext(ctx).getController());
|
|
1814
|
+
const scopes = ids.length > 0 ? await evaluateHandlers(ids) : void 0;
|
|
1815
|
+
if (!scopes) throw insufficientPrivileges(ids[0]?.resource ?? arbac.resource, ids[0]?.action ?? "query");
|
|
1816
|
+
storeResolved(ctx, arbac, void 0, scopes);
|
|
1632
1817
|
}
|
|
1633
1818
|
/** A scope without its row policy (`filter` / `check`). */
|
|
1634
1819
|
function withoutRowPolicy(scope) {
|
|
@@ -1653,12 +1838,17 @@ const actionScopes = cached(() => /* @__PURE__ */ new Map());
|
|
|
1653
1838
|
* `@DbAction` `name` may run on — the filter of the caller's grant on the
|
|
1654
1839
|
* action ({@link actionArbacIds}: every handler method of the action, or a
|
|
1655
1840
|
* class-level entry's fallback ids), including custom-field row filters and
|
|
1656
|
-
* credential attenuation.
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1841
|
+
* credential attenuation. Several grants union (`$or`); attenuation is
|
|
1842
|
+
* already intersected into each. No grant → a match-nothing filter (fail
|
|
1843
|
+
* closed); an unrestricted grant → `undefined`. When the current request IS
|
|
1844
|
+
* that action (its resolved scopes are the same evaluation), the row overlay
|
|
1659
1845
|
* already applies it → `undefined`. Equal filters are returned as ONE
|
|
1660
1846
|
* object per request.
|
|
1661
1847
|
*
|
|
1848
|
+
* Candidate-free on purpose (evaluated once per action per event) — see
|
|
1849
|
+
* `AsArbacDbController.actionRowScope` to bound an action by its candidate
|
|
1850
|
+
* rows.
|
|
1851
|
+
*
|
|
1662
1852
|
* @since 0.1.72
|
|
1663
1853
|
*/
|
|
1664
1854
|
async function arbacActionRowScope(name) {
|
|
@@ -1681,110 +1871,26 @@ async function arbacActionRowScope(name) {
|
|
|
1681
1871
|
* @since 0.1.72
|
|
1682
1872
|
*/
|
|
1683
1873
|
function arbacRowFilter(filter) {
|
|
1684
|
-
return conjoinScopeFilters(effectiveScope(requireRequestScopes()).filter, filter) ?? {};
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
* ARRAY of names, an exclusion an object — both are normalized first. A
|
|
1697
|
-
* requested parent (`$select=a`) narrows to what the scope shows: to the
|
|
1698
|
-
* whitelisted descendants (`a.b: 1`), or — for a hidden descendant
|
|
1699
|
-
* (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
|
|
1700
|
-
* the parent is dropped). Never the whole object, and never the empty
|
|
1701
|
-
* (universe) projection: when nothing survives, the scope's own projection
|
|
1702
|
-
* applies (`restrictProjection`'s ceiling fallback).
|
|
1703
|
-
*
|
|
1704
|
-
* An exclusion result names nested-object parents by their leaves
|
|
1705
|
-
* (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
|
|
1706
|
-
* exclusion against their leaf columns, so a parent key alone would strip
|
|
1707
|
-
* nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
|
|
1708
|
-
*
|
|
1709
|
-
* @since 0.1.72
|
|
1710
|
-
*/
|
|
1711
|
-
function applyArbacProjection(projection, scopes, readable) {
|
|
1712
|
-
return restrictSelect(projection, visibilityFor(scopes, readable));
|
|
1713
|
-
}
|
|
1714
|
-
/** {@link applyArbacProjection} over an already-built visibility level. */
|
|
1715
|
-
function restrictSelect(projection, vis) {
|
|
1716
|
-
if (Object.keys(vis.allowed).length === 0) return projection;
|
|
1717
|
-
const childrenOf = fieldChildrenOf(vis.table);
|
|
1718
|
-
return expandExcludeToLeaves(restrictProjection(normalizeSelect(projection) ?? {}, vis.allowed, childrenOf), childrenOf);
|
|
1719
|
-
}
|
|
1720
|
-
/**
|
|
1721
|
-
* Enforce the union of per-scope `controls` gates against the parsed
|
|
1722
|
-
* Uniquery controls. Throws `HttpError(403)` on the first violation.
|
|
1723
|
-
*
|
|
1724
|
-
* @since 0.1.72
|
|
1725
|
-
*/
|
|
1726
|
-
function applyArbacControls(controls, scopes) {
|
|
1727
|
-
enforceControlsPolicy(effectiveScope(scopes).controls, controls);
|
|
1728
|
-
}
|
|
1729
|
-
/**
|
|
1730
|
-
* Walk the user-supplied `$with` items in `controls` and overlay each joined
|
|
1731
|
-
* relation with the policy of its rows (the `$with` inherit-target policy):
|
|
1732
|
-
* a relation some scope declares `with.<name>` for gets the union of those
|
|
1733
|
-
* sub-scopes (parent authority — silent roles contribute nothing); any other
|
|
1734
|
-
* gets the caller's OWN read scopes on the related table, as resolved in
|
|
1735
|
-
* `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
|
|
1736
|
-
* restricted, per-relation `controls` gates enforced, recursively.
|
|
1874
|
+
return conjoinScopeFilters$1(effectiveScope(requireRequestScopes()).filter, filter) ?? {};
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Body of the ARBAC DB controllers' `transformRelationFilter` (since 0.1.74):
|
|
1878
|
+
* the operand of a CLIENT relational predicate at `path` (`ticket`,
|
|
1879
|
+
* `ticket.team`, `tickets.issues` inside `$with=tickets(…)`) CONJOINED
|
|
1880
|
+
* with the row filter of the related rows the caller may see there — the
|
|
1881
|
+
* same policy `$with` of that relation applies: a declared `with.<rel>`
|
|
1882
|
+
* sub-scope, else the caller's own read grant on the related table. So
|
|
1883
|
+
* `$some` matches, and `$none` excludes, only on rows the caller could
|
|
1884
|
+
* load. A relation the request did not resolve answers like an unknown
|
|
1885
|
+
* field (400 — the request gate rejects it first).
|
|
1737
1886
|
*
|
|
1738
|
-
*
|
|
1739
|
-
* Throws `HttpError(403)` if a per-relation control policy is violated. A
|
|
1740
|
-
* hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
|
|
1741
|
-
* level through `hasField`, byte-for-byte like a nonexistent one.
|
|
1742
|
-
*
|
|
1743
|
-
* @since 0.1.72
|
|
1887
|
+
* @since 0.1.74
|
|
1744
1888
|
*/
|
|
1745
|
-
function
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
/** One `$with` level of {@link applyArbacRelationScopes}. */
|
|
1751
|
-
function applyRelationLevel(controls, vis) {
|
|
1752
|
-
const withArr = controls.$with;
|
|
1753
|
-
if (!Array.isArray(withArr) || withArr.length === 0) return;
|
|
1754
|
-
for (const raw of withArr) {
|
|
1755
|
-
if (!raw || typeof raw !== "object") continue;
|
|
1756
|
-
const entry = raw;
|
|
1757
|
-
if (typeof entry.name !== "string" || entry.name.includes(".")) continue;
|
|
1758
|
-
const level = (!vis.relationNames || vis.relationNames.has(entry.name)) && isMetaFieldVisible(entry.name, vis) ? vis.relation?.(entry.name) : void 0;
|
|
1759
|
-
if (!level) continue;
|
|
1760
|
-
const sub = effectiveScope(level.scopes ?? []);
|
|
1761
|
-
const conjoined = conjoinScopeFilters(sub.filter, entry.filter);
|
|
1762
|
-
if (conjoined) entry.filter = conjoined;
|
|
1763
|
-
const entryControls = entry.controls ?? {};
|
|
1764
|
-
const restricted = restrictSelect(entryControls.$select, level);
|
|
1765
|
-
if (restricted !== void 0) {
|
|
1766
|
-
entryControls.$select = restricted;
|
|
1767
|
-
entry.controls = entryControls;
|
|
1768
|
-
}
|
|
1769
|
-
enforceControlsPolicy(sub.controls, entryControls);
|
|
1770
|
-
applyRelationLevel(entryControls, level);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
/**
|
|
1774
|
-
* `$select` may be an array of field names (uniquery inclusion list) or a
|
|
1775
|
-
* `TProjection` object. `restrictProjection` operates on the object form
|
|
1776
|
-
* (it would read `["a"]` as an exclusion keyed `"0"`), so normalize the array
|
|
1777
|
-
* to `{ field: 1 }` first. Returns undefined if the input is empty (caller
|
|
1778
|
-
* treats as "no projection set").
|
|
1779
|
-
*/
|
|
1780
|
-
function normalizeSelect(value) {
|
|
1781
|
-
if (value === void 0 || value === null) return void 0;
|
|
1782
|
-
if (Array.isArray(value)) {
|
|
1783
|
-
const out = {};
|
|
1784
|
-
for (const item of value) if (typeof item === "string") out[item] = 1;
|
|
1785
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
1786
|
-
}
|
|
1787
|
-
if (typeof value === "object") return value;
|
|
1889
|
+
function arbacRelationFilter(path, filter, readable) {
|
|
1890
|
+
let level = visibilityFor(requireRequestScopes(), readable);
|
|
1891
|
+
for (const name of path.split(".")) level = level && visibleRelation(name, level);
|
|
1892
|
+
if (!level?.scopes) throw badRequest(path, `Unknown field "${path}"`);
|
|
1893
|
+
return conjoinScopeFilters$1(effectiveScope(level.scopes).filter, filter) ?? filter;
|
|
1788
1894
|
}
|
|
1789
1895
|
//#endregion
|
|
1790
1896
|
//#region src/db/write-fields.ts
|
|
@@ -1914,14 +2020,20 @@ var UnsupportedCheckError = class extends Error {};
|
|
|
1914
2020
|
* BEFORE the write. Deliberately a small subset of the filter language
|
|
1915
2021
|
* (`$eq`/`$ne`/`$in`/`$nin`/ordering/`$exists` + `$and`/`$or`/`$not`) with the
|
|
1916
2022
|
* database's null model (`$eq: null` matches null or missing, `$exists`
|
|
1917
|
-
* means "holds a non-null value"); any other operator (e.g. `$regex`)
|
|
1918
|
-
* {
|
|
2023
|
+
* means "holds a non-null value"); any other operator (e.g. `$regex`) and
|
|
2024
|
+
* a relational predicate (`{ ticket: { $some: … } }` — it needs the related
|
|
2025
|
+
* rows) throw {@link UnsupportedCheckError} — callers fail closed.
|
|
1919
2026
|
*/
|
|
1920
2027
|
function compileScopeCheck(filter) {
|
|
1921
2028
|
return walkFilter(filter, checkVisitor) ?? (() => true);
|
|
1922
2029
|
}
|
|
1923
|
-
/**
|
|
2030
|
+
/**
|
|
2031
|
+
* The field paths a filter references (dotted, as written). A relational
|
|
2032
|
+
* predicate throws {@link UnsupportedCheckError} (its fields live on the
|
|
2033
|
+
* related table).
|
|
2034
|
+
*/
|
|
1924
2035
|
function checkFields(filter) {
|
|
2036
|
+
if (containsRelationFilter(filter)) throw new UnsupportedCheckError("relational predicate");
|
|
1925
2037
|
return new Set(computeInsights(filter).keys());
|
|
1926
2038
|
}
|
|
1927
2039
|
function same(a, b) {
|
|
@@ -1938,6 +2050,9 @@ const checkVisitor = {
|
|
|
1938
2050
|
and: (children) => (row) => children.every((c) => c(row)),
|
|
1939
2051
|
or: (children) => (row) => children.some((c) => c(row)),
|
|
1940
2052
|
not: (child) => (row) => !child(row),
|
|
2053
|
+
relation(field, op) {
|
|
2054
|
+
throw new UnsupportedCheckError(`${field}: ${op}`);
|
|
2055
|
+
},
|
|
1941
2056
|
comparison(field, op, value) {
|
|
1942
2057
|
const read = (row) => getPath(row, field);
|
|
1943
2058
|
const compare = (test) => (row) => {
|
|
@@ -2115,9 +2230,23 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
2115
2230
|
prepareRequest(ctx) {
|
|
2116
2231
|
return prepareArbacRequest(ctx, this.readable);
|
|
2117
2232
|
}
|
|
2233
|
+
/**
|
|
2234
|
+
* The request filter ∧ the union of the scopes' `filter`s. Computed
|
|
2235
|
+
* synchronously, but typed with moost-db's async-capable contract
|
|
2236
|
+
* (`FilterExpr | Promise<FilterExpr>`) so a subclass may override it
|
|
2237
|
+
* `async` — `await super.transformFilter(filter)` — and moost-db awaits it.
|
|
2238
|
+
*/
|
|
2118
2239
|
transformFilter(filter) {
|
|
2119
2240
|
return arbacRowFilter(filter);
|
|
2120
2241
|
}
|
|
2242
|
+
/** A client relational predicate's operand ∧ the related rows the caller may see ({@link arbacRelationFilter}). Since 0.1.74. */
|
|
2243
|
+
transformRelationFilter(path, filter) {
|
|
2244
|
+
return arbacRelationFilter(path, filter, this.readable);
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* `$select` restricted to the scopes' projection union. Synchronous, typed
|
|
2248
|
+
* async-capable like {@link transformFilter}.
|
|
2249
|
+
*/
|
|
2121
2250
|
transformProjection(projection) {
|
|
2122
2251
|
return applyArbacProjection(projection, requireRequestScopes(), this.readable);
|
|
2123
2252
|
}
|
|
@@ -2180,10 +2309,22 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
|
|
|
2180
2309
|
}
|
|
2181
2310
|
/**
|
|
2182
2311
|
* The rows each `@DbAction` may run on: the caller's grant on that action
|
|
2183
|
-
* (
|
|
2184
|
-
* in `$actions` and
|
|
2312
|
+
* ({@link arbacActionRowScope} — several grants union, attenuation
|
|
2313
|
+
* intersects). Enforced by the action gate and reflected in `$actions` and
|
|
2314
|
+
* `GET /meta/actions/:id`. Since 0.1.72.
|
|
2315
|
+
*
|
|
2316
|
+
* The grant does not depend on `ctx` (the candidate rows, since 0.1.74).
|
|
2317
|
+
* To bound an action further by the candidates, override and AND your
|
|
2318
|
+
* filter onto `super`'s — it must never replace it:
|
|
2319
|
+
*
|
|
2320
|
+
* ```ts
|
|
2321
|
+
* protected async actionRowScope(name: string, ctx: TDbActionScopeContext) {
|
|
2322
|
+
* const grant = await super.actionRowScope(name, ctx);
|
|
2323
|
+
* return conjoinScopeFilters(grant, await myCandidateScope(name, ctx));
|
|
2324
|
+
* }
|
|
2325
|
+
* ```
|
|
2185
2326
|
*/
|
|
2186
|
-
actionRowScope(name) {
|
|
2327
|
+
actionRowScope(name, _ctx) {
|
|
2187
2328
|
return arbacActionRowScope(name);
|
|
2188
2329
|
}
|
|
2189
2330
|
/**
|
|
@@ -2366,6 +2507,10 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
2366
2507
|
transformFilter(filter) {
|
|
2367
2508
|
return arbacRowFilter(filter);
|
|
2368
2509
|
}
|
|
2510
|
+
/** Same contract as {@link AsArbacDbController.transformRelationFilter}. Since 0.1.74. */
|
|
2511
|
+
transformRelationFilter(path, filter) {
|
|
2512
|
+
return arbacRelationFilter(path, filter, this.readable);
|
|
2513
|
+
}
|
|
2369
2514
|
/** Same contract as {@link AsArbacDbController.transformProjection}. */
|
|
2370
2515
|
transformProjection(projection) {
|
|
2371
2516
|
return applyArbacProjection(projection, requireRequestScopes(), this.readable);
|
|
@@ -2387,7 +2532,7 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
2387
2532
|
return authorizeArbacForm(actionNames);
|
|
2388
2533
|
}
|
|
2389
2534
|
/** Same contract as {@link AsArbacDbController.actionRowScope}. */
|
|
2390
|
-
actionRowScope(name) {
|
|
2535
|
+
actionRowScope(name, _ctx) {
|
|
2391
2536
|
return arbacActionRowScope(name);
|
|
2392
2537
|
}
|
|
2393
2538
|
/** Same contract as {@link AsArbacDbController.allowedActions}. */
|
|
@@ -2467,6 +2612,7 @@ let AsArbacValueHelpController = class AsArbacValueHelpController extends AsValu
|
|
|
2467
2612
|
async prepareRequest(_ctx) {
|
|
2468
2613
|
await resolveRequestScopes();
|
|
2469
2614
|
}
|
|
2615
|
+
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
2470
2616
|
transformFilter(filter) {
|
|
2471
2617
|
return arbacRowFilter(filter);
|
|
2472
2618
|
}
|
|
@@ -2501,25 +2647,25 @@ __decorate([
|
|
|
2501
2647
|
ArbacAction("query"),
|
|
2502
2648
|
__decorateMetadata("design:type", Function),
|
|
2503
2649
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2504
|
-
__decorateMetadata("design:returntype",
|
|
2650
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2505
2651
|
], AsArbacValueHelpController.prototype, "runQuery", null);
|
|
2506
2652
|
__decorate([
|
|
2507
2653
|
ArbacAction("pages"),
|
|
2508
2654
|
__decorateMetadata("design:type", Function),
|
|
2509
2655
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2510
|
-
__decorateMetadata("design:returntype",
|
|
2656
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2511
2657
|
], AsArbacValueHelpController.prototype, "runPages", null);
|
|
2512
2658
|
__decorate([
|
|
2513
2659
|
ArbacAction("getOne"),
|
|
2514
2660
|
__decorateMetadata("design:type", Function),
|
|
2515
2661
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2516
|
-
__decorateMetadata("design:returntype",
|
|
2662
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2517
2663
|
], AsArbacValueHelpController.prototype, "runGetOne", null);
|
|
2518
2664
|
__decorate([
|
|
2519
2665
|
ArbacAction("getOneComposite"),
|
|
2520
2666
|
__decorateMetadata("design:type", Function),
|
|
2521
2667
|
__decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
|
|
2522
|
-
__decorateMetadata("design:returntype",
|
|
2668
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2523
2669
|
], AsArbacValueHelpController.prototype, "runGetOneComposite", null);
|
|
2524
2670
|
AsArbacValueHelpController = __decorate([Inherit()], AsArbacValueHelpController);
|
|
2525
2671
|
let AsArbacJsonValueHelpController = class AsArbacJsonValueHelpController extends AsJsonValueHelpController {
|
|
@@ -2530,6 +2676,7 @@ let AsArbacJsonValueHelpController = class AsArbacJsonValueHelpController extend
|
|
|
2530
2676
|
async prepareRequest(_ctx) {
|
|
2531
2677
|
await resolveRequestScopes();
|
|
2532
2678
|
}
|
|
2679
|
+
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
2533
2680
|
transformFilter(filter) {
|
|
2534
2681
|
return arbacRowFilter(filter);
|
|
2535
2682
|
}
|
|
@@ -2564,25 +2711,25 @@ __decorate([
|
|
|
2564
2711
|
ArbacAction("query"),
|
|
2565
2712
|
__decorateMetadata("design:type", Function),
|
|
2566
2713
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2567
|
-
__decorateMetadata("design:returntype",
|
|
2714
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2568
2715
|
], AsArbacJsonValueHelpController.prototype, "runQuery", null);
|
|
2569
2716
|
__decorate([
|
|
2570
2717
|
ArbacAction("pages"),
|
|
2571
2718
|
__decorateMetadata("design:type", Function),
|
|
2572
2719
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2573
|
-
__decorateMetadata("design:returntype",
|
|
2720
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2574
2721
|
], AsArbacJsonValueHelpController.prototype, "runPages", null);
|
|
2575
2722
|
__decorate([
|
|
2576
2723
|
ArbacAction("getOne"),
|
|
2577
2724
|
__decorateMetadata("design:type", Function),
|
|
2578
2725
|
__decorateMetadata("design:paramtypes", [String]),
|
|
2579
|
-
__decorateMetadata("design:returntype",
|
|
2726
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2580
2727
|
], AsArbacJsonValueHelpController.prototype, "runGetOne", null);
|
|
2581
2728
|
__decorate([
|
|
2582
2729
|
ArbacAction("getOneComposite"),
|
|
2583
2730
|
__decorateMetadata("design:type", Function),
|
|
2584
2731
|
__decorateMetadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
|
|
2585
|
-
__decorateMetadata("design:returntype",
|
|
2732
|
+
__decorateMetadata("design:returntype", typeof ReturnType === "undefined" ? Object : ReturnType)
|
|
2586
2733
|
], AsArbacJsonValueHelpController.prototype, "runGetOneComposite", null);
|
|
2587
2734
|
AsArbacJsonValueHelpController = __decorate([Inherit()], AsArbacJsonValueHelpController);
|
|
2588
2735
|
//#endregion
|
|
@@ -2609,7 +2756,7 @@ async function useArbacDbScope() {
|
|
|
2609
2756
|
const visibleOn = (table) => (path) => isScopedFieldVisible(scopes, path, table);
|
|
2610
2757
|
return {
|
|
2611
2758
|
scopes,
|
|
2612
|
-
filter: (extra) => conjoinScopeFilters(eff.filter, extra) ?? {},
|
|
2759
|
+
filter: (extra) => conjoinScopeFilters$1(eff.filter, extra) ?? {},
|
|
2613
2760
|
set: () => ({ ...eff.set }),
|
|
2614
2761
|
check: () => eff.check ?? {},
|
|
2615
2762
|
async assertRowsInScope(table, ids) {
|
|
@@ -2649,4 +2796,4 @@ async function useArbacDbScope() {
|
|
|
2649
2796
|
};
|
|
2650
2797
|
}
|
|
2651
2798
|
//#endregion
|
|
2652
|
-
export { ARBAC_DELEGATED_AUTH, Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, MoostArbac, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
|
|
2799
|
+
export { ARBAC_DELEGATED_AUTH, Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, AsArbacJsonValueHelpController, AsArbacValueHelpController, MoostArbac, applyAllowedFieldsAndSet, applyArbacControls, applyArbacMetaOverlay, applyArbacProjection, applyArbacRelationScopes, arbacActionRowScope, arbacAllowedActions, arbacAuthorizeInterceptor, arbacPatternToRegex, arbacRelationFilter, arbacRowFilter, authorizeArbacForm, buildScopeVisibility, cachedRequestScopes, checkArbacWrite, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, conjoinScopeFilters, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, guardArbacRemove, guardArbacWrite, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, prepareArbacRequest, pruneMetaByVisibility, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Mate, TClassConstructor, TMateParamMeta, TMoostMetadata } from "moost";
|
|
2
1
|
import { ControlGate, TProjection, TProjectionChildren, TScopeFieldRules, TScopeFilter } from "@aooth/arbac";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Mate, TClassConstructor, TMateParamMeta, TMoostMetadata } from "moost";
|
|
3
|
+
import { FilterExpr, TDbRemoveGuardContext, TDbWriteAction, TDbWriteCheckContext, TDbWriteGuardContext, TMetaResponse, UniqueryControls } from "@atscript/db";
|
|
4
|
+
import { AsDbController, TDbActionScopeContext, TDbControlsType, TDbRequestContext } from "@atscript/moost-db";
|
|
5
5
|
import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
|
|
6
6
|
//#region src/arbac.mate.d.ts
|
|
7
7
|
/**
|
|
@@ -124,6 +124,7 @@ type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate> : {
|
|
|
124
124
|
declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[], isMergePatch?: (objectPath: string) => boolean): unknown;
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region src/db/as-arbac-db-controller.d.ts
|
|
127
|
+
type TSelect = UniqueryControls["$select"];
|
|
127
128
|
/**
|
|
128
129
|
* Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
|
|
129
130
|
*
|
|
@@ -269,8 +270,20 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
269
270
|
* is resolved for the requested relations.
|
|
270
271
|
*/
|
|
271
272
|
protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
/**
|
|
274
|
+
* The request filter ∧ the union of the scopes' `filter`s. Computed
|
|
275
|
+
* synchronously, but typed with moost-db's async-capable contract
|
|
276
|
+
* (`FilterExpr | Promise<FilterExpr>`) so a subclass may override it
|
|
277
|
+
* `async` — `await super.transformFilter(filter)` — and moost-db awaits it.
|
|
278
|
+
*/
|
|
279
|
+
protected transformFilter(filter: Record<string, unknown> | undefined): FilterExpr | Promise<FilterExpr>;
|
|
280
|
+
/** A client relational predicate's operand ∧ the related rows the caller may see ({@link arbacRelationFilter}). Since 0.1.74. */
|
|
281
|
+
protected transformRelationFilter(path: string, filter: FilterExpr): FilterExpr | Promise<FilterExpr>;
|
|
282
|
+
/**
|
|
283
|
+
* `$select` restricted to the scopes' projection union. Synchronous, typed
|
|
284
|
+
* async-capable like {@link transformFilter}.
|
|
285
|
+
*/
|
|
286
|
+
protected transformProjection(projection?: TSelect): TSelect | undefined | Promise<TSelect | undefined>;
|
|
274
287
|
/**
|
|
275
288
|
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
276
289
|
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
@@ -308,10 +321,22 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
|
|
|
308
321
|
protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
|
|
309
322
|
/**
|
|
310
323
|
* The rows each `@DbAction` may run on: the caller's grant on that action
|
|
311
|
-
* (
|
|
312
|
-
* in `$actions` and
|
|
324
|
+
* ({@link arbacActionRowScope} — several grants union, attenuation
|
|
325
|
+
* intersects). Enforced by the action gate and reflected in `$actions` and
|
|
326
|
+
* `GET /meta/actions/:id`. Since 0.1.72.
|
|
327
|
+
*
|
|
328
|
+
* The grant does not depend on `ctx` (the candidate rows, since 0.1.74).
|
|
329
|
+
* To bound an action further by the candidates, override and AND your
|
|
330
|
+
* filter onto `super`'s — it must never replace it:
|
|
331
|
+
*
|
|
332
|
+
* ```ts
|
|
333
|
+
* protected async actionRowScope(name: string, ctx: TDbActionScopeContext) {
|
|
334
|
+
* const grant = await super.actionRowScope(name, ctx);
|
|
335
|
+
* return conjoinScopeFilters(grant, await myCandidateScope(name, ctx));
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
313
338
|
*/
|
|
314
|
-
protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
|
|
339
|
+
protected actionRowScope(name: string, _ctx?: TDbActionScopeContext): Promise<Record<string, unknown> | undefined>;
|
|
315
340
|
/**
|
|
316
341
|
* The row-level actions `$actions` and `GET /meta/actions/:id` may list:
|
|
317
342
|
* those the caller holds a grant on (the `/meta` overlay's rule, without
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -60,31 +60,31 @@
|
|
|
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.12",
|
|
64
|
+
"@aooth/arbac-core": "0.1.74",
|
|
65
|
+
"@aooth/user": "0.1.74",
|
|
66
|
+
"@aooth/arbac": "0.1.74"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@atscript/core": "^0.1.
|
|
70
|
-
"@atscript/db": "^0.1.
|
|
71
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
72
|
-
"@atscript/db-sqlite": "^0.1.
|
|
73
|
-
"@atscript/moost-db": "^0.1.
|
|
74
|
-
"@atscript/typescript": "^0.1.
|
|
75
|
-
"@moostjs/event-http": "^0.6.
|
|
69
|
+
"@atscript/core": "^0.1.99",
|
|
70
|
+
"@atscript/db": "^0.1.147",
|
|
71
|
+
"@atscript/db-sql-tools": "^0.1.147",
|
|
72
|
+
"@atscript/db-sqlite": "^0.1.147",
|
|
73
|
+
"@atscript/moost-db": "^0.1.147",
|
|
74
|
+
"@atscript/typescript": "^0.1.99",
|
|
75
|
+
"@moostjs/event-http": "^0.6.43",
|
|
76
76
|
"@types/better-sqlite3": "^7.6.13",
|
|
77
77
|
"better-sqlite3": "^12.6.2",
|
|
78
|
-
"unplugin-atscript": "^0.1.
|
|
78
|
+
"unplugin-atscript": "^0.1.99"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
|
-
"@atscript/db": "^0.1.
|
|
82
|
-
"@atscript/moost-db": "^0.1.
|
|
83
|
-
"@atscript/typescript": "^0.1.
|
|
84
|
-
"@moostjs/event-http": "^0.6.
|
|
85
|
-
"@wooksjs/event-core": "^0.7.
|
|
86
|
-
"@wooksjs/event-http": "^0.7.
|
|
87
|
-
"moost": "^0.6.
|
|
81
|
+
"@atscript/db": "^0.1.147",
|
|
82
|
+
"@atscript/moost-db": "^0.1.147",
|
|
83
|
+
"@atscript/typescript": "^0.1.99",
|
|
84
|
+
"@moostjs/event-http": "^0.6.43",
|
|
85
|
+
"@wooksjs/event-core": "^0.7.26",
|
|
86
|
+
"@wooksjs/event-http": "^0.7.26",
|
|
87
|
+
"moost": "^0.6.43"
|
|
88
88
|
},
|
|
89
89
|
"peerDependenciesMeta": {
|
|
90
90
|
"@atscript/db": {
|