@aooth/arbac-moost 0.2.3 → 0.2.4

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-BbYgv9RC.mjs";
1
+ import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-D0cJ_iMd.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
  /**
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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-BbYgv9RC.mjs";
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-D0cJ_iMd.mjs";
2
2
  import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
3
3
  import { ControlGate, ControlGate as ControlGate$1, TProjection, TScopeFieldRules, TScopeFilter, conjoinScopeFilters } from "@aooth/arbac";
4
- import { TAuthGuardDef } from "@moostjs/event-http";
4
+ import { HttpError, TAuthGuardDef } from "@moostjs/event-http";
5
5
  import { EventContext } from "@wooksjs/event-core";
6
6
  import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions, UniqueryControls } from "@atscript/db";
7
7
  import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbActionScopeContext, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
@@ -615,9 +615,41 @@ export declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: strin
615
615
  * caller's readable rows match the query; the action itself is authorized
616
616
  * by its source controller on every batch.
617
617
  *
618
+ * A read endpoint (`query`, `pages`, `geo`, `one`) under a moost-db write
619
+ * handler (`insert`, `update`, `replace`, `remove`, `removeComposite`) is
620
+ * served only inside {@link readBackUnderReadGrant} (since 0.2.4), under the
621
+ * read grant it installs — otherwise a 403, never the write grant. moost-db's
622
+ * insert read-back (`Prefer: return=representation`) then answers the plain
623
+ * insert result.
624
+ *
618
625
  * @since 0.1.72
619
626
  */
620
627
  export declare function prepareArbacRequest(ctx: TDbRequestContext, readable?: VisibilityTableSource): Promise<void>;
628
+ /**
629
+ * Read back the row `id` after a write, exactly as `GET /one/:id` would
630
+ * answer the caller (since 0.2.4) — the body of
631
+ * `AsArbacDbController._readRepresentation`, and the way for a write handler
632
+ * override to answer the written row:
633
+ *
634
+ * ```ts
635
+ * await super.update(payload);
636
+ * return readBackUnderReadGrant(id, () => this.getOne(String(id), ""));
637
+ * ```
638
+ *
639
+ * `read` calls the controller's `/one` handler: `getOne` for a scalar `id`,
640
+ * `getOneComposite` for a key object. The caller's grant on that handler is
641
+ * evaluated (its `@ArbacResource` / `@ArbacAction`, credential attenuation
642
+ * included) and installed as the request's scopes while `read` runs — its
643
+ * row filter, projection and controls apply, and an overridden handler body
644
+ * runs too — then the write's scopes are restored.
645
+ *
646
+ * Answers `undefined` instead of the row when: the caller has no grant on
647
+ * that handler; the handler carries method-level interceptors (they would
648
+ * not run on a direct call); `read` answers or throws an `HttpError` (e.g.
649
+ * the row is outside the read filter → 404); or anything else fails (logged
650
+ * as a warning, never answered — the write is already committed).
651
+ */
652
+ export declare function readBackUnderReadGrant<R>(id: unknown, read: () => Promise<R>): Promise<Exclude<R, HttpError> | undefined>;
621
653
  /**
622
654
  * `authorizeForm` for the ARBAC DB controllers: a form is served iff the
623
655
  * caller may run at least one of the actions that take it as input — each
package/dist/index.mjs CHANGED
@@ -21,6 +21,15 @@ function getOrCreate(map, key, create) {
21
21
  function hasSelfOrAncestor(set, path) {
22
22
  return set.has(path) || findAncestorInSet(path, set) !== void 0;
23
23
  }
24
+ /**
25
+ * A key object (a composite id, read by `getOneComposite`) — a plain object.
26
+ * A class instance such as a MongoDB `ObjectId` is a scalar id: `String(id)`.
27
+ */
28
+ function isKeyObject(id) {
29
+ if (id === null || typeof id !== "object") return false;
30
+ const proto = Object.getPrototypeOf(id);
31
+ return proto === Object.prototype || proto === null;
32
+ }
24
33
  /** A patch (`update` / `updateMany`) — only the keys it carries are written. */
