@aooth/arbac-moost 0.1.70 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atscript/index.d.mts +1 -1
- package/dist/atscript/index.mjs +1 -6
- package/dist/{user.provider-CYiwr7y3.mjs → decorateMetadata-CU_mqYGT.mjs} +6 -1
- package/dist/index.d.mts +709 -210
- package/dist/index.mjs +2122 -576
- package/dist/{user.provider-C5BrFNxg.d.mts → user.provider-NMSzynGK.d.mts} +264 -51
- package/package.json +19 -19
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,201 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
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-NMSzynGK.mjs";
|
|
2
|
+
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacEvalResult as TArbacEvalResult$1, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
4
4
|
import { EventContext } from "@wooksjs/event-core";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
5
|
+
import { ControlGate, TProjection, TScopeFieldRules, TScopeFilter } from "@aooth/arbac";
|
|
6
|
+
import { FilterExpr, TDbRemoveGuardContext, TDbWriteCheckContext, TDbWriteGuardContext, TDeleteOptions, TMetaResponse, TRowResolveOptions, TWriteOptions } from "@atscript/db";
|
|
7
|
+
import { AsDbReadableController, AsJsonValueHelpController, AsValueHelpController, TDbControlsType, TDbIndexFieldPaths, TDbRequestContext, ValueHelpSelect } from "@atscript/moost-db";
|
|
8
|
+
import { TAtscriptAnnotatedType, TAtscriptDataType } from "@atscript/typescript/utils";
|
|
9
|
+
//#region src/db/visibility.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* The slice of an atscript-db readable (table / view) the visibility walk
|
|
12
|
+
* reads — a moost-db controller's `this.readable` satisfies it: identifiers
|
|
13
|
+
* (always visible), nav relations (to reach the joined table), the
|
|
14
|
+
* flattened schema (to split `$select` parents by scope) and the field
|
|
15
|
+
* descriptors (atomic JSON columns, derived-column sources).
|
|
16
|
+
*/
|
|
17
|
+
interface VisibilityTableSource {
|
|
18
|
+
primaryKeys: readonly string[];
|
|
19
|
+
preferredId: readonly string[];
|
|
20
|
+
relations?: ReadonlyMap<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* The target table of nav relation `navField` (atscript-db's
|
|
23
|
+
* `readable.relatedTable`); absent / `undefined` → the joined table's
|
|
24
|
+
* identifiers are not exempted.
|
|
25
|
+
*/
|
|
26
|
+
relatedTable?(navField: string): VisibilityTableSource | undefined;
|
|
27
|
+
/** Flattened schema (dot-paths) — used to split `$select` parents by scope. */
|
|
28
|
+
flatMap?: ReadonlyMap<string, unknown>;
|
|
29
|
+
/** Navigation field paths — their joined descendants are not own columns. */
|
|
30
|
+
navFields?: ReadonlySet<string>;
|
|
31
|
+
/**
|
|
32
|
+
* The readable's annotated type — the `$with` target registry key and the
|
|
33
|
+
* owner a self-`ref` points at.
|
|
34
|
+
*
|
|
35
|
+
* @since 0.1.72
|
|
36
|
+
*/
|
|
37
|
+
type?: object;
|
|
38
|
+
/**
|
|
39
|
+
* Field descriptors: `derived` marks a `@db.column.derived` field
|
|
40
|
+
* (visible only while its source is).
|
|
41
|
+
*
|
|
42
|
+
* @since 0.1.72
|
|
43
|
+
*/
|
|
44
|
+
fieldDescriptors?: ReadonlyArray<{
|
|
45
|
+
path: string;
|
|
46
|
+
derived?: {
|
|
47
|
+
sourcePath: string;
|
|
48
|
+
};
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Paths stored as ONE JSON column (atscript-db's `readable.jsonParents`,
|
|
52
|
+
* relational adapters): visible whole or not at all.
|
|
53
|
+
*
|
|
54
|
+
* @since 0.1.72
|
|
55
|
+
*/
|
|
56
|
+
jsonParents?: ReadonlySet<string>;
|
|
57
|
+
/** Whether the adapter searches natively (`/meta` search-surface pruning). @since 0.1.72 */
|
|
58
|
+
isSearchable?(): boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Relation name → the visibility its joined rows obey, or `null` when hidden. */
|
|
61
|
+
type ArbacRelationResolution = ReadonlyMap<string, MetaVisibility | null>;
|
|
62
|
+
/**
|
|
63
|
+
* The principal's field-visibility verdict set, assembled once per request
|
|
64
|
+
* (or per `/meta` overlay) and threaded through the pruning walk.
|
|
65
|
+
*/
|
|
66
|
+
interface MetaVisibility {
|
|
67
|
+
/**
|
|
68
|
+
* Projection union for this level's own fields. `{}` (the universe) means
|
|
69
|
+
* unrestricted — possible when only a `with` sub-scope restricts anything.
|
|
70
|
+
*/
|
|
71
|
+
allowed: TProjection;
|
|
72
|
+
/**
|
|
73
|
+
* Identifier fields reads ALWAYS return regardless of projection (the read
|
|
74
|
+
* path widens `preferredId` back in, and PK addressing requires the key) —
|
|
75
|
+
* hiding them would advertise less than reads deliver and break `/one/:id`.
|
|
76
|
+
*/
|
|
77
|
+
alwaysVisible: ReadonlySet<string>;
|
|
78
|
+
/** Relation names declared via `with.<name>` (see {@link collectWithGrantNames}). */
|
|
79
|
+
withGrants: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Visibility of a relation's JOINED fields: for a declared relation, the
|
|
82
|
+
* union of its `with.<name>` sub-scopes; for an undeclared one, the
|
|
83
|
+
* caller's own read scopes on the related table (resolved per request —
|
|
84
|
+
* `undefined` when the relation is hidden). The related table's own
|
|
85
|
+
* identifiers are always visible. A path `rel.x.y` is checked as `x.y`
|
|
86
|
+
* against it, recursively. A hand-built visibility without it lets
|
|
87
|
+
* declared paths through unchecked (legacy behavior).
|
|
88
|
+
*/
|
|
89
|
+
relation?: (name: string) => MetaVisibility | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Relations of this level's table. An undeclared one is visible only when
|
|
92
|
+
* the projection union allows its name AND {@link relation} resolves it
|
|
93
|
+
* (the `$with` inherit-target policy); absent → relation names are
|
|
94
|
+
* ordinary projection paths (legacy).
|
|
95
|
+
*/
|
|
96
|
+
relationNames?: ReadonlySet<string>;
|
|
97
|
+
/**
|
|
98
|
+
* Precompiled `isFieldAllowed(path, allowed)` — plus the derived-column
|
|
99
|
+
* rule (a `@db.column.derived` field is visible only while its source is).
|
|
100
|
+
* Supplied by {@link buildScopeVisibility}; a hand-built visibility falls
|
|
101
|
+
* back to `isFieldAllowed`.
|
|
102
|
+
*/
|
|
103
|
+
isAllowed?: (path: string) => boolean;
|
|
104
|
+
/** The scopes this level was built from (set by {@link buildScopeVisibility}). */
|
|
105
|
+
scopes?: ArbacDbScope[];
|
|
106
|
+
/** The table this level describes, when known (set by {@link buildScopeVisibility}). */
|
|
107
|
+
table?: VisibilityTableSource;
|
|
108
|
+
/**
|
|
109
|
+
* Fields the principal's WRITE scopes allow (union of `allowedFields`, or
|
|
110
|
+
* `"all"` for an unrestricted write grant). A field that is writable but not
|
|
111
|
+
* read-visible is NOT pruned from `/meta` — it survives with a `db.writeOnly`
|
|
112
|
+
* stamp (type only; rows/projections never carry it), so generic forms and
|
|
113
|
+
* client preflight validators can still SET sealed fields (e.g. credentials
|
|
114
|
+
* behind a read projection). Absent → nothing extra survives.
|
|
115
|
+
*/
|
|
116
|
+
writable?: ReadonlySet<string> | "all";
|
|
117
|
+
/**
|
|
118
|
+
* Nav relations the WRITE scopes opt into nested writes (union of
|
|
119
|
+
* `nestedWrites`). A relation path is writable only when listed here (and
|
|
120
|
+
* covered by {@link writable}) — mirroring the 403 the write endpoints
|
|
121
|
+
* answer for any other nav key. Absent → no relation is writable.
|
|
122
|
+
*
|
|
123
|
+
* @since 0.1.72
|
|
124
|
+
*/
|
|
125
|
+
nestedWrites?: ReadonlySet<string>;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Union the per-scope `projection` whitelists into the single access-control
|
|
129
|
+
* projection used for FIELD-EXISTENCE decisions (`hasField` parity + `/meta`
|
|
130
|
+
* pruning). Returns `undefined` when the union imposes no restriction — no
|
|
131
|
+
* scopes at all, or any scope without a `projection` (a universal grant makes
|
|
132
|
+
* `unionProjections` collapse to the universe `{}`).
|
|
133
|
+
*/
|
|
134
|
+
declare function unionScopeProjection(scopes: ArbacDbScope[]): TProjection | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Relation names any scope explicitly declares via `with.<name>` — those
|
|
137
|
+
* relations are governed by the declared sub-scopes alone (parent authority),
|
|
138
|
+
* and the declaration implies the relation EXISTS for this principal even
|
|
139
|
+
* when the projection union does not name it (see {@link ArbacDbScope.with}).
|
|
140
|
+
*/
|
|
141
|
+
declare function collectWithGrantNames(scopes: ArbacDbScope[]): ReadonlySet<string>;
|
|
142
|
+
/**
|
|
143
|
+
* Union of write-scope `allowedFields`; `"all"` for field-unrestricted write
|
|
144
|
+
* access. The whitelist exists only when at least one scope carries an
|
|
145
|
+
* `allowedFields` array — scoped-but-unlisted writes are field-unrestricted.
|
|
146
|
+
*/
|
|
147
|
+
declare function collectWritableFields(scopes: ArbacDbScope[], unrestricted: boolean): ReadonlySet<string> | "all" | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* Whether a flattened dot-path field exists for this principal.
|
|
150
|
+
*
|
|
151
|
+
* - A relation declared via `with.<name>` is visible; a path under it
|
|
152
|
+
* (`rel.x.y`) is checked as `x.y` against its sub-scope visibility.
|
|
153
|
+
* - An undeclared relation of the table (see {@link MetaVisibility.relationNames})
|
|
154
|
+
* is visible only when the projection allows its name and the caller's own
|
|
155
|
+
* grant on the related table resolved ({@link MetaVisibility.relation});
|
|
156
|
+
* paths under it are checked against that grant.
|
|
157
|
+
* - Anything else follows the projection union (and the derived-column rule).
|
|
158
|
+
*
|
|
159
|
+
* So a field a joined table hides is as unknown in a `$with` sub-query as a
|
|
160
|
+
* top-level hidden column is in the main query (no filter/sort value oracle).
|
|
161
|
+
*/
|
|
162
|
+
declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* PK + `preferredId` for a controller's table/readable — the
|
|
165
|
+
* {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
|
|
166
|
+
* controllers now pass `this.readable` and it is derived there.
|
|
167
|
+
*/
|
|
168
|
+
declare function metaAlwaysVisibleFields(_controller: object, source: {
|
|
169
|
+
primaryKeys: readonly string[];
|
|
170
|
+
preferredId: readonly string[];
|
|
171
|
+
}): ReadonlySet<string>;
|
|
172
|
+
/** Options of {@link buildScopeVisibility}. @since 0.1.72 */
|
|
173
|
+
interface ScopeVisibilityOptions {
|
|
174
|
+
/** Identifiers exempt from the projection; default: the table's PK + `preferredId`. */
|
|
175
|
+
alwaysVisible?: ReadonlySet<string>;
|
|
176
|
+
/**
|
|
177
|
+
* The undeclared relations resolved for this level (the caller's own grant
|
|
178
|
+
* on each related table — see `resolveRelationTree`); an undeclared
|
|
179
|
+
* relation missing here is hidden.
|
|
180
|
+
*/
|
|
181
|
+
relations?: ArbacRelationResolution;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Build the {@link MetaVisibility} a set of read scopes implies — the single
|
|
185
|
+
* structure behind `hasField`, `/meta` pruning, `$select` value stripping
|
|
186
|
+
* and the `$with` overlay, so they cannot drift. Own fields: the projection
|
|
187
|
+
* union (`{}` when unrestricted), normalized for atomic JSON columns and
|
|
188
|
+
* derived-column sources, precompiled into `isAllowed`. Relations: an
|
|
189
|
+
* undeclared one → its node in `relations` (none → hidden); a declared
|
|
190
|
+
* `with.<name>` → the union of its sub-scopes (lazily built when `relations`
|
|
191
|
+
* has no node for it). The related table's own PK / `preferredId` stay
|
|
192
|
+
* visible.
|
|
193
|
+
*
|
|
194
|
+
* @param opts - {@link ScopeVisibilityOptions}; a bare identifier set is the
|
|
195
|
+
* 0.1.71 `alwaysVisible` argument
|
|
196
|
+
*/
|
|
197
|
+
declare function buildScopeVisibility(scopes: ArbacDbScope[], table: VisibilityTableSource | undefined, opts?: ScopeVisibilityOptions | ReadonlySet<string>): MetaVisibility;
|
|
198
|
+
//#endregion
|
|
10
199
|
//#region src/arbac.composables.d.ts
|
|
11
200
|
/**
|
|
12
201
|
* The scopes cached for the current event (`undefined` before the authorize
|
|
@@ -17,13 +206,26 @@ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
|
17
206
|
* `TScope` is a caller-side type witness (see `evaluate` below).
|
|
18
207
|
*/
|
|
19
208
|
declare function getArbacScopes<TScope extends object>(ctx?: EventContext): TScope[] | undefined;
|
|
209
|
+
/** Options of `useArbac().evaluate()` / `evaluateOrThrow()`. */
|
|
210
|
+
interface ArbacEvaluateOptions {
|
|
211
|
+
/** Resource to evaluate; defaults to the current handler's. */
|
|
212
|
+
resource?: string;
|
|
213
|
+
/** Action to evaluate; defaults to the current handler's. */
|
|
214
|
+
action?: string;
|
|
215
|
+
/**
|
|
216
|
+
* The atscript-db readable whose schema a credential-attenuation
|
|
217
|
+
* conjunction uses to subtract nested exclusions exactly — pass the
|
|
218
|
+
* related table when evaluating a grant on it (e.g. a `$with` target).
|
|
219
|
+
* Defaults to the current controller's `readable`.
|
|
220
|
+
*
|
|
221
|
+
* @since 0.1.72
|
|
222
|
+
*/
|
|
223
|
+
table?: VisibilityTableSource;
|
|
224
|
+
}
|
|
20
225
|
interface ArbacBindings {
|
|
21
226
|
getScopes: <TScope extends object>() => TScope[] | undefined;
|
|
22
227
|
setScopes: <TScope extends object>(scope: TScope[] | undefined) => void;
|
|
23
|
-
evaluate: <TScope extends object>(opts?: {
|
|
24
|
-
resource?: string;
|
|
25
|
-
action?: string;
|
|
26
|
-
}) => Promise<{
|
|
228
|
+
evaluate: <TScope extends object>(opts?: ArbacEvaluateOptions) => Promise<{
|
|
27
229
|
allowed: boolean;
|
|
28
230
|
scopes?: TScope[];
|
|
29
231
|
userId: string;
|
|
@@ -37,10 +239,7 @@ interface ArbacBindings {
|
|
|
37
239
|
* with another policy, to filter UI metadata, or to fall through to a
|
|
38
240
|
* different authorization path).
|
|
39
241
|
*/
|
|
40
|
-
evaluateOrThrow: <TScope extends object>(opts?: {
|
|
41
|
-
resource?: string;
|
|
42
|
-
action?: string;
|
|
43
|
-
}) => Promise<{
|
|
242
|
+
evaluateOrThrow: <TScope extends object>(opts?: ArbacEvaluateOptions) => Promise<{
|
|
44
243
|
allowed: true;
|
|
45
244
|
scopes?: TScope[];
|
|
46
245
|
userId: string;
|
|
@@ -91,138 +290,164 @@ declare const ArbacResource: (name: string) => MethodDecorator & ClassDecorator
|
|
|
91
290
|
*/
|
|
92
291
|
declare const ArbacAction: (name: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
|
|
93
292
|
//#endregion
|
|
94
|
-
//#region src/arbac.mate.d.ts
|
|
95
|
-
/**
|
|
96
|
-
* ARBAC metadata fields attached to classes and methods by ARBAC decorators.
|
|
97
|
-
*
|
|
98
|
-
* Augments the shared `TMoostMetadata` workspace via TypeScript declaration
|
|
99
|
-
* merging so other moost-aware tooling (and `getArbacMate()` readers) sees
|
|
100
|
-
* these fields with full type safety.
|
|
101
|
-
*/
|
|
102
|
-
interface TArbacMeta {
|
|
103
|
-
arbacResourceId?: string;
|
|
104
|
-
arbacActionId?: string;
|
|
105
|
-
arbacPublic?: boolean;
|
|
106
|
-
}
|
|
107
|
-
declare module "moost" {
|
|
108
|
-
interface TMoostMetadata extends TArbacMeta {}
|
|
109
|
-
}
|
|
110
|
-
type ArbacMate = Mate<TMoostMetadata & {
|
|
111
|
-
params: TMateParamMeta[];
|
|
112
|
-
}, TMoostMetadata & {
|
|
113
|
-
params: TMateParamMeta[];
|
|
114
|
-
}>;
|
|
115
|
-
/**
|
|
116
|
-
* Returns the shared `Mate` instance typed with ARBAC metadata fields.
|
|
117
|
-
*
|
|
118
|
-
* All ARBAC decorators write through this typed wrapper so reads and writes
|
|
119
|
-
* stay type-checked against `TArbacMeta`.
|
|
120
|
-
*/
|
|
121
|
-
declare function getArbacMate(): ArbacMate;
|
|
122
|
-
//#endregion
|
|
123
293
|
//#region src/db/as-arbac-db-readable-controller.d.ts
|
|
124
294
|
/**
|
|
125
295
|
* Read-only mirror of {@link AsArbacDbController} for view-style controllers
|
|
126
296
|
* built on top of `@atscript/moost-db`'s {@link AsDbReadableController}.
|
|
127
|
-
* Applies the same filter / projection /
|
|
128
|
-
* hooks because the parent exposes none.
|
|
297
|
+
* Applies the same fail-closed `prepareRequest` and filter / projection /
|
|
298
|
+
* controls overlays — no write-side hooks because the parent exposes none.
|
|
129
299
|
*/
|
|
130
300
|
declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbReadableController<T> {
|
|
131
|
-
|
|
132
|
-
protected
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
301
|
+
/** Makes this controller the `$with` policy source for its table's ARBAC resource. */
|
|
302
|
+
protected readonly _arbacRelationTarget: true;
|
|
303
|
+
/** Same contract as {@link AsArbacDbController.prepareRequest}. */
|
|
304
|
+
protected prepareRequest(ctx: TDbRequestContext): Promise<void>;
|
|
305
|
+
/** Same contract as {@link AsArbacDbController.transformFilter}. */
|
|
306
|
+
protected transformFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
307
|
+
/** Same contract as {@link AsArbacDbController.transformProjection}. */
|
|
308
|
+
protected transformProjection(projection?: TProjection): TProjection | undefined;
|
|
309
|
+
/** Same contract as {@link AsArbacDbController.validateControls}. */
|
|
310
|
+
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
311
|
+
/** Same contract as {@link AsArbacDbController.applyMetaOverlay}. */
|
|
139
312
|
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
313
|
+
/** Same contract as {@link AsArbacDbController.authorizeForm}. */
|
|
314
|
+
protected authorizeForm(_name: string, actionNames: readonly string[]): Promise<boolean>;
|
|
315
|
+
/** Same contract as {@link AsArbacDbController.actionRowScope}. */
|
|
316
|
+
protected actionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
|
|
317
|
+
/** Same contract as {@link AsArbacDbController.allowedActions}. */
|
|
318
|
+
protected allowedActions(names: readonly string[]): Promise<string[]>;
|
|
319
|
+
/** Same contract as {@link AsArbacDbController.[ARBAC_DELEGATED_AUTH]}. */
|
|
320
|
+
[ARBAC_DELEGATED_AUTH](method: string): boolean;
|
|
321
|
+
/** Same contract as {@link AsArbacDbController.hasField}. */
|
|
144
322
|
protected hasField(path: string): boolean;
|
|
145
323
|
}
|
|
146
324
|
//#endregion
|
|
147
|
-
//#region src/db/
|
|
325
|
+
//#region src/db/as-arbac-value-help-controller.d.ts
|
|
148
326
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* `
|
|
327
|
+
* ARBAC-scoped mirror of `@atscript/moost-db`'s {@link AsValueHelpController}
|
|
328
|
+
* (bring your own backing source): the same scope contract as
|
|
329
|
+
* {@link AsArbacDbController} applied through the value-help seams.
|
|
330
|
+
*
|
|
331
|
+
* - `prepareRequest` — every route (`/query`, `/pages`, `/one`, `/meta`)
|
|
332
|
+
* resolves the scopes first: the authorize interceptor's cached ones, else
|
|
333
|
+
* a fresh evaluation. Denied → 403. `@Public()` / `arbacPublic` does NOT
|
|
334
|
+
* bypass it, and a missing interceptor fails closed rather than open.
|
|
335
|
+
* - `transformFilter` — the request filter ∧ the union of scope `filter`s
|
|
336
|
+
* (denied → match-nothing). `/one` outside the scope answers 404.
|
|
337
|
+
* - `transformProjection` — `$select` restricted to the scopes' projection
|
|
338
|
+
* union; the PK is always kept.
|
|
339
|
+
* - `hasField` — a scope-hidden field is `Unknown field "x"` (400) in
|
|
340
|
+
* filter / `$sort` / `$select`, and never matches `$search`.
|
|
341
|
+
* - `validateControls` — per-scope `controls` gates (e.g. `$search: false`).
|
|
342
|
+
* - `/meta` — `crud` filtered per action, `fields` / `type` pruned by the
|
|
343
|
+
* read scopes' projection union, `searchable` dropped when no searchable
|
|
344
|
+
* field survives.
|
|
345
|
+
*
|
|
346
|
+
* **Action names**: the data routes are re-tagged with the standard table
|
|
347
|
+
* READ action ids — `runQuery` → `query`, `runPages` → `pages`, `runGetOne`
|
|
348
|
+
* → `getOne`, `runGetOneComposite` → `getOneComposite` (`meta` / `metaForm`
|
|
349
|
+
* already match) — so `allowTableRead(resource, { scope })` grants a
|
|
350
|
+
* value-help source exactly like a table. The resource is the controller's
|
|
351
|
+
* `@ArbacResource(...)` (else the ARBAC fallback: controller id, then class
|
|
352
|
+
* name).
|
|
353
|
+
*
|
|
354
|
+
* Implementations of `query` must honor {@link hasField} for `$search` (skip
|
|
355
|
+
* hidden fields) — the ARBAC layer cannot see inside a custom search.
|
|
356
|
+
*
|
|
357
|
+
* @since 0.1.72
|
|
154
358
|
*/
|
|
155
|
-
declare
|
|
359
|
+
declare abstract class AsArbacValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsValueHelpController<T, DataType> {
|
|
360
|
+
private _arbacHost?;
|
|
361
|
+
private get arbacHost();
|
|
362
|
+
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
363
|
+
protected transformFilter(filter: FilterExpr): FilterExpr;
|
|
364
|
+
protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
|
|
365
|
+
protected hasField(path: string): boolean;
|
|
366
|
+
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
367
|
+
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
368
|
+
runQuery(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType[]>;
|
|
369
|
+
runPages(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | {
|
|
370
|
+
data: DataType[];
|
|
371
|
+
page: number;
|
|
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>>;
|
|
378
|
+
}
|
|
156
379
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
380
|
+
* ARBAC-scoped mirror of `@atscript/moost-db`'s
|
|
381
|
+
* {@link AsJsonValueHelpController} (a static in-memory row set) — the same
|
|
382
|
+
* contract, action names and resource naming as
|
|
383
|
+
* {@link AsArbacValueHelpController}; `$search` already skips scope-hidden
|
|
384
|
+
* fields.
|
|
385
|
+
*
|
|
386
|
+
* ```ts
|
|
387
|
+
* @Controller('dicts/status')
|
|
388
|
+
* @ArbacResource('dict-status')
|
|
389
|
+
* class StatusDictController extends AsArbacJsonValueHelpController<typeof StatusDict> {
|
|
390
|
+
* constructor(app: Moost) { super(StatusDict, STATUS_ROWS, app) }
|
|
391
|
+
* }
|
|
392
|
+
* // grant: allowTableRead('dict-status', { scope: () => ({ filter: { active: true } }) })
|
|
393
|
+
* ```
|
|
394
|
+
*
|
|
395
|
+
* @since 0.1.72
|
|
161
396
|
*/
|
|
162
|
-
declare
|
|
397
|
+
declare class AsArbacJsonValueHelpController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsJsonValueHelpController<T, DataType> {
|
|
398
|
+
private _arbacHost?;
|
|
399
|
+
private get arbacHost();
|
|
400
|
+
protected prepareRequest(_ctx: TDbRequestContext): Promise<void>;
|
|
401
|
+
protected transformFilter(filter: FilterExpr): FilterExpr;
|
|
402
|
+
protected transformProjection(select: ValueHelpSelect<DataType> | undefined): ValueHelpSelect<DataType> | undefined;
|
|
403
|
+
protected hasField(path: string): boolean;
|
|
404
|
+
protected validateControls(controls: Record<string, unknown>, type: TDbControlsType): string | undefined;
|
|
405
|
+
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
406
|
+
runQuery(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | DataType[]>;
|
|
407
|
+
runPages(url: string): Promise<import("@wooksjs/event-http").HttpError<import("@wooksjs/event-http").TWooksErrorBody> | {
|
|
408
|
+
data: DataType[];
|
|
409
|
+
page: number;
|
|
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>;
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/db/relation-policy.d.ts
|
|
163
419
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
420
|
+
* Register an ARBAC DB controller as an owner of its readable's ARBAC
|
|
421
|
+
* resource, so a `$with` from another controller can evaluate the caller's
|
|
422
|
+
* own grant on the joined table. Called once per controller instance (a
|
|
423
|
+
* field initializer of the ARBAC DB controllers). Scoped per moost app and
|
|
424
|
+
* keyed by the readable's annotated type; a newer instance of the same class
|
|
425
|
+
* replaces the older one.
|
|
426
|
+
*
|
|
427
|
+
* @returns `true` (so it can initialize a field)
|
|
428
|
+
* @since 0.1.72
|
|
166
429
|
*/
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Projection union for this level's own fields. `{}` (the universe) means
|
|
170
|
-
* unrestricted — possible when only a `with` sub-scope restricts anything.
|
|
171
|
-
*/
|
|
172
|
-
allowed: TProjection;
|
|
173
|
-
/**
|
|
174
|
-
* Identifier fields reads ALWAYS return regardless of projection (the read
|
|
175
|
-
* path widens `preferredId` back in, and PK addressing requires the key) —
|
|
176
|
-
* hiding them would advertise less than reads deliver and break `/one/:id`.
|
|
177
|
-
*/
|
|
178
|
-
alwaysVisible: ReadonlySet<string>;
|
|
179
|
-
/** Relation names granted via `with.<name>` (see {@link collectWithGrantNames}). */
|
|
180
|
-
withGrants: ReadonlySet<string>;
|
|
181
|
-
/**
|
|
182
|
-
* Visibility of a `with`-granted relation's JOINED fields, built from the
|
|
183
|
-
* union of its `with.<name>` sub-scopes (with the related table's own
|
|
184
|
-
* identifiers always visible). A path `rel.x.y` is checked as `x.y`
|
|
185
|
-
* against it, recursively for nested relations. The scope-driven builders
|
|
186
|
-
* ({@link buildScopeVisibility}) always supply it; a hand-built visibility
|
|
187
|
-
* without it lets granted paths through unchecked (legacy behavior).
|
|
188
|
-
*/
|
|
189
|
-
relation?: (name: string) => MetaVisibility | undefined;
|
|
190
|
-
/**
|
|
191
|
-
* Precompiled `isFieldAllowed(path, allowed)`. Supplied by
|
|
192
|
-
* {@link buildScopeVisibility}; a hand-built visibility falls back to
|
|
193
|
-
* `isFieldAllowed`.
|
|
194
|
-
*/
|
|
195
|
-
isAllowed?: (path: string) => boolean;
|
|
196
|
-
/** The scopes this level was built from (set by {@link buildScopeVisibility}). */
|
|
197
|
-
scopes?: ArbacDbScope[];
|
|
198
|
-
/** The table this level describes, when known (set by {@link buildScopeVisibility}). */
|
|
199
|
-
table?: VisibilityTableSource;
|
|
200
|
-
/**
|
|
201
|
-
* Fields the principal's WRITE scopes allow (union of `allowedFields`, or
|
|
202
|
-
* `"all"` for an unrestricted write grant). A field that is writable but not
|
|
203
|
-
* read-visible is NOT pruned from `/meta` — it survives with a `db.writeOnly`
|
|
204
|
-
* stamp (type only; rows/projections never carry it), so generic forms and
|
|
205
|
-
* client preflight validators can still SET sealed fields (e.g. credentials
|
|
206
|
-
* behind a read projection). Absent → nothing extra survives.
|
|
207
|
-
*/
|
|
208
|
-
writable?: ReadonlySet<string> | "all";
|
|
209
|
-
}
|
|
430
|
+
declare function registerArbacDbTarget(controller: object): true;
|
|
210
431
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
432
|
+
* The ARBAC resource + action a controller's handler is authorized as — the
|
|
433
|
+
* precedence `useArbac()` applies while that handler serves a request.
|
|
434
|
+
* Resource: method `@ArbacResource` → class `@ArbacResource` → controller id
|
|
435
|
+
* → class name. Action: method `@ArbacAction` → `@DbAction` name → class
|
|
436
|
+
* `@ArbacAction` → method `@Id` → method name.
|
|
437
|
+
*
|
|
438
|
+
* @since 0.1.72
|
|
215
439
|
*/
|
|
216
|
-
declare function
|
|
440
|
+
declare function resolveHandlerArbacIds(instance: object, methodName: string): {
|
|
441
|
+
resource: string;
|
|
442
|
+
action: string;
|
|
443
|
+
};
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/db/meta-projection.d.ts
|
|
217
446
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* a path UNDER it (`rel.x.y`) is checked as `x.y` against the relation's
|
|
221
|
-
* sub-scope visibility ({@link MetaVisibility.relation}) — so a field the
|
|
222
|
-
* sub-scope hides is as unknown in a `$with` sub-query as a top-level hidden
|
|
223
|
-
* column is in the main query (no filter/sort value oracle).
|
|
447
|
+
* Foreign `ref` targets for `/meta` pruning: annotated type → the caller's
|
|
448
|
+
* visibility on it, or `null` when the caller cannot read it.
|
|
224
449
|
*/
|
|
225
|
-
|
|
450
|
+
type MetaRefTargets = ReadonlyMap<object, MetaVisibility | null>;
|
|
226
451
|
/**
|
|
227
452
|
* Prune a `/meta` envelope down to the fields the principal's read scopes can
|
|
228
453
|
* ever surface, so a scoped UI cannot even OFFER an out-of-scope column (the
|
|
@@ -231,96 +456,50 @@ declare function isMetaFieldVisible(path: string, vis: MetaVisibility): boolean;
|
|
|
231
456
|
*
|
|
232
457
|
* - `fields` — the flat capability map (sortable/filterable flags).
|
|
233
458
|
* - `type` — the serialized annotated type (what dynamic clients build
|
|
234
|
-
* tables/forms from); nested object props prune by dot-path. A
|
|
235
|
-
* prop
|
|
236
|
-
*
|
|
237
|
-
*
|
|
459
|
+
* tables/forms from); nested object props prune by dot-path. A visible
|
|
460
|
+
* relation's nav prop is pruned by that relation's visibility (declared
|
|
461
|
+
* sub-scope or the caller's own grant on the target), recursively. A
|
|
462
|
+
* `ref` whose target field is hidden (own table) or whose target table the
|
|
463
|
+
* caller cannot read (`refTargets`) is stripped.
|
|
464
|
+
* - `relations` — hidden relations dropped.
|
|
238
465
|
* - `versionColumn` — dropped when the OCC column itself is hidden.
|
|
239
466
|
*
|
|
240
467
|
* NEVER mutates the input — the base controller caches the static envelope
|
|
241
468
|
* (`applyMetaOverlay` contract); every pruned branch is a fresh object.
|
|
242
469
|
*/
|
|
243
|
-
declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility): TMetaResponse;
|
|
470
|
+
declare function pruneMetaByVisibility(meta: TMetaResponse, vis: MetaVisibility, refTargets?: MetaRefTargets): TMetaResponse;
|
|
244
471
|
/**
|
|
245
|
-
* The
|
|
246
|
-
*
|
|
247
|
-
* (always visible), nav relations (to reach the joined table) and the
|
|
248
|
-
* flattened schema (to split `$select` parents by scope).
|
|
249
|
-
*/
|
|
250
|
-
interface VisibilityTableSource {
|
|
251
|
-
primaryKeys: readonly string[];
|
|
252
|
-
preferredId: readonly string[];
|
|
253
|
-
relations?: ReadonlyMap<string, unknown>;
|
|
254
|
-
/**
|
|
255
|
-
* The target table of nav relation `navField` (atscript-db's
|
|
256
|
-
* `readable.relatedTable`); absent / `undefined` → the joined table's
|
|
257
|
-
* identifiers are not exempted.
|
|
258
|
-
*/
|
|
259
|
-
relatedTable?(navField: string): VisibilityTableSource | undefined;
|
|
260
|
-
/** Flattened schema (dot-paths) — used to split `$select` parents by scope. */
|
|
261
|
-
flatMap?: ReadonlyMap<string, unknown>;
|
|
262
|
-
/** Navigation field paths — their joined descendants are not own columns. */
|
|
263
|
-
navFields?: ReadonlySet<string>;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Build the {@link MetaVisibility} a set of read scopes implies — the single
|
|
267
|
-
* structure behind `hasField`, `/meta` pruning, `$select` value stripping
|
|
268
|
-
* and the `$with` overlay, so they cannot drift. Own fields: the projection
|
|
269
|
-
* union (`{}` when unrestricted), precompiled into `isAllowed`. A
|
|
270
|
-
* `with`-granted relation: a lazily built (memoized) child visibility over
|
|
271
|
-
* the union of its `with.<name>` sub-scopes — the same sub-scopes joined rows
|
|
272
|
-
* are stripped with — whose `alwaysVisible` is the related table's own PK /
|
|
273
|
-
* `preferredId` (reached through `table.relatedTable`; without it, no
|
|
274
|
-
* related identifier is exempt).
|
|
472
|
+
* The full ARBAC `/meta` overlay, shared by the ARBAC DB and value-help
|
|
473
|
+
* controllers.
|
|
275
474
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* relation
|
|
285
|
-
*
|
|
475
|
+
* 1. `actions` and `crud` are filtered by evaluating each one exactly as its
|
|
476
|
+
* handler is authorized (`useArbac` precedence): a crud op through its
|
|
477
|
+
* handler method(s) — `one` via `getOne` / `getOneComposite`, `remove`
|
|
478
|
+
* via `remove` / `removeComposite` (allowed when any is) — and an action
|
|
479
|
+
* through its method (`@ArbacAction` → `@DbAction` name → …).
|
|
480
|
+
* 2. The FIELD surface (`fields`, serialized `type`, `relations`,
|
|
481
|
+
* `versionColumn`) is pruned by the read scopes' visibility: the
|
|
482
|
+
* projection union, derived-column sources, atomic JSON columns, and the
|
|
483
|
+
* `$with` relation policy (declared `with` sub-scopes, else the caller's
|
|
484
|
+
* own grant on the related table — no grant → relation and nav type
|
|
485
|
+
* dropped). `ref`s to hidden targets are stripped. An allowed read op
|
|
486
|
+
* without scopes is an unscoped grant (no field restriction; relations
|
|
487
|
+
* still follow the relation policy). No read op allowed → no pruning:
|
|
488
|
+
* `crud` already advertises no read surface, and write-only principals
|
|
489
|
+
* still need `type` for their insert/update forms. PK + `preferredId` are
|
|
490
|
+
* never pruned.
|
|
491
|
+
* 3. The search surface: `searchIndexes` reading a hidden field are dropped,
|
|
492
|
+
* `searchable` / `vectorSearchable` / `geoSearchable` recomputed, and the
|
|
493
|
+
* `query` / `pages` control lists lose the controls that became unusable
|
|
494
|
+
* (`crud.geo` goes when no geo index is left).
|
|
286
495
|
*
|
|
287
|
-
* @param source - the controller's `this.readable
|
|
288
|
-
*
|
|
496
|
+
* @param source - the controller's `this.readable` (identifiers, relations
|
|
497
|
+
* and schema rules are derived from it); an identifier set (the 0.1.67
|
|
498
|
+
* signature) still works, without them
|
|
499
|
+
* @param indexes - the controller's `indexFieldPaths()`; defaults to calling
|
|
500
|
+
* it on the current controller when it has one
|
|
289
501
|
*/
|
|
290
|
-
declare function
|
|
291
|
-
/**
|
|
292
|
-
* PK + `preferredId` for a controller's table/readable — the
|
|
293
|
-
* {@link MetaVisibility.alwaysVisible} set. Kept for compatibility: the
|
|
294
|
-
* controllers now pass `this.readable` and it is derived there.
|
|
295
|
-
*/
|
|
296
|
-
declare function metaAlwaysVisibleFields(_controller: object, source: {
|
|
297
|
-
primaryKeys: readonly string[];
|
|
298
|
-
preferredId: readonly string[];
|
|
299
|
-
}): ReadonlySet<string>;
|
|
300
|
-
/**
|
|
301
|
-
* The full ARBAC `/meta` overlay, shared by `AsArbacDbController` and
|
|
302
|
-
* `AsArbacDbReadableController`: filter `actions` + `crud` by per-action
|
|
303
|
-
* evaluation, then prune the FIELD surface (`fields`, serialized `type`,
|
|
304
|
-
* `relations`, `versionColumn`) by the read scopes' projection union.
|
|
305
|
-
*
|
|
306
|
-
* Field pruning (BUG-3): a scope projection must remove fields from the META
|
|
307
|
-
* entirely — `transformProjection` already strips their VALUES from every
|
|
308
|
-
* read, but the envelope still advertised the full field map, so a scoped UI
|
|
309
|
-
* offered columns that could never populate, and secret-bearing column NAMES
|
|
310
|
-
* leaked. The pruning union comes from the read-op evaluations already
|
|
311
|
-
* computed for the `crud` overlay (same `unionProjections` the read path
|
|
312
|
-
* applies). An allowed read op WITHOUT scopes is an unscoped grant —
|
|
313
|
-
* universe, no pruning (unchanged behavior for unscoped roles). No read op
|
|
314
|
-
* allowed → the projection union is empty → no pruning: `crud` already
|
|
315
|
-
* advertises no read surface, and write-only principals still need `type`
|
|
316
|
-
* for their insert/update forms. PK + `preferredId` are never pruned —
|
|
317
|
-
* reads always return those fields (projection widening / id addressing).
|
|
318
|
-
*
|
|
319
|
-
* @param source - the controller's `this.readable` (identifiers and related
|
|
320
|
-
* tables are derived from it); an identifier set (the 0.1.67 signature)
|
|
321
|
-
* still works, without related-table identifiers
|
|
322
|
-
*/
|
|
323
|
-
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>): Promise<TMetaResponse>;
|
|
502
|
+
declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTableSource | ReadonlySet<string>, indexes?: readonly TDbIndexFieldPaths[]): Promise<TMetaResponse>;
|
|
324
503
|
/**
|
|
325
504
|
* Test-friendly internal helper — exported for unit tests and helper
|
|
326
505
|
* composition; regular consumers should not call this directly.
|
|
@@ -335,6 +514,277 @@ declare function applyArbacMetaOverlay(meta: TMetaResponse, source: VisibilityTa
|
|
|
335
514
|
*/
|
|
336
515
|
declare function collectMethodNames(instance: object): string[];
|
|
337
516
|
//#endregion
|
|
517
|
+
//#region src/db/request-scopes.d.ts
|
|
518
|
+
/**
|
|
519
|
+
* The scopes resolved for the current event, or `undefined` before
|
|
520
|
+
* {@link resolveRequestScopes} ran.
|
|
521
|
+
*
|
|
522
|
+
* @since 0.1.72
|
|
523
|
+
*/
|
|
524
|
+
declare function cachedRequestScopes(): ArbacDbScope[] | undefined;
|
|
525
|
+
/**
|
|
526
|
+
* Resolve the current event's scopes once: the ones the authorize
|
|
527
|
+
* interceptor cached, else an ARBAC evaluation of the handler's
|
|
528
|
+
* resource/action. The empty-scope rule (`normalizeScopes` from
|
|
529
|
+
* `@aooth/arbac`): an allowed grant without scopes is unrestricted (`[{}]`),
|
|
530
|
+
* a scope predicate returning nothing contributes nothing, and an allowed
|
|
531
|
+
* evaluation left with NO scope is a deny. A deny throws 403.
|
|
532
|
+
*
|
|
533
|
+
* @since 0.1.72
|
|
534
|
+
*/
|
|
535
|
+
declare function resolveRequestScopes(): Promise<ArbacDbScope[]>;
|
|
536
|
+
/**
|
|
537
|
+
* The resolved scopes, synchronously — for the hooks that run after the
|
|
538
|
+
* entry point (`validateControls`, `transformFilter`, the write guards …).
|
|
539
|
+
* Unresolved scopes are a 403, never "unrestricted".
|
|
540
|
+
*
|
|
541
|
+
* @since 0.1.72
|
|
542
|
+
*/
|
|
543
|
+
declare function requireRequestScopes(): ArbacDbScope[];
|
|
544
|
+
/**
|
|
545
|
+
* Shared body of the ARBAC controllers' `hasField` overrides: a field
|
|
546
|
+
* outside the read-scope visibility — hidden by the projection union, a
|
|
547
|
+
* derived field whose source is hidden, a path inside a hidden atomic JSON
|
|
548
|
+
* column, a hidden relation, or a `rel.x` path the relation's policy hides —
|
|
549
|
+
* is indistinguishable from a field that does not exist. Unresolved scopes
|
|
550
|
+
* hide every field.
|
|
551
|
+
*
|
|
552
|
+
* @since 0.1.72
|
|
553
|
+
*/
|
|
554
|
+
declare function requestFieldVisible(path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
|
|
555
|
+
/**
|
|
556
|
+
* Whether `path` is visible to `scopes` over `source` (see
|
|
557
|
+
* {@link requestFieldVisible}, which reads the request's scopes).
|
|
558
|
+
*
|
|
559
|
+
* @param source - the controller's `this.readable`; an identifier set (the
|
|
560
|
+
* 0.1.67 signature) still works, without relation / schema rules
|
|
561
|
+
*/
|
|
562
|
+
declare function isScopedFieldVisible(scopes: ArbacDbScope[], path: string, source: VisibilityTableSource | ReadonlySet<string>): boolean;
|
|
563
|
+
/**
|
|
564
|
+
* Body of the ARBAC DB / value-help controllers' `prepareRequest` — every
|
|
565
|
+
* endpoint, `@DbAction` handlers included (`endpoint: "action"`, before any
|
|
566
|
+
* action id / row is read): resolve the request's scopes
|
|
567
|
+
* ({@link resolveRequestScopes} — 403 on denial, `arbacPublic` does not
|
|
568
|
+
* bypass) and, on read endpoints, the `$with` relation policy for the
|
|
569
|
+
* requested relations.
|
|
570
|
+
*
|
|
571
|
+
* @since 0.1.72
|
|
572
|
+
*/
|
|
573
|
+
declare function prepareArbacRequest(ctx: TDbRequestContext, readable?: VisibilityTableSource): Promise<void>;
|
|
574
|
+
/**
|
|
575
|
+
* `authorizeForm` for the ARBAC DB controllers: a form is served iff the
|
|
576
|
+
* caller may run at least one of the actions that take it as input — each
|
|
577
|
+
* evaluated as the resource/action its handler is authorized as.
|
|
578
|
+
*
|
|
579
|
+
* @since 0.1.72
|
|
580
|
+
*/
|
|
581
|
+
declare function authorizeArbacForm(actionNames: readonly string[]): Promise<boolean>;
|
|
582
|
+
/**
|
|
583
|
+
* Body of the ARBAC DB controllers' `allowedActions`: the action `names` the
|
|
584
|
+
* caller holds a grant on — each evaluated as {@link actionArbacIds} names
|
|
585
|
+
* it, exactly the `/meta` overlay's action rule (memoized per event, so the
|
|
586
|
+
* evaluations `prepareRequest` / the overlay already ran are reused).
|
|
587
|
+
*
|
|
588
|
+
* @since 0.1.72
|
|
589
|
+
*/
|
|
590
|
+
declare function arbacAllowedActions(names: readonly string[]): Promise<string[]>;
|
|
591
|
+
/**
|
|
592
|
+
* Body of the ARBAC DB controllers' `actionRowScope`: the rows the
|
|
593
|
+
* `@DbAction` `name` may run on — the filter of the caller's grant on the
|
|
594
|
+
* action ({@link actionArbacIds}: every handler method of the action, or a
|
|
595
|
+
* class-level entry's fallback ids), including custom-field row filters and
|
|
596
|
+
* credential attenuation. No grant → a match-nothing filter (fail closed);
|
|
597
|
+
* an unrestricted grant → `undefined`. When the current request IS that
|
|
598
|
+
* action (its resolved scopes are the same evaluation), the row overlay
|
|
599
|
+
* already applies it → `undefined`. Equal filters are returned as ONE
|
|
600
|
+
* object per request.
|
|
601
|
+
*
|
|
602
|
+
* @since 0.1.72
|
|
603
|
+
*/
|
|
604
|
+
declare function arbacActionRowScope(name: string): Promise<Record<string, unknown> | undefined>;
|
|
605
|
+
/**
|
|
606
|
+
* Body of the ARBAC controllers' `transformFilter` (and so of the row
|
|
607
|
+
* overlay `/one`, `DELETE` and `@DbAction` ids use): the user filter
|
|
608
|
+
* CONJOINED with the union of the request's scope filters.
|
|
609
|
+
*
|
|
610
|
+
* @since 0.1.72
|
|
611
|
+
*/
|
|
612
|
+
declare function arbacRowFilter(filter: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/db/shared-read-helpers.d.ts
|
|
615
|
+
/**
|
|
616
|
+
* Restrict the user-supplied `$select` to the scopes' projection union.
|
|
617
|
+
* Returns the original projection untouched when no scope declares a
|
|
618
|
+
* projection (so caller sees the unrestricted shape). Reads the same cached
|
|
619
|
+
* visibility (`vis.allowed`) `hasField` / `/meta` pruning use, so value
|
|
620
|
+
* stripping and name hiding cannot drift.
|
|
621
|
+
*
|
|
622
|
+
* `projection` is the `$select` as moost-db parsed it: an inclusion is an
|
|
623
|
+
* ARRAY of names, an exclusion an object — both are normalized first. A
|
|
624
|
+
* requested parent (`$select=a`) narrows to what the scope shows: to the
|
|
625
|
+
* whitelisted descendants (`a.b: 1`), or — for a hidden descendant
|
|
626
|
+
* (`a.c: 0`) — to its visible children via `childrenOf` (without a schema
|
|
627
|
+
* the parent is dropped). Never the whole object, and never the empty
|
|
628
|
+
* (universe) projection: when nothing survives, the scope's own projection
|
|
629
|
+
* applies (`restrictProjection`'s ceiling fallback).
|
|
630
|
+
*
|
|
631
|
+
* An exclusion result names nested-object parents by their leaves
|
|
632
|
+
* (`{ a: 0 }` → `{ "a.b": 0, "a.c": 0 }`): flattening adapters invert an
|
|
633
|
+
* exclusion against their leaf columns, so a parent key alone would strip
|
|
634
|
+
* nothing (a scope `{ a: 0 }` would otherwise return all of `a`).
|
|
635
|
+
*
|
|
636
|
+
* @since 0.1.72
|
|
637
|
+
*/
|
|
638
|
+
declare function applyArbacProjection(projection: unknown, scopes: ArbacDbScope[], readable?: VisibilityTableSource): TProjection | undefined;
|
|
639
|
+
/**
|
|
640
|
+
* Enforce the union of per-scope `controls` gates against the parsed
|
|
641
|
+
* Uniquery controls. Throws `HttpError(403)` on the first violation.
|
|
642
|
+
*
|
|
643
|
+
* @since 0.1.72
|
|
644
|
+
*/
|
|
645
|
+
declare function applyArbacControls(controls: Record<string, unknown>, scopes: ArbacDbScope[]): void;
|
|
646
|
+
/**
|
|
647
|
+
* Walk the user-supplied `$with` items in `controls` and overlay each joined
|
|
648
|
+
* relation with the policy of its rows (the `$with` inherit-target policy):
|
|
649
|
+
* a relation some scope declares `with.<name>` for gets the union of those
|
|
650
|
+
* sub-scopes (parent authority — silent roles contribute nothing); any other
|
|
651
|
+
* gets the caller's OWN read scopes on the related table, as resolved in
|
|
652
|
+
* `prepareRequest` (for the request's own scopes). Filter conjoined, `$select`
|
|
653
|
+
* restricted, per-relation `controls` gates enforced, recursively.
|
|
654
|
+
*
|
|
655
|
+
* Mutates `controls` in place (matches the `validateControls` contract).
|
|
656
|
+
* Throws `HttpError(403)` if a per-relation control policy is violated. A
|
|
657
|
+
* hidden relation is skipped: moost-db (≥ 0.1.143) rejects it at every
|
|
658
|
+
* level through `hasField`, byte-for-byte like a nonexistent one.
|
|
659
|
+
*
|
|
660
|
+
* @since 0.1.72
|
|
661
|
+
*/
|
|
662
|
+
declare function applyArbacRelationScopes(controls: Record<string, unknown>, scopes: ArbacDbScope[], readable?: VisibilityTableSource): void;
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/db/write-policy.d.ts
|
|
665
|
+
/** The table surface the write guards need (`AtscriptDbTable` satisfies it). */
|
|
666
|
+
interface ArbacWriteTable {
|
|
667
|
+
count(query: {
|
|
668
|
+
filter: TScopeFilter;
|
|
669
|
+
controls?: Record<string, never>;
|
|
670
|
+
}): Promise<number>;
|
|
671
|
+
getAdapter(): {
|
|
672
|
+
isInTransaction(): boolean;
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* The in-transaction write guard of the ARBAC DB layer — the body of
|
|
677
|
+
* `AsArbacDbController.guardWrite` and of `useArbacDbScope().writeOptions()`'s
|
|
678
|
+
* `guard`:
|
|
679
|
+
*
|
|
680
|
+
* - USING — every update / replace target (the exact record filter the
|
|
681
|
+
* write uses) must match the scope filter — one count, no pre-image read
|
|
682
|
+
* (missing or out of scope → 404);
|
|
683
|
+
* - `checkRefs` — every enforced FK the write sets must reference rows the
|
|
684
|
+
* caller can read on the target table (→ 403);
|
|
685
|
+
* - WITH CHECK when the adapter cannot roll back — validated here, before
|
|
686
|
+
* the write (see {@link checkArbacWrite} for the transactional path).
|
|
687
|
+
*/
|
|
688
|
+
declare function guardArbacWrite(ctx: TDbWriteGuardContext, scopes: readonly ArbacDbScope[], table: ArbacWriteTable, refs: RefTableSource): Promise<void>;
|
|
689
|
+
/**
|
|
690
|
+
* WITH CHECK after the write, inside its transaction: every written row must
|
|
691
|
+
* match the scopes' `check` (default `filter`) — else 403 and the write rolls
|
|
692
|
+
* back. Non-transactional adapters were checked in {@link guardArbacWrite}.
|
|
693
|
+
*/
|
|
694
|
+
declare function checkArbacWrite(ctx: TDbWriteCheckContext, scopes: readonly ArbacDbScope[]): Promise<void>;
|
|
695
|
+
/** USING for deletes: the exact row the delete targets must match the scope filter (else 404). */
|
|
696
|
+
declare function guardArbacRemove(ctx: TDbRemoveGuardContext, scopes: readonly ArbacDbScope[], table: Pick<ArbacWriteTable, "count">): Promise<void>;
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region src/db/use-arbac-db-scope.d.ts
|
|
699
|
+
/**
|
|
700
|
+
* The slice of an atscript-db table {@link ArbacDbScopeHelpers.assertRowsInScope}
|
|
701
|
+
* needs — satisfied by `AtscriptDbTable` / a controller's `this.table`.
|
|
702
|
+
*/
|
|
703
|
+
interface ArbacScopedTable extends VisibilityTableSource {
|
|
704
|
+
resolveRowFilter(id: unknown, opts?: TRowResolveOptions): Promise<object | null | undefined>;
|
|
705
|
+
count(query: {
|
|
706
|
+
filter: TScopeFilter;
|
|
707
|
+
controls?: Record<string, never>;
|
|
708
|
+
}): Promise<number>;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* The table surface {@link ArbacDbScopeHelpers.writeOptions} /
|
|
712
|
+
* {@link ArbacDbScopeHelpers.removeOptions} guard — satisfied by
|
|
713
|
+
* `AtscriptDbTable` / a controller's `this.table`.
|
|
714
|
+
*
|
|
715
|
+
* @since 0.1.72
|
|
716
|
+
*/
|
|
717
|
+
type ArbacGuardedTable = ArbacScopedTable & ArbacWriteTable & RefTableSource;
|
|
718
|
+
/** The current event's merged ARBAC DB scope — returned by {@link useArbacDbScope}. */
|
|
719
|
+
interface ArbacDbScopeHelpers<T = unknown> {
|
|
720
|
+
/** The evaluated scopes (one per matching rule; one composite when attenuated). */
|
|
721
|
+
scopes: ArbacDbScope<T>[];
|
|
722
|
+
/**
|
|
723
|
+
* Union of the scopes' row filters CONJOINED (`$and`) with `extra` — never
|
|
724
|
+
* spread, so `extra` can only narrow. `{}` when unrestricted.
|
|
725
|
+
*/
|
|
726
|
+
filter(extra?: TScopeFilter): TScopeFilter;
|
|
727
|
+
/** Merged `set` overrides (later scopes win) — overlay them on inserted / updated data. */
|
|
728
|
+
set(): Record<string, unknown>;
|
|
729
|
+
/**
|
|
730
|
+
* Union of the scopes' effective WITH CHECK filters (`check`, defaulting to
|
|
731
|
+
* `filter`; `{}` = none) — what a written row must match. `{}` when unrestricted.
|
|
732
|
+
*/
|
|
733
|
+
check(): TScopeFilter;
|
|
734
|
+
/**
|
|
735
|
+
* 404 `Not found` unless every id resolves (PK first, else a visible unique
|
|
736
|
+
* key — exactly one row per id) to a row inside the scope filter. Duplicate
|
|
737
|
+
* ids count once.
|
|
738
|
+
*/
|
|
739
|
+
assertRowsInScope(table: ArbacScopedTable, ids: readonly unknown[]): Promise<void>;
|
|
740
|
+
/**
|
|
741
|
+
* `checkRefs` for rows a handler is about to insert: 403 `Referenced row
|
|
742
|
+
* "<field>" is outside your scope` unless every foreign key the scopes
|
|
743
|
+
* enforce (see {@link ArbacDbScope.checkRefs}, union rule included)
|
|
744
|
+
* references a row the caller can read on the target table. Null / absent
|
|
745
|
+
* FKs pass. The same check the ARBAC DB controllers run on their CRUD writes.
|
|
746
|
+
*
|
|
747
|
+
* @since 0.1.72
|
|
748
|
+
*/
|
|
749
|
+
assertRefsInScope(table: RefTableSource, rows: ReadonlyArray<Record<string, unknown>>): Promise<void>;
|
|
750
|
+
/**
|
|
751
|
+
* `TWriteOptions` for `table.insertOne/Many`, `replaceOne` / `bulkReplace`,
|
|
752
|
+
* `updateOne` / `bulkUpdate` in a handler: the same in-transaction
|
|
753
|
+
* enforcement the ARBAC DB controllers' CRUD writes get — nested writes
|
|
754
|
+
* (`nestedWrites`, 403), USING (pre-image in scope, 404), `checkRefs` (403)
|
|
755
|
+
* and WITH CHECK (403, rolled back). `allowedFields` / `set` are not applied
|
|
756
|
+
* — overlay {@link set} yourself.
|
|
757
|
+
*
|
|
758
|
+
* @since 0.1.72
|
|
759
|
+
*/
|
|
760
|
+
writeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable): TWriteOptions<Row>;
|
|
761
|
+
/**
|
|
762
|
+
* `TDeleteOptions` for `table.deleteOne` in a handler: the id is pinned
|
|
763
|
+
* among in-scope rows and the row must match the scope filter (else 404) —
|
|
764
|
+
* the ARBAC DB controllers' `DELETE` enforcement.
|
|
765
|
+
*
|
|
766
|
+
* @since 0.1.72
|
|
767
|
+
*/
|
|
768
|
+
removeOptions<Row extends object = Record<string, unknown>>(table: ArbacGuardedTable): TDeleteOptions<Row>;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* The current event's merged ARBAC DB scope, for custom routes and
|
|
772
|
+
* `@DbAction` handlers that query the table directly. Resolves the event's
|
|
773
|
+
* scopes once (the ones the authorize interceptor cached, else an ARBAC
|
|
774
|
+
* evaluation of the current resource/action) — a deny, or an allowed
|
|
775
|
+
* evaluation with no scope left, throws 403.
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```ts
|
|
779
|
+
* const scope = await useArbacDbScope<typeof Task>();
|
|
780
|
+
* await scope.assertRowsInScope(this.table, [id]);
|
|
781
|
+
* await this.table.updateMany(scope.filter({ id }), { ...patch, ...scope.set() });
|
|
782
|
+
* // A handler-side insert with the CRUD endpoints' write enforcement.
|
|
783
|
+
* await this.table.insertOne({ ...row, ...scope.set() }, scope.writeOptions(this.table));
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
786
|
+
declare function useArbacDbScope<T = unknown>(): Promise<ArbacDbScopeHelpers<T>>;
|
|
787
|
+
//#endregion
|
|
338
788
|
//#region src/moost-arbac.d.ts
|
|
339
789
|
/**
|
|
340
790
|
* A DI-enabled extension of the `Arbac` class for use within Moost.
|
|
@@ -347,6 +797,55 @@ declare function collectMethodNames(instance: object): string[];
|
|
|
347
797
|
* @template TUserAttrs - The type representing user attributes relevant to access control.
|
|
348
798
|
* @template TScope - The type representing access control scopes.
|
|
349
799
|
*/
|
|
350
|
-
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {
|
|
800
|
+
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {
|
|
801
|
+
private scopeFieldRules;
|
|
802
|
+
/**
|
|
803
|
+
* Register the conjunction rule of custom (declaration-merged) scope
|
|
804
|
+
* fields — used wherever two scope lists are conjoined into one composite
|
|
805
|
+
* scope (credential attenuation, and the `with` sub-scopes it builds). A
|
|
806
|
+
* custom field present in a conjunction without a rule fails the request
|
|
807
|
+
* with a generic 500 (never dropped: a dropped field would read as
|
|
808
|
+
* unrestricted). A rule's optional `rowFilter` turns the field into a ROW
|
|
809
|
+
* restriction every framework row path enforces (folded into each
|
|
810
|
+
* evaluated scope's `filter`). Each field is registered once; a built-in
|
|
811
|
+
* `ArbacDbScope` key is rejected.
|
|
812
|
+
*
|
|
813
|
+
* @example
|
|
814
|
+
* ```ts
|
|
815
|
+
* arbac.registerScopeFields({
|
|
816
|
+
* teams: {
|
|
817
|
+
* conjoin: (user, cred) => intersectTeams(user, cred),
|
|
818
|
+
* rowFilter: (teams) => ({ teamId: { $in: teams } }),
|
|
819
|
+
* },
|
|
820
|
+
* });
|
|
821
|
+
* ```
|
|
822
|
+
* @since 0.1.72
|
|
823
|
+
*/
|
|
824
|
+
registerScopeFields(rules: TScopeFieldRules<TScope>): this;
|
|
825
|
+
/**
|
|
826
|
+
* The engine's evaluation with every custom field's `rowFilter` folded
|
|
827
|
+
* into each evaluated scope's `filter` (both passes of an attenuated
|
|
828
|
+
* evaluation, each with its own scopes) — so every consumer, direct
|
|
829
|
+
* engine calls included, sees the row restriction. A non-built-in scope
|
|
830
|
+
* key without a registered rule is warned about once.
|
|
831
|
+
*/
|
|
832
|
+
evaluate<T extends string | undefined>(res: {
|
|
833
|
+
resource: string;
|
|
834
|
+
action: string;
|
|
835
|
+
}, user: {
|
|
836
|
+
id: T;
|
|
837
|
+
roles: string[];
|
|
838
|
+
attrs: TUserAttrs | ((userId: T) => TUserAttrs | Promise<TUserAttrs>);
|
|
839
|
+
attenuate?: {
|
|
840
|
+
roles?: string[];
|
|
841
|
+
attrs?: Partial<TUserAttrs>;
|
|
842
|
+
allowUnheldRoles?: boolean;
|
|
843
|
+
};
|
|
844
|
+
}): Promise<TArbacEvalResult$1<TScope>>;
|
|
845
|
+
private readonly warnedFields;
|
|
846
|
+
private readonly warnUnknownField;
|
|
847
|
+
/** The registered custom scope field rules. @since 0.1.72 */
|
|
848
|
+
getScopeFields(): TScopeFieldRules<TScope>;
|
|
849
|
+
}
|
|
351
850
|
//#endregion
|
|
352
|
-
export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, type MetaVisibility, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, type VisibilityTableSource, applyAllowedFieldsAndSet, applyArbacMetaOverlay, arbacAuthorizeInterceptor, arbacPatternToRegex, buildScopeVisibility, collectMethodNames, collectWithGrantNames, collectWritableFields, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, getArbacScopes, isMetaFieldVisible, isScopedFieldVisible, metaAlwaysVisibleFields, pruneMetaByVisibility, unionScopeProjection, useArbac };
|
|
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 };
|