@aooth/arbac-moost 0.1.6 → 0.1.8
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 +56 -3
- package/dist/atscript/index.mjs +128 -24
- package/dist/index.d.mts +5 -217
- package/dist/index.mjs +116 -11
- package/dist/plugin.d.mts +13 -0
- package/dist/plugin.mjs +31 -1
- package/dist/user-B5vN7nVU.mjs +15 -0
- package/dist/{user.provider-CGWueNjx.mjs → user.provider-BiM5P8fl.mjs} +1 -1
- package/dist/user.provider-CKPMp3G8.d.mts +314 -0
- package/package.json +27 -21
- package/dist/user-ChbLPYhG.mjs +0 -15
- package/dist/user.provider-JpI5dD6Z.d.mts +0 -42
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
2
|
-
import { t as ArbacUserProvider } from "../user.provider-
|
|
1
|
+
import { t as AoothArbacUserCredentials } from "../user-B5vN7nVU.mjs";
|
|
2
|
+
import { r as AoothArbacClaims, t as ArbacUserProvider } from "../user.provider-CKPMp3G8.mjs";
|
|
3
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
4
4
|
|
|
5
5
|
//#region src/atscript/auto-provider.d.ts
|
|
@@ -88,4 +88,57 @@ declare abstract class AtscriptArbacUserProvider<T extends object = object> exte
|
|
|
88
88
|
private fetchRecord;
|
|
89
89
|
}
|
|
90
90
|
//#endregion
|
|
91
|
-
|
|
91
|
+
//#region src/atscript/attenuation-extract.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* Spec computed once per credential `TAtscriptAnnotatedType` — which typed root
|
|
94
|
+
* fields carry the restrict-only ARBAC attenuation:
|
|
95
|
+
*
|
|
96
|
+
* - `roleField` — the single `@arbac.attenuate.role`-annotated prop (holds the
|
|
97
|
+
* assumed-role SUBSET, `string[]`). More than one throws at spec time.
|
|
98
|
+
* - `attrFields` — every `@arbac.attenuate.attr "userAttr"` prop, paired with
|
|
99
|
+
* the target user-attribute key it narrows (the annotation argument).
|
|
100
|
+
*/
|
|
101
|
+
interface ArbacAttenuationSpec {
|
|
102
|
+
roleField: string | undefined;
|
|
103
|
+
attrFields: Array<{
|
|
104
|
+
field: string;
|
|
105
|
+
userAttr: string;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Walk a credential model's `@arbac.attenuate.*` annotations into a cached
|
|
110
|
+
* {@link ArbacAttenuationSpec}. Mirrors the user-side `getArbacExtractSpec`:
|
|
111
|
+
* structural, computed once, fail-loud on an ambiguous (multiple) role field.
|
|
112
|
+
*/
|
|
113
|
+
declare function getArbacAttenuationSpec(credType: TAtscriptAnnotatedType): ArbacAttenuationSpec;
|
|
114
|
+
/**
|
|
115
|
+
* Boot-time cross-model check: every `@arbac.attenuate.attr "userAttr"` target
|
|
116
|
+
* must name a real key in the user model's `@arbac.attribute` keyspace
|
|
117
|
+
* (`validUserAttrs`), else a typo silently no-ops the narrowing. Throws on the
|
|
118
|
+
* first miss. Call once at provider construction; safe and cheap (the spec is
|
|
119
|
+
* cached). Soundness does NOT depend on this — the scope conjunction clips
|
|
120
|
+
* regardless — but it turns a silent misconfig into a loud boot failure.
|
|
121
|
+
*/
|
|
122
|
+
declare function validateAttenuationTargets(credType: TAtscriptAnnotatedType, validUserAttrs: Iterable<string>): void;
|
|
123
|
+
/**
|
|
124
|
+
* Build the restrict-only {@link AoothArbacClaims} for a credential `record`
|
|
125
|
+
* (its typed root fields — e.g. the auth context, which carries the credential
|
|
126
|
+
* payload flat) by reading the fields the credential model annotates. Designed
|
|
127
|
+
* to back a consumer's `ArbacUserProvider.getAttenuation()` override while
|
|
128
|
+
* arbac-moost stays auth-agnostic (the consumer supplies the record).
|
|
129
|
+
*
|
|
130
|
+
* Returns:
|
|
131
|
+
* - `undefined` when the model declares NO attenuation fields, OR a normal
|
|
132
|
+
* (non-attenuated) token carries none of them set → full user authority.
|
|
133
|
+
* - `{ roles, attrs }` otherwise. The narrowing is then clipped by the engine's
|
|
134
|
+
* scope conjunction, so it can never widen beyond the user.
|
|
135
|
+
*
|
|
136
|
+
* Fail-closed: when the assumed-role field IS set but holds a malformed value
|
|
137
|
+
* (no usable role strings), {@link parseRoles} yields `[]` — deny-all — never
|
|
138
|
+
* full authority. Absent (`undefined`/`null`) = the field was not set, so roles
|
|
139
|
+
* are omitted (attrs-only narrowing), which is the distinct "no role narrowing"
|
|
140
|
+
* case, not a malformed one.
|
|
141
|
+
*/
|
|
142
|
+
declare function extractAttenuation(credType: TAtscriptAnnotatedType, record: object | null | undefined): AoothArbacClaims | undefined;
|
|
143
|
+
//#endregion
|
|
144
|
+
export { AoothArbacUserCredentials, type ArbacAttenuationSpec, type ArbacUserTable, AtscriptArbacUserProvider, extractAttenuation, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/atscript/index.mjs
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
import { r as __decorate, t as ArbacUserProvider } from "../user.provider-
|
|
2
|
-
import { t as AoothArbacUserCredentials } from "../user-
|
|
1
|
+
import { r as __decorate, t as ArbacUserProvider } from "../user.provider-BiM5P8fl.mjs";
|
|
2
|
+
import { t as AoothArbacUserCredentials } from "../user-B5vN7nVU.mjs";
|
|
3
3
|
import { defineWook, key } from "@wooksjs/event-core";
|
|
4
4
|
import { Injectable } from "moost";
|
|
5
|
-
//#region
|
|
5
|
+
//#region src/atscript/role-list.ts
|
|
6
|
+
/**
|
|
7
|
+
* Collect unique, non-empty strings from an iterable of unknown values,
|
|
8
|
+
* preserving first-seen order and dropping anything that is not a non-empty
|
|
9
|
+
* string. Shared by the user-side role extraction
|
|
10
|
+
* (`AtscriptArbacUserProvider.extractRoles`) and the credential-side
|
|
11
|
+
* attenuation role parse (`extractAttenuation`) so both apply identical
|
|
12
|
+
* dedup / drop-empty rules.
|
|
13
|
+
*/
|
|
14
|
+
function uniqueStrings(values) {
|
|
15
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const v of values) if (typeof v === "string" && v !== "" && !seen.has(v)) {
|
|
18
|
+
seen.add(v);
|
|
19
|
+
out.push(v);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateMetadata.js
|
|
6
25
|
function __decorateMetadata(k, v) {
|
|
7
26
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
8
27
|
}
|
|
9
28
|
//#endregion
|
|
10
29
|
//#region src/atscript/auto-provider.ts
|
|
11
|
-
const specCache = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
const specCache$1 = /* @__PURE__ */ new WeakMap();
|
|
12
31
|
/**
|
|
13
32
|
* Resolve the identifier field on an object type using the three-step chain
|
|
14
33
|
* documented on `ArbacExtractSpec.userIdField`. Used for the user type AND
|
|
@@ -47,7 +66,7 @@ function resolveIdentifierField(type) {
|
|
|
47
66
|
return metaId;
|
|
48
67
|
}
|
|
49
68
|
function getArbacExtractSpec(type) {
|
|
50
|
-
const cached = specCache.get(type);
|
|
69
|
+
const cached = specCache$1.get(type);
|
|
51
70
|
if (cached !== void 0) return cached;
|
|
52
71
|
const spec = {
|
|
53
72
|
userIdField: void 0,
|
|
@@ -56,7 +75,7 @@ function getArbacExtractSpec(type) {
|
|
|
56
75
|
};
|
|
57
76
|
const def = type.type;
|
|
58
77
|
if (def.kind !== "object") {
|
|
59
|
-
specCache.set(type, spec);
|
|
78
|
+
specCache$1.set(type, spec);
|
|
60
79
|
return spec;
|
|
61
80
|
}
|
|
62
81
|
const roleCandidates = [];
|
|
@@ -87,7 +106,7 @@ function getArbacExtractSpec(type) {
|
|
|
87
106
|
};
|
|
88
107
|
}
|
|
89
108
|
spec.userIdField = resolveIdentifierField(type);
|
|
90
|
-
specCache.set(type, spec);
|
|
109
|
+
specCache$1.set(type, spec);
|
|
91
110
|
return spec;
|
|
92
111
|
}
|
|
93
112
|
/**
|
|
@@ -127,6 +146,8 @@ const useFetchCache = defineWook((ctx) => {
|
|
|
127
146
|
return cache;
|
|
128
147
|
});
|
|
129
148
|
let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUserProvider {
|
|
149
|
+
userType;
|
|
150
|
+
table;
|
|
130
151
|
spec;
|
|
131
152
|
projection;
|
|
132
153
|
withClause;
|
|
@@ -167,22 +188,10 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
167
188
|
if (!roleField) return [];
|
|
168
189
|
const raw = record[roleField.name];
|
|
169
190
|
if (raw === void 0 || raw === null) return [];
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
seen.add(val);
|
|
175
|
-
out.push(val);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
if (roleField.shape === "inline") if (Array.isArray(raw)) for (const item of raw) push(item);
|
|
179
|
-
else push(raw);
|
|
180
|
-
else {
|
|
181
|
-
if (!Array.isArray(raw)) return out;
|
|
182
|
-
const idField = roleField.roleTargetIdField;
|
|
183
|
-
for (const item of raw) if (item && typeof item === "object") push(item[idField]);
|
|
184
|
-
}
|
|
185
|
-
return out;
|
|
191
|
+
if (roleField.shape === "inline") return uniqueStrings(Array.isArray(raw) ? raw : [raw]);
|
|
192
|
+
if (!Array.isArray(raw)) return [];
|
|
193
|
+
const idField = roleField.roleTargetIdField;
|
|
194
|
+
return uniqueStrings(raw.map((item) => item && typeof item === "object" ? item[idField] : void 0));
|
|
186
195
|
}
|
|
187
196
|
/**
|
|
188
197
|
* Override seam: `@arbac.attribute` fields keyed by prop name. Undefined
|
|
@@ -226,4 +235,99 @@ let AtscriptArbacUserProvider = class AtscriptArbacUserProvider extends ArbacUse
|
|
|
226
235
|
};
|
|
227
236
|
AtscriptArbacUserProvider = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [Object, Object])], AtscriptArbacUserProvider);
|
|
228
237
|
//#endregion
|
|
229
|
-
|
|
238
|
+
//#region src/atscript/attenuation-extract.ts
|
|
239
|
+
const specCache = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
/**
|
|
241
|
+
* Walk a credential model's `@arbac.attenuate.*` annotations into a cached
|
|
242
|
+
* {@link ArbacAttenuationSpec}. Mirrors the user-side `getArbacExtractSpec`:
|
|
243
|
+
* structural, computed once, fail-loud on an ambiguous (multiple) role field.
|
|
244
|
+
*/
|
|
245
|
+
function getArbacAttenuationSpec(credType) {
|
|
246
|
+
const cached = specCache.get(credType);
|
|
247
|
+
if (cached !== void 0) return cached;
|
|
248
|
+
const spec = {
|
|
249
|
+
roleField: void 0,
|
|
250
|
+
attrFields: []
|
|
251
|
+
};
|
|
252
|
+
const def = credType.type;
|
|
253
|
+
if (def.kind !== "object") {
|
|
254
|
+
specCache.set(credType, spec);
|
|
255
|
+
return spec;
|
|
256
|
+
}
|
|
257
|
+
const roleCandidates = [];
|
|
258
|
+
for (const [fieldName, fieldType] of def.props) {
|
|
259
|
+
const md = fieldType.metadata;
|
|
260
|
+
if (md.get("arbac.attenuate.role")) roleCandidates.push(fieldName);
|
|
261
|
+
const userAttr = md.get("arbac.attenuate.attr");
|
|
262
|
+
if (typeof userAttr === "string" && userAttr !== "") spec.attrFields.push({
|
|
263
|
+
field: fieldName,
|
|
264
|
+
userAttr
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (roleCandidates.length > 1) throw new Error(`getArbacAttenuationSpec: multiple @arbac.attenuate.role fields declared (${roleCandidates.join(", ")}). Exactly one assumed-role source is supported — drop @arbac.attenuate.role from all but the canonical field.`);
|
|
268
|
+
if (roleCandidates.length === 1) spec.roleField = roleCandidates[0];
|
|
269
|
+
specCache.set(credType, spec);
|
|
270
|
+
return spec;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Boot-time cross-model check: every `@arbac.attenuate.attr "userAttr"` target
|
|
274
|
+
* must name a real key in the user model's `@arbac.attribute` keyspace
|
|
275
|
+
* (`validUserAttrs`), else a typo silently no-ops the narrowing. Throws on the
|
|
276
|
+
* first miss. Call once at provider construction; safe and cheap (the spec is
|
|
277
|
+
* cached). Soundness does NOT depend on this — the scope conjunction clips
|
|
278
|
+
* regardless — but it turns a silent misconfig into a loud boot failure.
|
|
279
|
+
*/
|
|
280
|
+
function validateAttenuationTargets(credType, validUserAttrs) {
|
|
281
|
+
const spec = getArbacAttenuationSpec(credType);
|
|
282
|
+
const valid = new Set(validUserAttrs);
|
|
283
|
+
for (const { field, userAttr } of spec.attrFields) if (!valid.has(userAttr)) throw new Error(`validateAttenuationTargets: @arbac.attenuate.attr "${userAttr}" on field "${field}" targets a user attribute that does not exist in the user model's @arbac.attribute keyspace (${[...valid].join(", ") || "<none>"}). Fix the annotation argument.`);
|
|
284
|
+
}
|
|
285
|
+
/** Lenient role parse: accept `string | string[]`, drop empty/non-string, dedupe. */
|
|
286
|
+
function parseRoles(raw) {
|
|
287
|
+
return uniqueStrings(Array.isArray(raw) ? raw : [raw]);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Build the restrict-only {@link AoothArbacClaims} for a credential `record`
|
|
291
|
+
* (its typed root fields — e.g. the auth context, which carries the credential
|
|
292
|
+
* payload flat) by reading the fields the credential model annotates. Designed
|
|
293
|
+
* to back a consumer's `ArbacUserProvider.getAttenuation()` override while
|
|
294
|
+
* arbac-moost stays auth-agnostic (the consumer supplies the record).
|
|
295
|
+
*
|
|
296
|
+
* Returns:
|
|
297
|
+
* - `undefined` when the model declares NO attenuation fields, OR a normal
|
|
298
|
+
* (non-attenuated) token carries none of them set → full user authority.
|
|
299
|
+
* - `{ roles, attrs }` otherwise. The narrowing is then clipped by the engine's
|
|
300
|
+
* scope conjunction, so it can never widen beyond the user.
|
|
301
|
+
*
|
|
302
|
+
* Fail-closed: when the assumed-role field IS set but holds a malformed value
|
|
303
|
+
* (no usable role strings), {@link parseRoles} yields `[]` — deny-all — never
|
|
304
|
+
* full authority. Absent (`undefined`/`null`) = the field was not set, so roles
|
|
305
|
+
* are omitted (attrs-only narrowing), which is the distinct "no role narrowing"
|
|
306
|
+
* case, not a malformed one.
|
|
307
|
+
*/
|
|
308
|
+
function extractAttenuation(credType, record) {
|
|
309
|
+
const spec = getArbacAttenuationSpec(credType);
|
|
310
|
+
if (spec.roleField === void 0 && spec.attrFields.length === 0) return void 0;
|
|
311
|
+
if (!record) return void 0;
|
|
312
|
+
const rec = record;
|
|
313
|
+
let roles;
|
|
314
|
+
if (spec.roleField !== void 0) {
|
|
315
|
+
const raw = rec[spec.roleField];
|
|
316
|
+
if (raw !== void 0 && raw !== null) roles = parseRoles(raw);
|
|
317
|
+
}
|
|
318
|
+
let attrs;
|
|
319
|
+
for (const { field, userAttr } of spec.attrFields) {
|
|
320
|
+
const value = rec[field];
|
|
321
|
+
if (value !== void 0 && value !== null) {
|
|
322
|
+
attrs ??= {};
|
|
323
|
+
attrs[userAttr] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (roles === void 0 && attrs === void 0) return void 0;
|
|
327
|
+
const out = {};
|
|
328
|
+
if (roles !== void 0) out.roles = roles;
|
|
329
|
+
if (attrs !== void 0) out.attrs = attrs;
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
export { AoothArbacUserCredentials, AtscriptArbacUserProvider, extractAttenuation, getArbacAttenuationSpec, validateAttenuationTargets };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { n as ArbacUserProviderToken, t as ArbacUserProvider } from "./user.provider-
|
|
1
|
+
import { a as ArbacDbScope, c as enforceControlsPolicy, d as NavRelationKey, f as NavTarget, i as conjoinArbacDbScopes, l as extractUsedControlValues, m as ProjectionOf, n as ArbacUserProviderToken, o as AsArbacDbController, p as OwnFieldKey, r as AoothArbacClaims, s as applyAllowedFieldsAndSet, t as ArbacUserProvider, u as ControlsOf } from "./user.provider-CKPMp3G8.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, TArbacCompiledRule, TArbacEvalResult, TArbacRole, TArbacRoleForResource, TArbacRule, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { TAuthGuardDef } from "@moostjs/event-http";
|
|
4
4
|
import { EventContext } from "@wooksjs/event-core";
|
|
5
5
|
import { Mate, TMateParamMeta, TMoostMetadata } from "moost";
|
|
6
|
-
import { ControlGate,
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { TMetaResponse } from "@atscript/db";
|
|
6
|
+
import { ControlGate, TProjection } from "@aooth/arbac";
|
|
7
|
+
import { AsDbReadableController } from "@atscript/moost-db";
|
|
8
|
+
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
10
9
|
|
|
11
10
|
//#region src/arbac.composables.d.ts
|
|
12
11
|
interface ArbacBindings {
|
|
@@ -112,217 +111,6 @@ type ArbacMate = Mate<TMoostMetadata & {
|
|
|
112
111
|
*/
|
|
113
112
|
declare function getArbacMate(): ArbacMate;
|
|
114
113
|
//#endregion
|
|
115
|
-
//#region src/db/scope-types.d.ts
|
|
116
|
-
/**
|
|
117
|
-
* Unwrap a `NavPropsOf<T>[K]` value to its target model type:
|
|
118
|
-
* `Comment[]` → `Comment`, `Task` → `Task`. Used to recurse `with.<K>` into
|
|
119
|
-
* the joined model's own type so per-relation scopes get typed against the
|
|
120
|
-
* RIGHT entity (not the array wrapper).
|
|
121
|
-
*/
|
|
122
|
-
type NavTarget<U> = U extends readonly (infer V)[] ? V : U;
|
|
123
|
-
/**
|
|
124
|
-
* True only when `T` is `unknown` (or `any`). `unknown extends T` holds for
|
|
125
|
-
* both. Callers use this to pick the legacy untyped shape when no model is
|
|
126
|
-
* annotated.
|
|
127
|
-
*/
|
|
128
|
-
type IsUnknown<T> = unknown extends T ? true : false;
|
|
129
|
-
/**
|
|
130
|
-
* Permissive own-field key set. With a typed `T`, autocompletes against
|
|
131
|
-
* `keyof OwnPropsOf<T>` while still accepting dotted-path projections like
|
|
132
|
-
* `mfa.value` via the `(string & {})` escape hatch. With `T = unknown`,
|
|
133
|
-
* `OwnPropsOf<unknown>` collapses to `unknown` so `keyof` is `never` →
|
|
134
|
-
* falls back to plain `string`.
|
|
135
|
-
*/
|
|
136
|
-
type OwnFieldKey<T> = keyof OwnPropsOf<T> extends never ? string : (keyof OwnPropsOf<T> & string) | (string & {});
|
|
137
|
-
/**
|
|
138
|
-
* Same idiom for nav relation names. `NavPropsOf<unknown>` is
|
|
139
|
-
* `Record<string, never>`, so its `keyof` is `string` — the typed branch
|
|
140
|
-
* still resolves to plain `string` (the `(string & {})` escape collapses
|
|
141
|
-
* with `string`).
|
|
142
|
-
*/
|
|
143
|
-
type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof NavPropsOf<T> & string) | (string & {});
|
|
144
|
-
/**
|
|
145
|
-
* Typed projection keyed by own-field paths (with dotted-path escape).
|
|
146
|
-
* Falls back to the legacy `TProjection = Record<string, 0 | 1>` for
|
|
147
|
-
* untyped scopes so existing callers that pass the result straight to
|
|
148
|
-
* `unionProjections` / `restrictProjection` keep compiling.
|
|
149
|
-
*/
|
|
150
|
-
type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
|
|
151
|
-
/**
|
|
152
|
-
* Typed control gates: `$with` against nav names, `$select` against own
|
|
153
|
-
* fields, other `$`-prefixed controls passthrough. Falls back to the
|
|
154
|
-
* legacy untyped `Record<string, ControlGate>` for `T = unknown` so
|
|
155
|
-
* internal helpers (`unionControlsPolicy`) and existing untyped call
|
|
156
|
-
* sites keep compiling.
|
|
157
|
-
*/
|
|
158
|
-
type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate$1> : {
|
|
159
|
-
$with?: Array<NavRelationKey<T>> | boolean;
|
|
160
|
-
$select?: Array<OwnFieldKey<T>> | boolean;
|
|
161
|
-
$groupBy?: ControlGate$1;
|
|
162
|
-
$having?: ControlGate$1;
|
|
163
|
-
[key: `$${string}`]: ControlGate$1 | Array<string> | undefined;
|
|
164
|
-
};
|
|
165
|
-
//#endregion
|
|
166
|
-
//#region src/db/as-arbac-db-controller.d.ts
|
|
167
|
-
/**
|
|
168
|
-
* Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
|
|
169
|
-
*
|
|
170
|
-
* Apps can extend this interface with their own fields via TypeScript
|
|
171
|
-
* declaration merging — both at role-definition time (returned from
|
|
172
|
-
* `role.allow(...)`) and at consumption time (custom controller overrides
|
|
173
|
-
* reading `scopes[i].myCustomField` get full type safety):
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* ```ts
|
|
177
|
-
* declare module '@aooth/arbac-moost' {
|
|
178
|
-
* interface ArbacDbScope {
|
|
179
|
-
* tenantId?: string
|
|
180
|
-
* }
|
|
181
|
-
* }
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
|
-
interface ArbacDbScope<T = unknown> {
|
|
185
|
-
filter?: TScopeFilter;
|
|
186
|
-
projection?: ProjectionOf<T>;
|
|
187
|
-
set?: Partial<Record<OwnFieldKey<T>, unknown>>;
|
|
188
|
-
allowedFields?: Array<OwnFieldKey<T>>;
|
|
189
|
-
/**
|
|
190
|
-
* Per-control gates for Uniquery URL controls (`$with`, `$groupBy`, `$having`, …).
|
|
191
|
-
* Evaluated by {@link AsArbacDbController.validateControls} before query execution;
|
|
192
|
-
* a violation throws `HttpError(403)`.
|
|
193
|
-
*
|
|
194
|
-
* Per-key semantics ({@link ControlGate}):
|
|
195
|
-
* - absent / `true` — allowed.
|
|
196
|
-
* - `false` — denied entirely.
|
|
197
|
-
* - `readonly string[]` — whitelist (e.g. `{ $with: ['comments'] }` allows
|
|
198
|
-
* `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
|
|
199
|
-
* (relation names) and `$groupBy` (column names) in v1.
|
|
200
|
-
*
|
|
201
|
-
* Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
|
|
202
|
-
*
|
|
203
|
-
* @example `{ controls: { $with: false } }` — disable $with for this role.
|
|
204
|
-
* @example `{ controls: { $with: ['comments', 'owner'] } }` — restrict relations.
|
|
205
|
-
*/
|
|
206
|
-
controls?: ControlsOf<T>;
|
|
207
|
-
/**
|
|
208
|
-
* Per-relation sub-scopes applied when the request expands a relation via
|
|
209
|
-
* `?$with=<name>`. Recursive — each sub-scope has the same shape and can
|
|
210
|
-
* declare its own `with` for nested expansions (e.g. tasks → comments → task).
|
|
211
|
-
*
|
|
212
|
-
* **Authority model**: the PARENT scope owns the policy for joined rows.
|
|
213
|
-
* arbac-moost does NOT re-evaluate ARBAC against the joined resource's own
|
|
214
|
-
* scopes — that would be a confusing indirection and a perf hit. Whatever
|
|
215
|
-
* the parent declares here is what surfaces from the expansion.
|
|
216
|
-
*
|
|
217
|
-
* **Union across roles**: when multiple roles allow the same parent table,
|
|
218
|
-
* their `with[name]` sub-scopes are unioned at every nested level using the
|
|
219
|
-
* existing `unionProjections` / `mergeScopeFilters` / `unionControlsPolicy`
|
|
220
|
-
* primitives (additive: broader access wins, same rules as the parent).
|
|
221
|
-
*
|
|
222
|
-
* **Silence wins**: if no role declares `with.<name>`, expansion is
|
|
223
|
-
* unrestricted (matches `controls.$with` whitelist semantics; the gate
|
|
224
|
-
* still applies if declared).
|
|
225
|
-
*/
|
|
226
|
-
with?: WithOf<T>;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Per-relation sub-scope map. For `T = unknown` falls back to the legacy
|
|
230
|
-
* untyped `Record<string, ArbacDbScope>`. With a typed `T`, each known
|
|
231
|
-
* relation key gets its scope typed against the joined model (via
|
|
232
|
-
* `NavTarget` to unwrap arrays), while arbitrary `(string & {})` keys
|
|
233
|
-
* keep the untyped escape hatch. Lives here (not in `scope-types.ts`)
|
|
234
|
-
* because it must reference `ArbacDbScope` recursively.
|
|
235
|
-
*/
|
|
236
|
-
type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavRelationKey<T>]?: K extends keyof NavPropsOf<T> ? ArbacDbScope<NavTarget<NavPropsOf<T>[K]>> : ArbacDbScope };
|
|
237
|
-
declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
|
|
238
|
-
protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
|
|
239
|
-
protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
|
|
240
|
-
/**
|
|
241
|
-
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
242
|
-
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
243
|
-
* no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
|
|
244
|
-
* non-string position) surfaces as a server error — misleading, since the
|
|
245
|
-
* server is fine and the client sent bad input.
|
|
246
|
-
*
|
|
247
|
-
* Narrow targeting: only `SyntaxError` raised from inside the parser is
|
|
248
|
-
* remapped. Other errors (e.g. a programmer-introduced TypeError in a future
|
|
249
|
-
* override) still bubble as 500. SQL-injection-shaped payloads remain safely
|
|
250
|
-
* handled either way — see SEC-17 for the parameterisation invariant; this
|
|
251
|
-
* override is an orthogonal robustness pin around the parse step.
|
|
252
|
-
*/
|
|
253
|
-
protected parseQueryString(url: string): ReturnType<AsDbController<T>["parseQueryString"]>;
|
|
254
|
-
/**
|
|
255
|
-
* Same parser-error → 400 remap as {@link parseQueryString}, applied to the
|
|
256
|
-
* `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
|
|
257
|
-
*/
|
|
258
|
-
protected parseControlsOnlyFromUrl(url: string): ReturnType<AsDbController<T>["parseControlsOnlyFromUrl"]>;
|
|
259
|
-
/**
|
|
260
|
-
* Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
|
|
261
|
-
* controls of a request. Runs after the base validator (which checks the
|
|
262
|
-
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
263
|
-
*
|
|
264
|
-
* `transformFilter` runs first on every read endpoint and caches the
|
|
265
|
-
* evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
|
|
266
|
-
* directly here without re-evaluating ARBAC.
|
|
267
|
-
*
|
|
268
|
-
* On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
|
|
269
|
-
* `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
|
|
270
|
-
* thrown error bubbles to moost which translates it to a 403 response.
|
|
271
|
-
*/
|
|
272
|
-
protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
|
|
273
|
-
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
274
|
-
protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
|
|
275
|
-
protected onRemove(id: unknown): Promise<unknown>;
|
|
276
|
-
private assertInScope;
|
|
277
|
-
private identifierFields;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
281
|
-
* composition; regular consumers should not call this directly.
|
|
282
|
-
*
|
|
283
|
-
* Enforce a per-control policy against a parsed Uniquery `controls` map.
|
|
284
|
-
*
|
|
285
|
-
* Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
|
|
286
|
-
* trivially unit-testable; `validateControls` wires it up to the controller's
|
|
287
|
-
* cached scopes.
|
|
288
|
-
*
|
|
289
|
-
* Semantics per gate (see {@link ControlGate}):
|
|
290
|
-
* - `true` (or absent — dropped by `unionControlsPolicy`): allow.
|
|
291
|
-
* - `false`: deny if the control is used at all.
|
|
292
|
-
* - `readonly string[]`: allow only the listed values; reject any other.
|
|
293
|
-
*
|
|
294
|
-
* "Used" means the control key is present AND non-empty (an empty array is
|
|
295
|
-
* treated as not used, matching how the parser leaves missing controls).
|
|
296
|
-
*/
|
|
297
|
-
declare function enforceControlsPolicy(policy: Record<string, ControlGate$1>, controls: Record<string, unknown>): void;
|
|
298
|
-
/**
|
|
299
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
300
|
-
* composition; regular consumers should not call this directly.
|
|
301
|
-
*
|
|
302
|
-
* Extract the set of "named values" from a Uniquery control payload, for
|
|
303
|
-
* use against a whitelist gate.
|
|
304
|
-
*
|
|
305
|
-
* Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
|
|
306
|
-
* - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
|
|
307
|
-
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
|
|
308
|
-
* Bare strings are tolerated for forward compatibility.
|
|
309
|
-
* - `$groupBy` — array of column names (strings). Returned as-is.
|
|
310
|
-
*
|
|
311
|
-
* For unknown controls we return an empty array; the caller then enforces
|
|
312
|
-
* `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
|
|
313
|
-
* gate, which throws if a non-whitelistable control receives a string[]).
|
|
314
|
-
*/
|
|
315
|
-
declare function extractUsedControlValues(key: string, value: unknown): string[];
|
|
316
|
-
/**
|
|
317
|
-
* Test-friendly internal helper — exported for unit tests and helper
|
|
318
|
-
* composition; regular consumers should not call this directly.
|
|
319
|
-
*
|
|
320
|
-
* Apply the union of `allowedFields` whitelists (with `preserveFields`
|
|
321
|
-
* always preserved) and overlay each scope's `set` overrides. Returns a
|
|
322
|
-
* shallow copy; original `data` is not mutated.
|
|
323
|
-
*/
|
|
324
|
-
declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[]): unknown;
|
|
325
|
-
//#endregion
|
|
326
114
|
//#region src/db/as-arbac-db-readable-controller.d.ts
|
|
327
115
|
/**
|
|
328
116
|
* Read-only mirror of {@link AsArbacDbController} for view-style controllers
|
|
@@ -350,4 +138,4 @@ declare class AsArbacDbReadableController<T extends TAtscriptAnnotatedType = TAt
|
|
|
350
138
|
*/
|
|
351
139
|
declare class MoostArbac<TUserAttrs extends object, TScope extends object> extends Arbac$1<TUserAttrs, TScope> {}
|
|
352
140
|
//#endregion
|
|
353
|
-
export { Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|
|
141
|
+
export { type AoothArbacClaims, Arbac, ArbacAction, ArbacAuthorize, ArbacDbScope, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, type ControlGate, type ControlsOf, MoostArbac, type NavRelationKey, type NavTarget, type OwnFieldKey, type ProjectionOf, type TArbacCompiledRule, type TArbacEvalResult, TArbacMeta, type TArbacRole, type TArbacRoleForResource, type TArbacRule, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,102 @@
|
|
|
1
|
-
import { n as ArbacUserProviderToken, r as __decorate, t as ArbacUserProvider } from "./user.provider-
|
|
1
|
+
import { n as ArbacUserProviderToken, r as __decorate, t as ArbacUserProvider } from "./user.provider-BiM5P8fl.mjs";
|
|
2
2
|
import { Arbac, Arbac as Arbac$1, arbacPatternToRegex } from "@aooth/arbac-core";
|
|
3
3
|
import { Authenticate, HttpError } from "@moostjs/event-http";
|
|
4
4
|
import { current, key } from "@wooksjs/event-core";
|
|
5
5
|
import { Inherit, Injectable, TInterceptorPriority, defineBeforeInterceptor, getConstructor, getInstanceOwnMethods, getMoostMate, useControllerContext, useLogger } from "moost";
|
|
6
|
-
import { mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
|
|
6
|
+
import { conjoinScopeFilters, intersectControlsPolicy, mergeScopeFilters, restrictProjection, unionControlsPolicy, unionProjections } from "@aooth/arbac";
|
|
7
7
|
import { AsDbController, AsDbReadableController } from "@atscript/moost-db";
|
|
8
|
+
//#region src/attenuation.ts
|
|
9
|
+
/**
|
|
10
|
+
* Conjoin the full-authority scopes (`userScopes`, the ceiling) and the
|
|
11
|
+
* credential's narrowed scopes (`credScopes`) into ONE composite
|
|
12
|
+
* `ArbacDbScope`. A row/field/control is admitted only if BOTH passes admit
|
|
13
|
+
* it — the normative restrict-only clip that closes the attr-widen hole.
|
|
14
|
+
*
|
|
15
|
+
* Each side is first UNIONed with the existing additive helpers (today's
|
|
16
|
+
* machinery), then the two RESULTS are CONJOINED with the dedicated combiners
|
|
17
|
+
* ({@link conjoinScopeFilters} `$and`, {@link restrictProjection} field ∩,
|
|
18
|
+
* {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
|
|
19
|
+
* additive union helpers, which would silently widen.
|
|
20
|
+
*
|
|
21
|
+
* Returned as a single-element list so every downstream scope-application
|
|
22
|
+
* site (which UNIONs the cached scope list per facet) sees the identity of a
|
|
23
|
+
* one-element union — i.e. the conjunction — with no change to those sites.
|
|
24
|
+
*/
|
|
25
|
+
function conjoinArbacDbScopes(userScopes, credScopes) {
|
|
26
|
+
const filter = conjoinScopeFilters(mergeScopeFilters(userScopes.map((s) => s.filter ?? {})), mergeScopeFilters(credScopes.map((s) => s.filter ?? {})));
|
|
27
|
+
const projection = restrictProjection(unionProjections(...userScopes.map((s) => s.projection ?? {})), unionProjections(...credScopes.map((s) => s.projection ?? {})));
|
|
28
|
+
const controls = intersectControlsPolicy(unionControlsPolicy(userScopes), unionControlsPolicy(credScopes));
|
|
29
|
+
const allowedFields = intersectAllowedFields(userScopes, credScopes);
|
|
30
|
+
const set = combineSet(userScopes, credScopes);
|
|
31
|
+
const withMap = conjoinWith(userScopes, credScopes);
|
|
32
|
+
const s = {};
|
|
33
|
+
if (filter && Object.keys(filter).length > 0) s.filter = filter;
|
|
34
|
+
if (Object.keys(projection).length > 0) s.projection = projection;
|
|
35
|
+
if (Object.keys(controls).length > 0) s.controls = controls;
|
|
36
|
+
if (allowedFields) s.allowedFields = allowedFields;
|
|
37
|
+
if (set) s.set = set;
|
|
38
|
+
if (withMap) s.with = withMap;
|
|
39
|
+
return [s];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Intersect the two sides' `allowedFields` write-whitelists (the credential
|
|
43
|
+
* may write FEWER fields). A side with no whitelist is unrestricted (all
|
|
44
|
+
* fields), so the other side wins; both restricting → set intersection.
|
|
45
|
+
*/
|
|
46
|
+
function intersectAllowedFields(userScopes, credScopes) {
|
|
47
|
+
const u = unionAllowedFields(userScopes);
|
|
48
|
+
const c = unionAllowedFields(credScopes);
|
|
49
|
+
if (u === void 0) return c;
|
|
50
|
+
if (c === void 0) return u;
|
|
51
|
+
const cset = new Set(c);
|
|
52
|
+
return [...new Set(u)].filter((f) => cset.has(f)).toSorted();
|
|
53
|
+
}
|
|
54
|
+
function unionAllowedFields(scopes) {
|
|
55
|
+
let set;
|
|
56
|
+
for (const s of scopes) if (Array.isArray(s.allowedFields)) {
|
|
57
|
+
set ??= /* @__PURE__ */ new Set();
|
|
58
|
+
for (const f of s.allowedFields) set.add(f);
|
|
59
|
+
}
|
|
60
|
+
return set ? [...set] : void 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Combine the two sides' `set` overlays. `set` forces field values on writes;
|
|
64
|
+
* both sides' forced constraints apply, and on a key conflict the USER's value
|
|
65
|
+
* wins so the credential can only ADD constraints, never override the owner's.
|
|
66
|
+
*/
|
|
67
|
+
function combineSet(userScopes, credScopes) {
|
|
68
|
+
const u = unionSet(userScopes);
|
|
69
|
+
const c = unionSet(credScopes);
|
|
70
|
+
if (!u && !c) return void 0;
|
|
71
|
+
return {
|
|
72
|
+
...c,
|
|
73
|
+
...u
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function unionSet(scopes) {
|
|
77
|
+
let out;
|
|
78
|
+
for (const s of scopes) if (s.set) {
|
|
79
|
+
out ??= {};
|
|
80
|
+
Object.assign(out, s.set);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Recurse the conjunction into joined-resource sub-scopes. A relation declared
|
|
86
|
+
* on only one side is conjoined against the other side's silence (unrestricted
|
|
87
|
+
* = identity), so a credential can narrow a relation the user left open but
|
|
88
|
+
* never widen one the user restricted.
|
|
89
|
+
*/
|
|
90
|
+
function conjoinWith(userScopes, credScopes) {
|
|
91
|
+
const relNames = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const s of userScopes) if (s.with) for (const k of Object.keys(s.with)) relNames.add(k);
|
|
93
|
+
for (const s of credScopes) if (s.with) for (const k of Object.keys(s.with)) relNames.add(k);
|
|
94
|
+
if (relNames.size === 0) return void 0;
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const rel of relNames) out[rel] = conjoinArbacDbScopes(userScopes.map((s) => s.with?.[rel]).filter(Boolean), credScopes.map((s) => s.with?.[rel]).filter(Boolean))[0];
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
8
100
|
//#region src/moost-arbac.ts
|
|
9
101
|
let MoostArbac = class MoostArbac extends Arbac$1 {};
|
|
10
102
|
MoostArbac = __decorate([Injectable()], MoostArbac);
|
|
@@ -52,15 +144,28 @@ const useArbac = (_ctx) => {
|
|
|
52
144
|
if (!effectiveAction) throw new Error("useArbac().evaluate(): `action` is required — could not be resolved from controller/method metadata. Pass it explicitly.");
|
|
53
145
|
const [user, arbac] = await Promise.all([cc.instantiate(ArbacUserProviderToken), cc.instantiate(MoostArbac)]);
|
|
54
146
|
const userId = await user.getUserId();
|
|
147
|
+
const att = user.getAttenuation ? await user.getAttenuation() : void 0;
|
|
148
|
+
const attenuate = att && (att.roles !== void 0 || att.attrs !== void 0) ? {
|
|
149
|
+
roles: att.roles,
|
|
150
|
+
attrs: att.attrs
|
|
151
|
+
} : void 0;
|
|
152
|
+
const result = await arbac.evaluate({
|
|
153
|
+
resource: effectiveResource,
|
|
154
|
+
action: effectiveAction
|
|
155
|
+
}, {
|
|
156
|
+
id: userId,
|
|
157
|
+
roles: await user.getRoles(userId),
|
|
158
|
+
attrs: (id) => user.getAttrs(id),
|
|
159
|
+
attenuate
|
|
160
|
+
});
|
|
161
|
+
if (result.allowed && result.credScopes !== void 0) return {
|
|
162
|
+
allowed: true,
|
|
163
|
+
scopes: conjoinArbacDbScopes(result.scopes ?? [], result.credScopes),
|
|
164
|
+
userId
|
|
165
|
+
};
|
|
55
166
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
action: effectiveAction
|
|
59
|
-
}, {
|
|
60
|
-
id: userId,
|
|
61
|
-
roles: await user.getRoles(userId),
|
|
62
|
-
attrs: (id) => user.getAttrs(id)
|
|
63
|
-
}),
|
|
167
|
+
allowed: result.allowed,
|
|
168
|
+
scopes: result.scopes,
|
|
64
169
|
userId
|
|
65
170
|
};
|
|
66
171
|
};
|
|
@@ -516,4 +621,4 @@ let AsArbacDbReadableController = class AsArbacDbReadableController extends AsDb
|
|
|
516
621
|
};
|
|
517
622
|
AsArbacDbReadableController = __decorate([Inherit()], AsArbacDbReadableController);
|
|
518
623
|
//#endregion
|
|
519
|
-
export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|
|
624
|
+
export { Arbac, ArbacAction, ArbacAuthorize, ArbacResource, ArbacUserProvider, ArbacUserProviderToken, AsArbacDbController, AsArbacDbReadableController, MoostArbac, applyAllowedFieldsAndSet, arbacAuthorizeInterceptor, arbacPatternToRegex, conjoinArbacDbScopes, enforceControlsPolicy, extractUsedControlValues, getArbacMate, useArbac };
|
package/dist/plugin.d.mts
CHANGED
|
@@ -16,6 +16,19 @@ import { TAtscriptPlugin } from "@atscript/core";
|
|
|
16
16
|
* (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
|
|
17
17
|
* group, (3) `@meta.id` — first match wins.
|
|
18
18
|
*
|
|
19
|
+
* Credential-side (restrict-only attenuation — a credential authorizes for
|
|
20
|
+
* strictly LESS than its owning user; soundness is the engine's scope
|
|
21
|
+
* conjunction, these only mark WHERE the narrowing inputs live):
|
|
22
|
+
*
|
|
23
|
+
* - `@arbac.attenuate.role` — marks the ONE credential field holding the
|
|
24
|
+
* assumed-role SUBSET (`string[]`). Intersected with the user's roles
|
|
25
|
+
* (fail-closed: a claimed role the user lacks is dropped). Exactly one per
|
|
26
|
+
* type — multiple declarations throw at boot.
|
|
27
|
+
* - `@arbac.attenuate.attr "userAttrName"` — marks a credential field whose
|
|
28
|
+
* value narrows the named USER attribute. The string argument is the target
|
|
29
|
+
* key in the user model's `@arbac.attribute` keyspace (validated to exist at
|
|
30
|
+
* boot). Multiple fields may each carry it, targeting different attrs.
|
|
31
|
+
*
|
|
19
32
|
* Install in `atscript.config.ts`:
|
|
20
33
|
*
|
|
21
34
|
* ```ts
|
package/dist/plugin.mjs
CHANGED
|
@@ -15,6 +15,19 @@ import { AnnotationSpec } from "@atscript/core";
|
|
|
15
15
|
* (1) `@arbac.userId`, (2) the single field of the `@db.table.preferredId.uniqueIndex`
|
|
16
16
|
* group, (3) `@meta.id` — first match wins.
|
|
17
17
|
*
|
|
18
|
+
* Credential-side (restrict-only attenuation — a credential authorizes for
|
|
19
|
+
* strictly LESS than its owning user; soundness is the engine's scope
|
|
20
|
+
* conjunction, these only mark WHERE the narrowing inputs live):
|
|
21
|
+
*
|
|
22
|
+
* - `@arbac.attenuate.role` — marks the ONE credential field holding the
|
|
23
|
+
* assumed-role SUBSET (`string[]`). Intersected with the user's roles
|
|
24
|
+
* (fail-closed: a claimed role the user lacks is dropped). Exactly one per
|
|
25
|
+
* type — multiple declarations throw at boot.
|
|
26
|
+
* - `@arbac.attenuate.attr "userAttrName"` — marks a credential field whose
|
|
27
|
+
* value narrows the named USER attribute. The string argument is the target
|
|
28
|
+
* key in the user model's `@arbac.attribute` keyspace (validated to exist at
|
|
29
|
+
* boot). Multiple fields may each carry it, targeting different attrs.
|
|
30
|
+
*
|
|
18
31
|
* Install in `atscript.config.ts`:
|
|
19
32
|
*
|
|
20
33
|
* ```ts
|
|
@@ -41,7 +54,24 @@ function arbacPlugin() {
|
|
|
41
54
|
description: "Overrides which field provides the user identifier for ARBAC. Resolution chain: @arbac.userId → @db.table.preferredId.uniqueIndex field → @meta.id.",
|
|
42
55
|
nodeType: ["prop"],
|
|
43
56
|
multiple: false
|
|
44
|
-
})
|
|
57
|
+
}),
|
|
58
|
+
attenuate: {
|
|
59
|
+
role: new AnnotationSpec({
|
|
60
|
+
description: "Marks the credential field holding the assumed-role SUBSET (string[]) for restrict-only ARBAC attenuation. Intersected with the user's roles (fail-closed). Exactly one @arbac.attenuate.role field per type — multiple declarations throw at boot.",
|
|
61
|
+
nodeType: ["prop"],
|
|
62
|
+
multiple: false
|
|
63
|
+
}),
|
|
64
|
+
attr: new AnnotationSpec({
|
|
65
|
+
description: "Marks a credential field whose value narrows the named USER attribute for restrict-only ARBAC attenuation. The argument is the target key in the user model's @arbac.attribute keyspace (validated to exist at boot). Multiple fields may each carry it, targeting different attrs.",
|
|
66
|
+
nodeType: ["prop"],
|
|
67
|
+
multiple: false,
|
|
68
|
+
argument: {
|
|
69
|
+
name: "userAttr",
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Target user-attribute key (a field annotated @arbac.attribute on the user model)."
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
45
75
|
} } };
|
|
46
76
|
}
|
|
47
77
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
|
|
2
|
+
import "@aooth/user/atscript-db/model.as";
|
|
3
|
+
//#region src/atscript/models/user.as
|
|
4
|
+
var AoothArbacUserCredentials = class {
|
|
5
|
+
static __is_atscript_annotated_type = true;
|
|
6
|
+
static type = {};
|
|
7
|
+
static metadata = /* @__PURE__ */ new Map();
|
|
8
|
+
static id = "AoothArbacUserCredentials";
|
|
9
|
+
static toJsonSchema() {
|
|
10
|
+
throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
defineAnnotatedType("object", AoothArbacUserCredentials).prop("id", defineAnnotatedType().designType("string").tags("string").annotate("meta.id", true).annotate("db.default.uuid", true).$type).prop("username", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "username_idx", true).$type).prop("email", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "email_idx", true).optional().$type).prop("version", defineAnnotatedType().designType("number").tags("int", "number").annotate("db.column.version", true).annotate("expect.int", true).$type).prop("password", defineAnnotatedType("object").prop("hash", defineAnnotatedType().designType("string").tags("string").$type).prop("history", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).prop("lastChanged", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("isInitial", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("account", defineAnnotatedType("object").prop("active", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("locked", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("lockReason", defineAnnotatedType().designType("string").tags("string").$type).prop("lockEnds", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("failedLoginAttempts", defineAnnotatedType().designType("number").tags("number").$type).prop("lastLogin", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("pendingInvitation", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).annotate("db.patch.strategy", "merge").$type).prop("mfa", defineAnnotatedType("object").prop("methods", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("name", defineAnnotatedType().designType("string").tags("string").$type).prop("confirmed", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("value", defineAnnotatedType().designType("string").tags("string").$type).prop("lastUsedWindow", defineAnnotatedType().designType("number").tags("int", "number").annotate("expect.int", true).optional().$type).$type).$type).prop("defaultMethod", defineAnnotatedType().designType("string").tags("string").$type).prop("autoSend", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("trustedDevices", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("token", defineAnnotatedType().designType("string").tags("string").$type).prop("ip", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("issuedAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("expiresAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("name", defineAnnotatedType().designType("string").tags("string").optional().$type).$type).annotate("db.patch.strategy", "merge").optional().$type).prop("roles", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("arbac.role", true).$type);
|
|
14
|
+
//#endregion
|
|
15
|
+
export { AoothArbacUserCredentials as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { TClassConstructor } from "moost";
|
|
2
|
+
import { ControlGate, TProjection, TScopeFilter } from "@aooth/arbac";
|
|
3
|
+
import { AsDbController } from "@atscript/moost-db";
|
|
4
|
+
import { NavPropsOf, OwnPropsOf, TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
5
|
+
import { TMetaResponse } from "@atscript/db";
|
|
6
|
+
|
|
7
|
+
//#region src/db/scope-types.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Unwrap a `NavPropsOf<T>[K]` value to its target model type:
|
|
10
|
+
* `Comment[]` → `Comment`, `Task` → `Task`. Used to recurse `with.<K>` into
|
|
11
|
+
* the joined model's own type so per-relation scopes get typed against the
|
|
12
|
+
* RIGHT entity (not the array wrapper).
|
|
13
|
+
*/
|
|
14
|
+
type NavTarget<U> = U extends readonly (infer V)[] ? V : U;
|
|
15
|
+
/**
|
|
16
|
+
* True only when `T` is `unknown` (or `any`). `unknown extends T` holds for
|
|
17
|
+
* both. Callers use this to pick the legacy untyped shape when no model is
|
|
18
|
+
* annotated.
|
|
19
|
+
*/
|
|
20
|
+
type IsUnknown<T> = unknown extends T ? true : false;
|
|
21
|
+
/**
|
|
22
|
+
* Permissive own-field key set. With a typed `T`, autocompletes against
|
|
23
|
+
* `keyof OwnPropsOf<T>` while still accepting dotted-path projections like
|
|
24
|
+
* `mfa.value` via the `(string & {})` escape hatch. With `T = unknown`,
|
|
25
|
+
* `OwnPropsOf<unknown>` collapses to `unknown` so `keyof` is `never` →
|
|
26
|
+
* falls back to plain `string`.
|
|
27
|
+
*/
|
|
28
|
+
type OwnFieldKey<T> = keyof OwnPropsOf<T> extends never ? string : (keyof OwnPropsOf<T> & string) | (string & {});
|
|
29
|
+
/**
|
|
30
|
+
* Same idiom for nav relation names. `NavPropsOf<unknown>` is
|
|
31
|
+
* `Record<string, never>`, so its `keyof` is `string` — the typed branch
|
|
32
|
+
* still resolves to plain `string` (the `(string & {})` escape collapses
|
|
33
|
+
* with `string`).
|
|
34
|
+
*/
|
|
35
|
+
type NavRelationKey<T> = keyof NavPropsOf<T> extends never ? string : (keyof NavPropsOf<T> & string) | (string & {});
|
|
36
|
+
/**
|
|
37
|
+
* Typed projection keyed by own-field paths (with dotted-path escape).
|
|
38
|
+
* Falls back to the legacy `TProjection = Record<string, 0 | 1>` for
|
|
39
|
+
* untyped scopes so existing callers that pass the result straight to
|
|
40
|
+
* `unionProjections` / `restrictProjection` keep compiling.
|
|
41
|
+
*/
|
|
42
|
+
type ProjectionOf<T> = IsUnknown<T> extends true ? TProjection : Partial<Record<OwnFieldKey<T>, 0 | 1>>;
|
|
43
|
+
/**
|
|
44
|
+
* Typed control gates: `$with` against nav names, `$select` against own
|
|
45
|
+
* fields, other `$`-prefixed controls passthrough. Falls back to the
|
|
46
|
+
* legacy untyped `Record<string, ControlGate>` for `T = unknown` so
|
|
47
|
+
* internal helpers (`unionControlsPolicy`) and existing untyped call
|
|
48
|
+
* sites keep compiling.
|
|
49
|
+
*/
|
|
50
|
+
type ControlsOf<T> = IsUnknown<T> extends true ? Record<string, ControlGate> : {
|
|
51
|
+
$with?: Array<NavRelationKey<T>> | boolean;
|
|
52
|
+
$select?: Array<OwnFieldKey<T>> | boolean;
|
|
53
|
+
$groupBy?: ControlGate;
|
|
54
|
+
$having?: ControlGate;
|
|
55
|
+
[key: `$${string}`]: ControlGate | Array<string> | undefined;
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/db/as-arbac-db-controller.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* Contract returned by an ARBAC role's scope predicate on a DB-backed resource.
|
|
61
|
+
*
|
|
62
|
+
* Apps can extend this interface with their own fields via TypeScript
|
|
63
|
+
* declaration merging — both at role-definition time (returned from
|
|
64
|
+
* `role.allow(...)`) and at consumption time (custom controller overrides
|
|
65
|
+
* reading `scopes[i].myCustomField` get full type safety):
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* declare module '@aooth/arbac-moost' {
|
|
70
|
+
* interface ArbacDbScope {
|
|
71
|
+
* tenantId?: string
|
|
72
|
+
* }
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
interface ArbacDbScope<T = unknown> {
|
|
77
|
+
filter?: TScopeFilter;
|
|
78
|
+
projection?: ProjectionOf<T>;
|
|
79
|
+
set?: Partial<Record<OwnFieldKey<T>, unknown>>;
|
|
80
|
+
allowedFields?: Array<OwnFieldKey<T>>;
|
|
81
|
+
/**
|
|
82
|
+
* Per-control gates for Uniquery URL controls (`$with`, `$groupBy`, `$having`, …).
|
|
83
|
+
* Evaluated by {@link AsArbacDbController.validateControls} before query execution;
|
|
84
|
+
* a violation throws `HttpError(403)`.
|
|
85
|
+
*
|
|
86
|
+
* Per-key semantics ({@link ControlGate}):
|
|
87
|
+
* - absent / `true` — allowed.
|
|
88
|
+
* - `false` — denied entirely.
|
|
89
|
+
* - `readonly string[]` — whitelist (e.g. `{ $with: ['comments'] }` allows
|
|
90
|
+
* `?$with=comments`, rejects `?$with=tasks`). Supported only for `$with`
|
|
91
|
+
* (relation names) and `$groupBy` (column names) in v1.
|
|
92
|
+
*
|
|
93
|
+
* Across roles, gates union additively (silence wins) via {@link unionControlsPolicy}.
|
|
94
|
+
*
|
|
95
|
+
* @example `{ controls: { $with: false } }` — disable $with for this role.
|
|
96
|
+
* @example `{ controls: { $with: ['comments', 'owner'] } }` — restrict relations.
|
|
97
|
+
*/
|
|
98
|
+
controls?: ControlsOf<T>;
|
|
99
|
+
/**
|
|
100
|
+
* Per-relation sub-scopes applied when the request expands a relation via
|
|
101
|
+
* `?$with=<name>`. Recursive — each sub-scope has the same shape and can
|
|
102
|
+
* declare its own `with` for nested expansions (e.g. tasks → comments → task).
|
|
103
|
+
*
|
|
104
|
+
* **Authority model**: the PARENT scope owns the policy for joined rows.
|
|
105
|
+
* arbac-moost does NOT re-evaluate ARBAC against the joined resource's own
|
|
106
|
+
* scopes — that would be a confusing indirection and a perf hit. Whatever
|
|
107
|
+
* the parent declares here is what surfaces from the expansion.
|
|
108
|
+
*
|
|
109
|
+
* **Union across roles**: when multiple roles allow the same parent table,
|
|
110
|
+
* their `with[name]` sub-scopes are unioned at every nested level using the
|
|
111
|
+
* existing `unionProjections` / `mergeScopeFilters` / `unionControlsPolicy`
|
|
112
|
+
* primitives (additive: broader access wins, same rules as the parent).
|
|
113
|
+
*
|
|
114
|
+
* **Silence wins**: if no role declares `with.<name>`, expansion is
|
|
115
|
+
* unrestricted (matches `controls.$with` whitelist semantics; the gate
|
|
116
|
+
* still applies if declared).
|
|
117
|
+
*/
|
|
118
|
+
with?: WithOf<T>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Per-relation sub-scope map. For `T = unknown` falls back to the legacy
|
|
122
|
+
* untyped `Record<string, ArbacDbScope>`. With a typed `T`, each known
|
|
123
|
+
* relation key gets its scope typed against the joined model (via
|
|
124
|
+
* `NavTarget` to unwrap arrays), while arbitrary `(string & {})` keys
|
|
125
|
+
* keep the untyped escape hatch. Lives here (not in `scope-types.ts`)
|
|
126
|
+
* because it must reference `ArbacDbScope` recursively.
|
|
127
|
+
*/
|
|
128
|
+
type WithOf<T> = unknown extends T ? Record<string, ArbacDbScope> : { [K in NavRelationKey<T>]?: K extends keyof NavPropsOf<T> ? ArbacDbScope<NavTarget<NavPropsOf<T>[K]>> : ArbacDbScope };
|
|
129
|
+
declare class AsArbacDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType> extends AsDbController<T> {
|
|
130
|
+
protected transformFilter(filter: Record<string, unknown> | undefined): Promise<Record<string, unknown>>;
|
|
131
|
+
protected transformProjection(projection?: TProjection): TProjection | undefined | Promise<TProjection | undefined>;
|
|
132
|
+
/**
|
|
133
|
+
* Translate `@uniqu/url` parser failures into HTTP 400 instead of letting them
|
|
134
|
+
* bubble as 500. The base `parseQueryString` calls `parseUrl()` directly with
|
|
135
|
+
* no try/catch, so a malformed `?status=...` (e.g. unquoted single quote at a
|
|
136
|
+
* non-string position) surfaces as a server error — misleading, since the
|
|
137
|
+
* server is fine and the client sent bad input.
|
|
138
|
+
*
|
|
139
|
+
* Narrow targeting: only `SyntaxError` raised from inside the parser is
|
|
140
|
+
* remapped. Other errors (e.g. a programmer-introduced TypeError in a future
|
|
141
|
+
* override) still bubble as 500. SQL-injection-shaped payloads remain safely
|
|
142
|
+
* handled either way — see SEC-17 for the parameterisation invariant; this
|
|
143
|
+
* override is an orthogonal robustness pin around the parse step.
|
|
144
|
+
*/
|
|
145
|
+
protected parseQueryString(url: string): ReturnType<AsDbController<T>["parseQueryString"]>;
|
|
146
|
+
/**
|
|
147
|
+
* Same parser-error → 400 remap as {@link parseQueryString}, applied to the
|
|
148
|
+
* `/one` and `/one?…` code paths which use `parseControlsOnlyFromUrl`.
|
|
149
|
+
*/
|
|
150
|
+
protected parseControlsOnlyFromUrl(url: string): ReturnType<AsDbController<T>["parseControlsOnlyFromUrl"]>;
|
|
151
|
+
/**
|
|
152
|
+
* Enforce per-role `ArbacDbScope.controls` gates against the parsed Uniquery
|
|
153
|
+
* controls of a request. Runs after the base validator (which checks the
|
|
154
|
+
* controls DTO shape) and BEFORE the query/aggregation pipeline executes.
|
|
155
|
+
*
|
|
156
|
+
* `transformFilter` runs first on every read endpoint and caches the
|
|
157
|
+
* evaluated scopes via `arbac.setScopes(...)`, so we read those scopes
|
|
158
|
+
* directly here without re-evaluating ARBAC.
|
|
159
|
+
*
|
|
160
|
+
* On a violation we throw `HttpError(403)`. moost-db's `query` / `pages` /
|
|
161
|
+
* `getOne` handlers do NOT wrap `validateParsed` in try/catch, so the
|
|
162
|
+
* thrown error bubbles to moost which translates it to a 403 response.
|
|
163
|
+
*/
|
|
164
|
+
protected validateControls(controls: Record<string, unknown>, type: "query" | "pages" | "getOne"): string | undefined;
|
|
165
|
+
protected applyMetaOverlay(meta: TMetaResponse): Promise<TMetaResponse>;
|
|
166
|
+
protected onWrite(action: "insert" | "insertMany" | "replace" | "replaceMany" | "update" | "updateMany", data: unknown): Promise<unknown>;
|
|
167
|
+
protected onRemove(id: unknown): Promise<unknown>;
|
|
168
|
+
private assertInScope;
|
|
169
|
+
private identifierFields;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Test-friendly internal helper — exported for unit tests and helper
|
|
173
|
+
* composition; regular consumers should not call this directly.
|
|
174
|
+
*
|
|
175
|
+
* Enforce a per-control policy against a parsed Uniquery `controls` map.
|
|
176
|
+
*
|
|
177
|
+
* Throws `HttpError(403)` on the first violation. Pure (no DI) so it is
|
|
178
|
+
* trivially unit-testable; `validateControls` wires it up to the controller's
|
|
179
|
+
* cached scopes.
|
|
180
|
+
*
|
|
181
|
+
* Semantics per gate (see {@link ControlGate}):
|
|
182
|
+
* - `true` (or absent — dropped by `unionControlsPolicy`): allow.
|
|
183
|
+
* - `false`: deny if the control is used at all.
|
|
184
|
+
* - `readonly string[]`: allow only the listed values; reject any other.
|
|
185
|
+
*
|
|
186
|
+
* "Used" means the control key is present AND non-empty (an empty array is
|
|
187
|
+
* treated as not used, matching how the parser leaves missing controls).
|
|
188
|
+
*/
|
|
189
|
+
declare function enforceControlsPolicy(policy: Record<string, ControlGate>, controls: Record<string, unknown>): void;
|
|
190
|
+
/**
|
|
191
|
+
* Test-friendly internal helper — exported for unit tests and helper
|
|
192
|
+
* composition; regular consumers should not call this directly.
|
|
193
|
+
*
|
|
194
|
+
* Extract the set of "named values" from a Uniquery control payload, for
|
|
195
|
+
* use against a whitelist gate.
|
|
196
|
+
*
|
|
197
|
+
* Currently supported (matches `WHITELISTABLE_CONTROLS` in `unionControlsPolicy`):
|
|
198
|
+
* - `$with` — array of `{ name, … }` objects (per `TypedWithRelation`,
|
|
199
|
+
* see `@uniqu/core` parser at `parseWithSegment`); we extract `name`.
|
|
200
|
+
* Bare strings are tolerated for forward compatibility.
|
|
201
|
+
* - `$groupBy` — array of column names (strings). Returned as-is.
|
|
202
|
+
*
|
|
203
|
+
* For unknown controls we return an empty array; the caller then enforces
|
|
204
|
+
* `false`-only semantics (controlled by `unionControlsPolicy`'s whitelist
|
|
205
|
+
* gate, which throws if a non-whitelistable control receives a string[]).
|
|
206
|
+
*/
|
|
207
|
+
declare function extractUsedControlValues(key: string, value: unknown): string[];
|
|
208
|
+
/**
|
|
209
|
+
* Test-friendly internal helper — exported for unit tests and helper
|
|
210
|
+
* composition; regular consumers should not call this directly.
|
|
211
|
+
*
|
|
212
|
+
* Apply the union of `allowedFields` whitelists (with `preserveFields`
|
|
213
|
+
* always preserved) and overlay each scope's `set` overrides. Returns a
|
|
214
|
+
* shallow copy; original `data` is not mutated.
|
|
215
|
+
*/
|
|
216
|
+
declare function applyAllowedFieldsAndSet(data: unknown, scopes: ArbacDbScope[], preserveFields?: readonly string[]): unknown;
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/attenuation.d.ts
|
|
219
|
+
/**
|
|
220
|
+
* The restrict-only ARBAC attenuation carried by a credential — its assumed
|
|
221
|
+
* role SUBSET and narrowing attribute overrides. Sourced into evaluation via
|
|
222
|
+
* the optional `ArbacUserProvider.getAttenuation()` hook (typically built by
|
|
223
|
+
* walking the credential model's `@arbac.attenuate.*`-annotated typed root
|
|
224
|
+
* fields with {@link extractAttenuation}), it NARROWS (never expands) the
|
|
225
|
+
* principal — see the engine's `evaluate({ attenuate })` for the restrict-only
|
|
226
|
+
* outcome-intersection.
|
|
227
|
+
*
|
|
228
|
+
* - `roles` — assume a SUBSET of the user's roles. `[]` = no roles (deny-all,
|
|
229
|
+
* fail-closed); an OMITTED key = keep all the user's roles (attrs-only
|
|
230
|
+
* narrowing); a role the user lacks is dropped by the intersection.
|
|
231
|
+
* - `attrs` — extra/overriding inputs to scope predicates, keyed by the target
|
|
232
|
+
* user-attribute name, intended to narrow scopes. They are merged LOCALLY
|
|
233
|
+
* into the credential pass only and clipped by the scope conjunction, so they
|
|
234
|
+
* can never widen beyond the user.
|
|
235
|
+
*/
|
|
236
|
+
interface AoothArbacClaims {
|
|
237
|
+
roles?: string[];
|
|
238
|
+
attrs?: Record<string, unknown>;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Conjoin the full-authority scopes (`userScopes`, the ceiling) and the
|
|
242
|
+
* credential's narrowed scopes (`credScopes`) into ONE composite
|
|
243
|
+
* `ArbacDbScope`. A row/field/control is admitted only if BOTH passes admit
|
|
244
|
+
* it — the normative restrict-only clip that closes the attr-widen hole.
|
|
245
|
+
*
|
|
246
|
+
* Each side is first UNIONed with the existing additive helpers (today's
|
|
247
|
+
* machinery), then the two RESULTS are CONJOINED with the dedicated combiners
|
|
248
|
+
* ({@link conjoinScopeFilters} `$and`, {@link restrictProjection} field ∩,
|
|
249
|
+
* {@link intersectControlsPolicy} deny-wins, `with` recursion) — never the
|
|
250
|
+
* additive union helpers, which would silently widen.
|
|
251
|
+
*
|
|
252
|
+
* Returned as a single-element list so every downstream scope-application
|
|
253
|
+
* site (which UNIONs the cached scope list per facet) sees the identity of a
|
|
254
|
+
* one-element union — i.e. the conjunction — with no change to those sites.
|
|
255
|
+
*/
|
|
256
|
+
declare function conjoinArbacDbScopes(userScopes: ArbacDbScope[], credScopes: ArbacDbScope[]): ArbacDbScope[];
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/user.provider.d.ts
|
|
259
|
+
/**
|
|
260
|
+
* Abstract base class for providing user data required for ARBAC evaluations.
|
|
261
|
+
*
|
|
262
|
+
* Consumers must extend this class and implement the three abstract methods
|
|
263
|
+
* to plug their own authentication/identity layer into the ARBAC authorize
|
|
264
|
+
* interceptor. The subclass must re-apply `@Injectable()` (moost does not
|
|
265
|
+
* inherit decorator metadata) and be registered via Moost's
|
|
266
|
+
* `setReplaceRegistry` (or `@Replace`) so DI resolves the concrete provider.
|
|
267
|
+
*
|
|
268
|
+
* @template TUserAttrs - The type representing user attributes relevant to access control.
|
|
269
|
+
*/
|
|
270
|
+
declare abstract class ArbacUserProvider<TUserAttrs extends object = object> {
|
|
271
|
+
/**
|
|
272
|
+
* Retrieves the unique identifier of the current user.
|
|
273
|
+
*/
|
|
274
|
+
abstract getUserId(): string | Promise<string>;
|
|
275
|
+
/**
|
|
276
|
+
* Retrieves the roles assigned to a user based on their ID.
|
|
277
|
+
*
|
|
278
|
+
* @param id - The user ID.
|
|
279
|
+
*/
|
|
280
|
+
abstract getRoles(id: string): string[] | Promise<string[]>;
|
|
281
|
+
/**
|
|
282
|
+
* Retrieves the attributes associated with a user based on their ID.
|
|
283
|
+
*
|
|
284
|
+
* @param id - The user ID.
|
|
285
|
+
*/
|
|
286
|
+
abstract getAttrs(id: string): TUserAttrs | Promise<TUserAttrs>;
|
|
287
|
+
/**
|
|
288
|
+
* OPTIONAL: source the credential's restrict-only ARBAC attenuation for the
|
|
289
|
+
* current request. When it returns claims, `useArbac().evaluate()` runs the
|
|
290
|
+
* engine's dual-pass outcome-intersection so the credential can do/see/affect
|
|
291
|
+
* strictly LESS than its owning user; returning `undefined` (the default —
|
|
292
|
+
* this method is unimplemented on the base/atscript providers) means "no
|
|
293
|
+
* narrowing", i.e. the credential authorizes with the user's full authority.
|
|
294
|
+
*
|
|
295
|
+
* `arbac-moost` is auth-agnostic, so the base class does NOT read the auth
|
|
296
|
+
* layer. A consumer wires it up by overriding this to read the credential's
|
|
297
|
+
* typed root fields and return their attenuation — typically
|
|
298
|
+
* `extractAttenuation(CredentialModel, useAuth().getAuthContext())`, which
|
|
299
|
+
* walks the model's `@arbac.attenuate.*`-annotated fields (from
|
|
300
|
+
* `@aooth/arbac-moost/atscript`). The restrict-only intersection happens in
|
|
301
|
+
* the engine regardless, so an app cannot get the safety wrong here.
|
|
302
|
+
*/
|
|
303
|
+
getAttenuation?(): AoothArbacClaims | undefined | Promise<AoothArbacClaims | undefined>;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* DI token form of {@link ArbacUserProvider}. The class itself is `abstract`,
|
|
307
|
+
* so it does not satisfy moost's `TClassConstructor` (`new (...) => T`)
|
|
308
|
+
* structurally; this re-typed alias is the supported way to pass the base
|
|
309
|
+
* class to `cc.instantiate(...)`, `createReplaceRegistry(...)`, `@Replace(...)`,
|
|
310
|
+
* etc. Runtime resolves to the concrete subclass registered via DI.
|
|
311
|
+
*/
|
|
312
|
+
declare const ArbacUserProviderToken: TClassConstructor<ArbacUserProvider>;
|
|
313
|
+
//#endregion
|
|
314
|
+
export { ArbacDbScope as a, enforceControlsPolicy as c, NavRelationKey as d, NavTarget as f, conjoinArbacDbScopes as i, extractUsedControlValues as l, ProjectionOf as m, ArbacUserProviderToken as n, AsArbacDbController as o, OwnFieldKey as p, AoothArbacClaims as r, applyAllowedFieldsAndSet as s, ArbacUserProvider as t, ControlsOf as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/arbac-moost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Moost RBAC integration for aoothjs (migrated from @moostjs/arbac)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abac",
|
|
@@ -42,8 +42,14 @@
|
|
|
42
42
|
"types": "./dist/atscript/index.d.mts",
|
|
43
43
|
"import": "./dist/atscript/index.mjs"
|
|
44
44
|
},
|
|
45
|
-
"./atscript/models":
|
|
46
|
-
|
|
45
|
+
"./atscript/models": {
|
|
46
|
+
"types": "./src/atscript/models/user.as.d.ts",
|
|
47
|
+
"default": "./src/atscript/models/user.as"
|
|
48
|
+
},
|
|
49
|
+
"./atscript/models.as": {
|
|
50
|
+
"types": "./src/atscript/models/user.as.d.ts",
|
|
51
|
+
"default": "./src/atscript/models/user.as"
|
|
52
|
+
},
|
|
47
53
|
"./plugin": {
|
|
48
54
|
"types": "./dist/plugin.d.mts",
|
|
49
55
|
"import": "./dist/plugin.mjs"
|
|
@@ -54,31 +60,31 @@
|
|
|
54
60
|
"access": "public"
|
|
55
61
|
},
|
|
56
62
|
"dependencies": {
|
|
57
|
-
"@aooth/arbac": "0.1.
|
|
58
|
-
"@aooth/arbac-core": "0.1.
|
|
59
|
-
"@aooth/user": "0.1.
|
|
63
|
+
"@aooth/arbac": "0.1.8",
|
|
64
|
+
"@aooth/arbac-core": "0.1.8",
|
|
65
|
+
"@aooth/user": "0.1.8"
|
|
60
66
|
},
|
|
61
67
|
"devDependencies": {
|
|
62
|
-
"@atscript/core": "^0.1.
|
|
63
|
-
"@atscript/db": "^0.1.
|
|
64
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
65
|
-
"@atscript/db-sqlite": "^0.1.
|
|
66
|
-
"@atscript/moost-db": "^0.1.
|
|
67
|
-
"@atscript/typescript": "^0.1.
|
|
68
|
-
"@moostjs/event-http": "^0.6.
|
|
68
|
+
"@atscript/core": "^0.1.69",
|
|
69
|
+
"@atscript/db": "^0.1.96",
|
|
70
|
+
"@atscript/db-sql-tools": "^0.1.96",
|
|
71
|
+
"@atscript/db-sqlite": "^0.1.96",
|
|
72
|
+
"@atscript/moost-db": "^0.1.96",
|
|
73
|
+
"@atscript/typescript": "^0.1.69",
|
|
74
|
+
"@moostjs/event-http": "^0.6.23",
|
|
69
75
|
"@types/better-sqlite3": "^7.6.13",
|
|
70
76
|
"@uniqu/core": "^0.1.6",
|
|
71
77
|
"better-sqlite3": "^12.6.2",
|
|
72
|
-
"unplugin-atscript": "^0.1.
|
|
78
|
+
"unplugin-atscript": "^0.1.69"
|
|
73
79
|
},
|
|
74
80
|
"peerDependencies": {
|
|
75
|
-
"@atscript/db": "
|
|
76
|
-
"@atscript/moost-db": "
|
|
77
|
-
"@atscript/typescript": "
|
|
78
|
-
"@moostjs/event-http": "
|
|
79
|
-
"@wooksjs/event-core": "
|
|
80
|
-
"@wooksjs/event-http": "
|
|
81
|
-
"moost": "
|
|
81
|
+
"@atscript/db": "^0.1.96",
|
|
82
|
+
"@atscript/moost-db": "^0.1.96",
|
|
83
|
+
"@atscript/typescript": "^0.1.69",
|
|
84
|
+
"@moostjs/event-http": "^0.6.23",
|
|
85
|
+
"@wooksjs/event-core": "^0.7.17",
|
|
86
|
+
"@wooksjs/event-http": "^0.7.17",
|
|
87
|
+
"moost": "^0.6.23"
|
|
82
88
|
},
|
|
83
89
|
"peerDependenciesMeta": {
|
|
84
90
|
"@atscript/db": {
|
package/dist/user-ChbLPYhG.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { defineAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
|
|
2
|
-
import "@aooth/user/atscript-db/model.as";
|
|
3
|
-
//#region src/atscript/models/user.as
|
|
4
|
-
var AoothArbacUserCredentials = class {
|
|
5
|
-
static __is_atscript_annotated_type = true;
|
|
6
|
-
static type = {};
|
|
7
|
-
static metadata = /* @__PURE__ */ new Map();
|
|
8
|
-
static id = "AoothArbacUserCredentials";
|
|
9
|
-
static toJsonSchema() {
|
|
10
|
-
throwFeatureDisabled("JSON Schema", "jsonSchema", "emit.jsonSchema");
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
defineAnnotatedType("object", AoothArbacUserCredentials).prop("username", defineAnnotatedType().designType("string").tags("string").annotate("db.index.unique", "username_idx", true).$type).prop("version", defineAnnotatedType().designType("number").tags("int", "number").annotate("db.column.version", true).annotate("expect.int", true).$type).prop("password", defineAnnotatedType("object").prop("hash", defineAnnotatedType().designType("string").tags("string").$type).prop("history", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).$type).prop("lastChanged", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("isInitial", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("account", defineAnnotatedType("object").prop("active", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("locked", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("lockReason", defineAnnotatedType().designType("string").tags("string").$type).prop("lockEnds", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("failedLoginAttempts", defineAnnotatedType().designType("number").tags("number").$type).prop("lastLogin", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("pendingInvitation", defineAnnotatedType().designType("boolean").tags("boolean").optional().$type).annotate("db.patch.strategy", "merge").$type).prop("mfa", defineAnnotatedType("object").prop("methods", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("name", defineAnnotatedType().designType("string").tags("string").$type).prop("confirmed", defineAnnotatedType().designType("boolean").tags("boolean").$type).prop("value", defineAnnotatedType().designType("string").tags("string").$type).prop("lastUsedWindow", defineAnnotatedType().designType("number").tags("int", "number").annotate("expect.int", true).optional().$type).$type).$type).prop("defaultMethod", defineAnnotatedType().designType("string").tags("string").$type).prop("autoSend", defineAnnotatedType().designType("boolean").tags("boolean").$type).annotate("db.patch.strategy", "merge").$type).prop("trustedDevices", defineAnnotatedType("array").of(defineAnnotatedType("object").prop("token", defineAnnotatedType().designType("string").tags("string").$type).prop("ip", defineAnnotatedType().designType("string").tags("string").optional().$type).prop("issuedAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("expiresAt", defineAnnotatedType().designType("number").tags("timestamp", "number").annotate("expect.int", true).$type).prop("name", defineAnnotatedType().designType("string").tags("string").optional().$type).$type).annotate("db.patch.strategy", "merge").optional().$type).prop("backupCodes", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).optional().$type).prop("roles", defineAnnotatedType("array").of(defineAnnotatedType().designType("string").tags("string").$type).annotate("arbac.role", true).$type);
|
|
14
|
-
//#endregion
|
|
15
|
-
export { AoothArbacUserCredentials as t };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { TClassConstructor } from "moost";
|
|
2
|
-
|
|
3
|
-
//#region src/user.provider.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Abstract base class for providing user data required for ARBAC evaluations.
|
|
6
|
-
*
|
|
7
|
-
* Consumers must extend this class and implement the three abstract methods
|
|
8
|
-
* to plug their own authentication/identity layer into the ARBAC authorize
|
|
9
|
-
* interceptor. The subclass must re-apply `@Injectable()` (moost does not
|
|
10
|
-
* inherit decorator metadata) and be registered via Moost's
|
|
11
|
-
* `setReplaceRegistry` (or `@Replace`) so DI resolves the concrete provider.
|
|
12
|
-
*
|
|
13
|
-
* @template TUserAttrs - The type representing user attributes relevant to access control.
|
|
14
|
-
*/
|
|
15
|
-
declare abstract class ArbacUserProvider<TUserAttrs extends object = object> {
|
|
16
|
-
/**
|
|
17
|
-
* Retrieves the unique identifier of the current user.
|
|
18
|
-
*/
|
|
19
|
-
abstract getUserId(): string | Promise<string>;
|
|
20
|
-
/**
|
|
21
|
-
* Retrieves the roles assigned to a user based on their ID.
|
|
22
|
-
*
|
|
23
|
-
* @param id - The user ID.
|
|
24
|
-
*/
|
|
25
|
-
abstract getRoles(id: string): string[] | Promise<string[]>;
|
|
26
|
-
/**
|
|
27
|
-
* Retrieves the attributes associated with a user based on their ID.
|
|
28
|
-
*
|
|
29
|
-
* @param id - The user ID.
|
|
30
|
-
*/
|
|
31
|
-
abstract getAttrs(id: string): TUserAttrs | Promise<TUserAttrs>;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* DI token form of {@link ArbacUserProvider}. The class itself is `abstract`,
|
|
35
|
-
* so it does not satisfy moost's `TClassConstructor` (`new (...) => T`)
|
|
36
|
-
* structurally; this re-typed alias is the supported way to pass the base
|
|
37
|
-
* class to `cc.instantiate(...)`, `createReplaceRegistry(...)`, `@Replace(...)`,
|
|
38
|
-
* etc. Runtime resolves to the concrete subclass registered via DI.
|
|
39
|
-
*/
|
|
40
|
-
declare const ArbacUserProviderToken: TClassConstructor<ArbacUserProvider>;
|
|
41
|
-
//#endregion
|
|
42
|
-
export { ArbacUserProviderToken as n, ArbacUserProvider as t };
|