25
34
  function isPatchAction(action) {
26
35
  return action === "update" || action === "updateMany";
@@ -2603,10 +2612,11 @@ function cachedRequestScopes() {
2603
2612
  */
2604
2613
  async function resolveRequestScopes() {
2605
2614
  const ctx = current();
2615
+ const prior = resolved();
2616
+ if (prior?.readBack) return prior.scopes;
2606
2617
  const arbac = useArbac(ctx);
2607
2618
  const cachedScopes = arbac.getScopes();
2608
2619
  const fromInterceptor = cachedScopes && scopesOwnedBy(cachedScopes, arbac.resource, arbac.action) ? cachedScopes : void 0;
2609
- const prior = resolved();
2610
2620
  if (prior && prior.resource === arbac.resource && prior.action === arbac.action && prior.source === fromInterceptor) return prior.scopes;
2611
2621
  const scopes = normalizeScopes(fromInterceptor !== void 0 ? {
2612
2622
  allowed: true,
@@ -2684,6 +2694,13 @@ function isScopedFieldVisible(scopes, path, source) {
2684
2694
  * caller's readable rows match the query; the action itself is authorized
2685
2695
  * by its source controller on every batch.
2686
2696
  *
2697
+ * A read endpoint (`query`, `pages`, `geo`, `one`) under a moost-db write
2698
+ * handler (`insert`, `update`, `replace`, `remove`, `removeComposite`) is
2699
+ * served only inside {@link readBackUnderReadGrant} (since 0.2.4), under the
2700
+ * read grant it installs — otherwise a 403, never the write grant. moost-db's
2701
+ * insert read-back (`Prefer: return=representation`) then answers the plain
2702
+ * insert result.
2703
+ *
2687
2704
  * @since 0.1.72
2688
2705
  */
2689
2706
  async function prepareArbacRequest(ctx, readable) {
@@ -2695,10 +2712,67 @@ async function prepareArbacRequest(ctx, readable) {
2695
2712
  await authorizeDelegatedQueryTarget();
2696
2713
  return;
2697
2714
  }
2715
+ if (READ_ENDPOINTS.has(ctx.endpoint) && !resolved()?.readBack && underWriteHandler()) {
2716
+ const { resource, action } = useArbac();
2717
+ throw insufficientPrivileges(resource, action);
2718
+ }
2698
2719
  const scopes = await resolveRequestScopes();
2699
2720
  if (ctx.endpoint === "insert" && ctx.onConflict === "ignore") resolved().insertIgnore = true;
2700
2721
  if (READ_ENDPOINTS.has(ctx.endpoint)) enforceRelationGates(await resolveRequestRelations(scopes, ctx.controls, readable, ctx.filter), visibilityFor(scopes, readable));
2701
2722
  }
2723
+ /** moost-db's write handler methods — a read under one of them must be a read-back. */
2724
+ const WRITE_HANDLERS = new Set([...WRITE_CRUD_OPS].flatMap((op) => DB_CRUD_HANDLERS[op]));
2725
+ function underWriteHandler() {
2726
+ const method = useControllerContext().getMethod();
2727
+ return method !== void 0 && WRITE_HANDLERS.has(method);
2728
+ }
2729
+ /**
2730
+ * Read back the row `id` after a write, exactly as `GET /one/:id` would
2731
+ * answer the caller (since 0.2.4) — the body of
2732
+ * `AsArbacDbController._readRepresentation`, and the way for a write handler
2733
+ * override to answer the written row:
2734
+ *
2735
+ * ```ts
2736
+ * await super.update(payload);
2737
+ * return readBackUnderReadGrant(id, () => this.getOne(String(id), ""));
2738
+ * ```
2739
+ *
2740
+ * `read` calls the controller's `/one` handler: `getOne` for a scalar `id`,
2741
+ * `getOneComposite` for a key object. The caller's grant on that handler is
2742
+ * evaluated (its `@ArbacResource` / `@ArbacAction`, credential attenuation
2743
+ * included) and installed as the request's scopes while `read` runs — its
2744
+ * row filter, projection and controls apply, and an overridden handler body
2745
+ * runs too — then the write's scopes are restored.
2746
+ *
2747
+ * Answers `undefined` instead of the row when: the caller has no grant on
2748
+ * that handler; the handler carries method-level interceptors (they would
2749
+ * not run on a direct call); `read` answers or throws an `HttpError` (e.g.
2750
+ * the row is outside the read filter → 404); or anything else fails (logged
2751
+ * as a warning, never answered — the write is already committed).
2752
+ */
2753
+ async function readBackUnderReadGrant(id, read) {
2754
+ const ctx = current();
2755
+ const prior = resolved();
2756
+ if (!prior) return void 0;
2757
+ try {
2758
+ const instance = useControllerContext(ctx).getController();
2759
+ const method = isKeyObject(id) ? "getOneComposite" : "getOne";
2760
+ if (getMoostMate().read(instance, method)?.interceptors?.length) return void 0;
2761
+ const ids = resolveHandlerArbacIds(instance, method);
2762
+ const [outcome] = await evaluateHandlerOutcomes([ids]);
2763
+ const scopes = normalizeScopes(outcome);
2764
+ if (!scopes) return void 0;
2765
+ storeResolved(ctx, ids, void 0, scopes);
2766
+ ctx.get(requestScopesKey).readBack = true;
2767
+ const row = await read();
2768
+ return row instanceof HttpError ? void 0 : row;
2769
+ } catch (error) {
2770
+ if (!(error instanceof HttpError)) useLogger("arbac", ctx).warn(`Read-back of the written row failed: ${String(error)}`);
2771
+ return;
2772
+ } finally {
2773
+ ctx.set(requestScopesKey, prior);
2774
+ }
2775
+ }
2702
2776
  /**
2703
2777
  * `authorizeForm` for the ARBAC DB controllers: a form is served iff the
2704
2778
  * caller may run at least one of the actions that take it as input — each
@@ -3205,6 +3279,15 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
3205
3279
  return prepareArbacRequest(ctx, this.readable);
3206
3280
  }
3207
3281
  /**
3282
+ * The read-back of `Prefer: return=representation` on `POST /` (since
3283
+ * 0.2.4; opt in with `returnRepresentation()`): this controller's own
3284
+ * `/one` handler under the caller's grant on it — see
3285
+ * {@link readBackUnderReadGrant}.
3286
+ */
3287
+ _readRepresentation(id) {
3288
+ return readBackUnderReadGrant(id, () => isKeyObject(id) ? this.getOneComposite(textKey(id), "") : this.getOne(String(id), ""));
3289
+ }
3290
+ /**
3208
3291
  * The request filter ∧ the union of the scopes' `filter`s. Computed
3209
3292
  * synchronously, but typed with moost-db's async-capable contract
3210
3293
  * (`FilterExpr | Promise<FilterExpr>`) so a subclass may override it
@@ -3392,6 +3475,10 @@ let AsArbacDbController = class AsArbacDbController extends AsDbController {
3392
3475
  }
3393
3476
  };
3394
3477
  AsArbacDbController = __decorate([Inherit()], AsArbacDbController);
3478
+ /** A key object as `GET /one?…` receives it — every value as text. */
3479
+ function textKey(id) {
3480
+ return Object.fromEntries(Object.entries(id).map(([k, v]) => [k, String(v)]));
3481
+ }
3395
3482
  const preservedFieldsCache = /* @__PURE__ */ new WeakMap();
3396
3483
  /**
3397
3484
  * Test-friendly internal helper — exported for unit tests and helper
@@ -3813,4 +3900,4 @@ async function useArbacDbScope() {
3813
3900
  })
3814
3901
  };
3815
3902
  }
3816
- 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 };
3903
+ 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, readBackUnderReadGrant, registerArbacDbTarget, requestFieldVisible, requireRequestScopes, resolveHandlerArbacIds, resolveRequestScopes, unionScopeProjection, useArbac, useArbacDbScope };
@@ -268,6 +268,13 @@ declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAn
268
268
  * is resolved for the requested relations.
269
269
  */
270
270
  protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
271
+ /**
272
+ * The read-back of `Prefer: return=representation` on `POST /` (since
273
+ * 0.2.4; opt in with `returnRepresentation()`): this controller's own
274
+ * `/one` handler under the caller's grant on it — see
275
+ * {@link readBackUnderReadGrant}.
276
+ */
277
+ protected _readRepresentation(id: unknown): Promise<TAtscriptDataType<T> | undefined>;
271
278
  /**
272
279
  * The request filter ∧ the union of the scopes' `filter`s. Computed
273
280
  * synchronously, but typed with moost-db's async-capable contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aooth/arbac-moost",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
5
5
  "keywords": [
6
6
  "abac",
@@ -61,16 +61,16 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@uniqu/core": "^0.2.0",
64
- "@aooth/arbac": "0.2.3",
65
- "@aooth/user": "0.2.3",
66
- "@aooth/arbac-core": "0.2.3"
64
+ "@aooth/arbac": "0.2.4",
65
+ "@aooth/user": "0.2.4",
66
+ "@aooth/arbac-core": "0.2.4"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@atscript/core": "^0.2.4",
70
- "@atscript/db": "^0.1.168",
71
- "@atscript/db-sql-tools": "^0.1.168",
72
- "@atscript/db-sqlite": "^0.1.168",
73
- "@atscript/moost-db": "^0.1.168",
70
+ "@atscript/db": "^0.1.169",
71
+ "@atscript/db-sql-tools": "^0.1.169",
72
+ "@atscript/db-sqlite": "^0.1.169",
73
+ "@atscript/moost-db": "^0.1.169",
74
74
  "@atscript/typescript": "^0.2.4",
75
75
  "@moostjs/event-http": "^0.7.2",
76
76
  "@types/better-sqlite3": "^7.6.13",
@@ -78,8 +78,8 @@
78
78
  "unplugin-atscript": "^0.2.4"
79
79
  },
80
80
  "peerDependencies": {
81
- "@atscript/db": "^0.1.168",
82
- "@atscript/moost-db": "^0.1.168",
81
+ "@atscript/db": "^0.1.169",
82
+ "@atscript/moost-db": "^0.1.169",
83
83
  "@atscript/typescript": "^0.2.4",
84
84
  "@moostjs/event-http": "^0.7.2",
85
85
  "@wooksjs/event-core": "^0.8.0",