@atlasauth/emulator-core 0.2.0 → 0.3.0
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/custom-html.d.ts +57 -0
- package/dist/custom-html.js +491 -0
- package/dist/custom-html.js.map +1 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +4 -0
- package/dist/engine.js.map +1 -1
- package/dist/error-catalog.js +8 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/event-gate.js +60 -2
- package/dist/event-gate.js.map +1 -1
- package/dist/generic.js +5 -1
- package/dist/generic.js.map +1 -1
- package/dist/handlers/access-signals.d.ts +48 -0
- package/dist/handlers/access-signals.js +76 -0
- package/dist/handlers/access-signals.js.map +1 -0
- package/dist/handlers/approvals.js +54 -1
- package/dist/handlers/approvals.js.map +1 -1
- package/dist/handlers/authz-data.d.ts +2 -0
- package/dist/handlers/authz-data.js +627 -0
- package/dist/handlers/authz-data.js.map +1 -0
- package/dist/handlers/authz-layers.d.ts +229 -0
- package/dist/handlers/authz-layers.js +786 -0
- package/dist/handlers/authz-layers.js.map +1 -0
- package/dist/handlers/authz-state.d.ts +80 -0
- package/dist/handlers/authz-state.js +175 -0
- package/dist/handlers/authz-state.js.map +1 -0
- package/dist/handlers/authz.js +240 -177
- package/dist/handlers/authz.js.map +1 -1
- package/dist/handlers/billing.d.ts +99 -1
- package/dist/handlers/billing.js +556 -77
- package/dist/handlers/billing.js.map +1 -1
- package/dist/handlers/cedar-ext.d.ts +35 -0
- package/dist/handlers/cedar-ext.js +174 -0
- package/dist/handlers/cedar-ext.js.map +1 -0
- package/dist/handlers/cedar.d.ts +146 -0
- package/dist/handlers/cedar.js +689 -0
- package/dist/handlers/cedar.js.map +1 -0
- package/dist/handlers/connections.d.ts +4 -1
- package/dist/handlers/connections.js +267 -38
- package/dist/handlers/connections.js.map +1 -1
- package/dist/handlers/control.js +6 -1
- package/dist/handlers/control.js.map +1 -1
- package/dist/handlers/device.js +18 -1
- package/dist/handlers/device.js.map +1 -1
- package/dist/handlers/emu3-misc.d.ts +38 -0
- package/dist/handlers/emu3-misc.js +1056 -0
- package/dist/handlers/emu3-misc.js.map +1 -0
- package/dist/handlers/emu4-actions-access.d.ts +46 -0
- package/dist/handlers/emu4-actions-access.js +107 -0
- package/dist/handlers/emu4-actions-access.js.map +1 -0
- package/dist/handlers/emu4-actions-catalog-extra.d.ts +3 -0
- package/dist/handlers/emu4-actions-catalog-extra.js +204 -0
- package/dist/handlers/emu4-actions-catalog-extra.js.map +1 -0
- package/dist/handlers/emu4-actions-catalog.d.ts +3 -0
- package/dist/handlers/emu4-actions-catalog.js +143 -0
- package/dist/handlers/emu4-actions-catalog.js.map +1 -0
- package/dist/handlers/emu4-actions-def.d.ts +50 -0
- package/dist/handlers/emu4-actions-def.js +250 -0
- package/dist/handlers/emu4-actions-def.js.map +1 -0
- package/dist/handlers/emu4-actions-sandbox.d.ts +36 -0
- package/dist/handlers/emu4-actions-sandbox.js +318 -0
- package/dist/handlers/emu4-actions-sandbox.js.map +1 -0
- package/dist/handlers/emu4-actions-typegen.d.ts +5 -0
- package/dist/handlers/emu4-actions-typegen.js +73 -0
- package/dist/handlers/emu4-actions-typegen.js.map +1 -0
- package/dist/handlers/emu4-actions.d.ts +2 -0
- package/dist/handlers/emu4-actions.js +506 -0
- package/dist/handlers/emu4-actions.js.map +1 -0
- package/dist/handlers/emu4-authz-trust.d.ts +40 -0
- package/dist/handlers/emu4-authz-trust.js +488 -0
- package/dist/handlers/emu4-authz-trust.js.map +1 -0
- package/dist/handlers/emu4-authz.d.ts +2 -0
- package/dist/handlers/emu4-authz.js +752 -0
- package/dist/handlers/emu4-authz.js.map +1 -0
- package/dist/handlers/emu4-billing-core.d.ts +23 -0
- package/dist/handlers/emu4-billing-core.js +22 -0
- package/dist/handlers/emu4-billing-core.js.map +1 -0
- package/dist/handlers/emu4-billing-x.d.ts +2 -0
- package/dist/handlers/emu4-billing-x.js +489 -0
- package/dist/handlers/emu4-billing-x.js.map +1 -0
- package/dist/handlers/emu4-billing.d.ts +14 -0
- package/dist/handlers/emu4-billing.js +846 -0
- package/dist/handlers/emu4-billing.js.map +1 -0
- package/dist/handlers/emu4-clones.d.ts +2 -0
- package/dist/handlers/emu4-clones.js +322 -0
- package/dist/handlers/emu4-clones.js.map +1 -0
- package/dist/handlers/emu4-compliance.d.ts +8 -0
- package/dist/handlers/emu4-compliance.js +39 -0
- package/dist/handlers/emu4-compliance.js.map +1 -0
- package/dist/handlers/emu4-config.d.ts +14 -0
- package/dist/handlers/emu4-config.js +148 -0
- package/dist/handlers/emu4-config.js.map +1 -0
- package/dist/handlers/emu4-cookies.d.ts +8 -0
- package/dist/handlers/emu4-cookies.js +248 -0
- package/dist/handlers/emu4-cookies.js.map +1 -0
- package/dist/handlers/emu4-erasure.d.ts +2 -0
- package/dist/handlers/emu4-erasure.js +587 -0
- package/dist/handlers/emu4-erasure.js.map +1 -0
- package/dist/handlers/emu4-federation.d.ts +8 -0
- package/dist/handlers/emu4-federation.js +18 -0
- package/dist/handlers/emu4-federation.js.map +1 -0
- package/dist/handlers/emu4-journey-math.d.ts +42 -0
- package/dist/handlers/emu4-journey-math.js +99 -0
- package/dist/handlers/emu4-journey-math.js.map +1 -0
- package/dist/handlers/emu4-messaging.d.ts +21 -0
- package/dist/handlers/emu4-messaging.js +595 -0
- package/dist/handlers/emu4-messaging.js.map +1 -0
- package/dist/handlers/emu4-minors.d.ts +7 -0
- package/dist/handlers/emu4-minors.js +408 -0
- package/dist/handlers/emu4-minors.js.map +1 -0
- package/dist/handlers/emu4-oauth-audit.d.ts +5 -0
- package/dist/handlers/emu4-oauth-audit.js +17 -0
- package/dist/handlers/emu4-oauth-audit.js.map +1 -0
- package/dist/handlers/emu4-oauth-auth.d.ts +83 -0
- package/dist/handlers/emu4-oauth-auth.js +363 -0
- package/dist/handlers/emu4-oauth-auth.js.map +1 -0
- package/dist/handlers/emu4-oauth-delivery.d.ts +68 -0
- package/dist/handlers/emu4-oauth-delivery.js +207 -0
- package/dist/handlers/emu4-oauth-delivery.js.map +1 -0
- package/dist/handlers/emu4-oauth-keys.d.ts +11 -0
- package/dist/handlers/emu4-oauth-keys.js +268 -0
- package/dist/handlers/emu4-oauth-keys.js.map +1 -0
- package/dist/handlers/emu4-oauth-pub.d.ts +36 -0
- package/dist/handlers/emu4-oauth-pub.js +420 -0
- package/dist/handlers/emu4-oauth-pub.js.map +1 -0
- package/dist/handlers/emu4-oauth.d.ts +10 -0
- package/dist/handlers/emu4-oauth.js +23 -0
- package/dist/handlers/emu4-oauth.js.map +1 -0
- package/dist/handlers/emu4-policies.d.ts +2 -0
- package/dist/handlers/emu4-policies.js +408 -0
- package/dist/handlers/emu4-policies.js.map +1 -0
- package/dist/handlers/emu4-region.d.ts +2 -0
- package/dist/handlers/emu4-region.js +189 -0
- package/dist/handlers/emu4-region.js.map +1 -0
- package/dist/handlers/emu4-saml.d.ts +12 -0
- package/dist/handlers/emu4-saml.js +532 -0
- package/dist/handlers/emu4-saml.js.map +1 -0
- package/dist/handlers/emu4-shared.d.ts +44 -0
- package/dist/handlers/emu4-shared.js +90 -0
- package/dist/handlers/emu4-shared.js.map +1 -0
- package/dist/handlers/emu4-snapshots.d.ts +38 -0
- package/dist/handlers/emu4-snapshots.js +231 -0
- package/dist/handlers/emu4-snapshots.js.map +1 -0
- package/dist/handlers/emu4-sns.d.ts +9 -0
- package/dist/handlers/emu4-sns.js +21 -0
- package/dist/handlers/emu4-sns.js.map +1 -0
- package/dist/handlers/emu4-trust-binding.d.ts +27 -0
- package/dist/handlers/emu4-trust-binding.js +344 -0
- package/dist/handlers/emu4-trust-binding.js.map +1 -0
- package/dist/handlers/emu4-trust.d.ts +35 -0
- package/dist/handlers/emu4-trust.js +543 -0
- package/dist/handlers/emu4-trust.js.map +1 -0
- package/dist/handlers/fga-engine.d.ts +78 -0
- package/dist/handlers/fga-engine.js +663 -0
- package/dist/handlers/fga-engine.js.map +1 -0
- package/dist/handlers/hooks.js +13 -0
- package/dist/handlers/hooks.js.map +1 -1
- package/dist/handlers/index.js +14 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/machines.js +6 -0
- package/dist/handlers/machines.js.map +1 -1
- package/dist/handlers/messaging.d.ts +3 -1
- package/dist/handlers/messaging.js +455 -48
- package/dist/handlers/messaging.js.map +1 -1
- package/dist/handlers/oauth-adv.d.ts +191 -0
- package/dist/handlers/oauth-adv.js +1305 -0
- package/dist/handlers/oauth-adv.js.map +1 -0
- package/dist/handlers/oauth-flows.d.ts +25 -1
- package/dist/handlers/oauth-flows.js +111 -50
- package/dist/handlers/oauth-flows.js.map +1 -1
- package/dist/handlers/oauth.d.ts +24 -3
- package/dist/handlers/oauth.js +245 -60
- package/dist/handlers/oauth.js.map +1 -1
- package/dist/handlers/orgs.js +6 -0
- package/dist/handlers/orgs.js.map +1 -1
- package/dist/handlers/platform.d.ts +5 -0
- package/dist/handlers/platform.js +33 -15
- package/dist/handlers/platform.js.map +1 -1
- package/dist/handlers/scim.d.ts +2 -0
- package/dist/handlers/scim.js +198 -11
- package/dist/handlers/scim.js.map +1 -1
- package/dist/handlers/sessions.d.ts +2 -0
- package/dist/handlers/sessions.js +24 -16
- package/dist/handlers/sessions.js.map +1 -1
- package/dist/handlers/users.js +3 -0
- package/dist/handlers/users.js.map +1 -1
- package/dist/jwe.d.ts +18 -0
- package/dist/jwe.js +101 -0
- package/dist/jwe.js.map +1 -0
- package/dist/jws.js +2 -0
- package/dist/jws.js.map +1 -1
- package/dist/messaging-callbacks.d.ts +100 -0
- package/dist/messaging-callbacks.js +767 -0
- package/dist/messaging-callbacks.js.map +1 -0
- package/dist/openapi.js +41 -1
- package/dist/openapi.js.map +1 -1
- package/dist/parity-emu3-authz.d.ts +3 -0
- package/dist/parity-emu3-authz.js +61 -0
- package/dist/parity-emu3-authz.js.map +1 -0
- package/dist/parity-emu3-billing.d.ts +3 -0
- package/dist/parity-emu3-billing.js +25 -0
- package/dist/parity-emu3-billing.js.map +1 -0
- package/dist/parity-emu3-messaging.d.ts +3 -0
- package/dist/parity-emu3-messaging.js +27 -0
- package/dist/parity-emu3-messaging.js.map +1 -0
- package/dist/parity-emu3-misc.d.ts +2 -0
- package/dist/parity-emu3-misc.js +63 -0
- package/dist/parity-emu3-misc.js.map +1 -0
- package/dist/parity-emu3-oauth.d.ts +2 -0
- package/dist/parity-emu3-oauth.js +39 -0
- package/dist/parity-emu3-oauth.js.map +1 -0
- package/dist/parity-emu4-actions.d.ts +2 -0
- package/dist/parity-emu4-actions.js +38 -0
- package/dist/parity-emu4-actions.js.map +1 -0
- package/dist/parity-emu4-authz.d.ts +3 -0
- package/dist/parity-emu4-authz.js +68 -0
- package/dist/parity-emu4-authz.js.map +1 -0
- package/dist/parity-emu4-billing.d.ts +6 -0
- package/dist/parity-emu4-billing.js +44 -0
- package/dist/parity-emu4-billing.js.map +1 -0
- package/dist/parity-emu4-compliance.d.ts +2 -0
- package/dist/parity-emu4-compliance.js +92 -0
- package/dist/parity-emu4-compliance.js.map +1 -0
- package/dist/parity-emu4-federation.d.ts +2 -0
- package/dist/parity-emu4-federation.js +33 -0
- package/dist/parity-emu4-federation.js.map +1 -0
- package/dist/parity-emu4-messaging.d.ts +3 -0
- package/dist/parity-emu4-messaging.js +42 -0
- package/dist/parity-emu4-messaging.js.map +1 -0
- package/dist/parity-emu4-oauth.d.ts +2 -0
- package/dist/parity-emu4-oauth.js +55 -0
- package/dist/parity-emu4-oauth.js.map +1 -0
- package/dist/parity-emu4-trust.d.ts +2 -0
- package/dist/parity-emu4-trust.js +41 -0
- package/dist/parity-emu4-trust.js.map +1 -0
- package/dist/parity-routes.js +19 -1
- package/dist/parity-routes.js.map +1 -1
- package/dist/sns-cert.d.ts +9 -0
- package/dist/sns-cert.js +101 -0
- package/dist/sns-cert.js.map +1 -0
- package/dist/tokens.d.ts +69 -4
- package/dist/tokens.js +164 -38
- package/dist/tokens.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/**
|
|
4
|
+
* The relationship-model engine behind the FGA routes: an OpenFGA-style authorization model (JSON `type_definitions` or DSL
|
|
5
|
+
* text) with the six rewrite operators (direct, computed, tuple-to-userset, union, intersection, exclusion), tuple
|
|
6
|
+
* conditions over a small CEL subset, and check / list-objects / list-users / expand over a set of tuples.
|
|
7
|
+
*
|
|
8
|
+
* shortcut: conditions support comparisons, boolean and arithmetic operators, `in`, field and index access, list literals
|
|
9
|
+
* and the string functions startsWith, endsWith, contains, matches, size; no ipaddress, duration or map-typed parameters.
|
|
10
|
+
* Upgrade by running the real engine if a model depends on those.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.Resolver = exports.TupleError = exports.ModelError = void 0;
|
|
14
|
+
exports.dslToModel = dslToModel;
|
|
15
|
+
exports.compileModel = compileModel;
|
|
16
|
+
exports.compileStored = compileStored;
|
|
17
|
+
exports.parseUser = parseUser;
|
|
18
|
+
exports.parseObject = parseObject;
|
|
19
|
+
exports.validateTuple = validateTuple;
|
|
20
|
+
class ModelError extends Error {
|
|
21
|
+
errors;
|
|
22
|
+
constructor(errors) {
|
|
23
|
+
super(errors.join(' '));
|
|
24
|
+
this.errors = errors;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.ModelError = ModelError;
|
|
28
|
+
class TupleError extends Error {
|
|
29
|
+
}
|
|
30
|
+
exports.TupleError = TupleError;
|
|
31
|
+
// ── DSL ──
|
|
32
|
+
const IDENT = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
33
|
+
const isIdent = (s) => !!s && IDENT.test(s);
|
|
34
|
+
function parseRefs(body) {
|
|
35
|
+
return body.split(',').map((raw) => {
|
|
36
|
+
const m = /^\s*([A-Za-z0-9_-]+)(?::(\*)|#([A-Za-z0-9_-]+))?(?:\s+with\s+([A-Za-z0-9_-]+))?\s*$/.exec(raw);
|
|
37
|
+
if (!m)
|
|
38
|
+
throw new ModelError([`Cannot read the type restriction "${raw.trim()}".`]);
|
|
39
|
+
return { type: m[1], ...(m[3] ? { relation: m[3] } : {}), ...(m[2] ? { wildcard: {} } : {}), ...(m[4] ? { condition: m[4] } : {}) };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Parse one `define` expression into a rewrite and the direct type restrictions it names. */
|
|
43
|
+
function parseExpr(src, refs) {
|
|
44
|
+
const toks = src.match(/\[[^\]]*\]|\(|\)|[A-Za-z0-9_-]+|\S/g) ?? [];
|
|
45
|
+
let i = 0;
|
|
46
|
+
const primary = () => {
|
|
47
|
+
const t = toks[i++];
|
|
48
|
+
if (t === undefined)
|
|
49
|
+
throw new ModelError(['The relation definition ends too early.']);
|
|
50
|
+
if (t === '(') {
|
|
51
|
+
const e = expr();
|
|
52
|
+
if (toks[i++] !== ')')
|
|
53
|
+
throw new ModelError(['Missing ")" in a relation definition.']);
|
|
54
|
+
return e;
|
|
55
|
+
}
|
|
56
|
+
if (t.startsWith('[')) {
|
|
57
|
+
refs.push(...parseRefs(t.slice(1, -1)));
|
|
58
|
+
return { this: {} };
|
|
59
|
+
}
|
|
60
|
+
if (!isIdent(t) || t === 'or' || t === 'and' || t === 'but')
|
|
61
|
+
throw new ModelError([`Unexpected "${t}" in a relation definition.`]);
|
|
62
|
+
if (toks[i] === 'from') {
|
|
63
|
+
const ts = toks[i + 1];
|
|
64
|
+
if (!isIdent(ts))
|
|
65
|
+
throw new ModelError([`"${t} from" needs a relation name.`]);
|
|
66
|
+
i += 2;
|
|
67
|
+
return { tupleToUserset: { tupleset: { relation: ts }, computedUserset: { relation: t } } };
|
|
68
|
+
}
|
|
69
|
+
return { computedUserset: { relation: t } };
|
|
70
|
+
};
|
|
71
|
+
const expr = () => {
|
|
72
|
+
const first = primary();
|
|
73
|
+
const op = toks[i];
|
|
74
|
+
if (op === 'or' || op === 'and') {
|
|
75
|
+
const child = [first];
|
|
76
|
+
while (toks[i] === op) {
|
|
77
|
+
i++;
|
|
78
|
+
child.push(primary());
|
|
79
|
+
}
|
|
80
|
+
return { [op === 'or' ? 'union' : 'intersection']: { child } };
|
|
81
|
+
}
|
|
82
|
+
if (op === 'but') {
|
|
83
|
+
if (toks[i + 1] !== 'not')
|
|
84
|
+
throw new ModelError(['Expected "but not".']);
|
|
85
|
+
i += 2;
|
|
86
|
+
return { difference: { base: first, subtract: primary() } };
|
|
87
|
+
}
|
|
88
|
+
return first;
|
|
89
|
+
};
|
|
90
|
+
const out = expr();
|
|
91
|
+
if (i < toks.length && toks[i] !== ')')
|
|
92
|
+
throw new ModelError([`Unexpected "${toks[i]}" in a relation definition (mix "or", "and" and "but not" with parentheses).`]);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** OpenFGA DSL text to the JSON model. */
|
|
96
|
+
function dslToModel(dsl) {
|
|
97
|
+
const defs = [];
|
|
98
|
+
const conditions = {};
|
|
99
|
+
let schema = '1.1';
|
|
100
|
+
const lines = dsl.split(/\r?\n/);
|
|
101
|
+
let cur = null;
|
|
102
|
+
for (let n = 0; n < lines.length; n++) {
|
|
103
|
+
const line = lines[n].replace(/\s+#.*$/, '').replace(/^\s*#.*$/, '').trim();
|
|
104
|
+
if (!line || line === 'model' || line === 'relations')
|
|
105
|
+
continue;
|
|
106
|
+
let m;
|
|
107
|
+
if ((m = /^schema\s+(\S+)$/.exec(line)))
|
|
108
|
+
schema = m[1];
|
|
109
|
+
else if ((m = /^type\s+([A-Za-z0-9_-]+)$/.exec(line)))
|
|
110
|
+
defs.push((cur = { type: m[1], relations: {}, metadata: { relations: {} } }));
|
|
111
|
+
else if ((m = /^condition\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*\{(.*)$/.exec(line))) {
|
|
112
|
+
let body = m[3];
|
|
113
|
+
while (!/\}\s*$/.test(body) && n + 1 < lines.length)
|
|
114
|
+
body += `\n${lines[++n]}`;
|
|
115
|
+
const parameters = {};
|
|
116
|
+
for (const p of m[2].split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
117
|
+
const pm = /^([A-Za-z0-9_]+)\s*:\s*(.+)$/.exec(p);
|
|
118
|
+
if (!pm)
|
|
119
|
+
throw new ModelError([`Cannot read the condition parameter "${p}".`]);
|
|
120
|
+
parameters[pm[1]] = { type_name: `TYPE_NAME_${pm[2].trim().replace(/<.*$/, '').toUpperCase()}` };
|
|
121
|
+
}
|
|
122
|
+
conditions[m[1]] = { name: m[1], expression: body.replace(/\}\s*$/, '').trim(), parameters };
|
|
123
|
+
}
|
|
124
|
+
else if ((m = /^define\s+([A-Za-z0-9_-]+)\s*:\s*(.+)$/.exec(line))) {
|
|
125
|
+
if (!cur)
|
|
126
|
+
throw new ModelError([`"define ${m[1]}" is outside a type.`]);
|
|
127
|
+
const refs = [];
|
|
128
|
+
cur.relations[m[1]] = parseExpr(m[2], refs);
|
|
129
|
+
cur.metadata.relations[m[1]] = { directly_related_user_types: refs };
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
throw new ModelError([`Cannot read the model line "${line}".`]);
|
|
133
|
+
}
|
|
134
|
+
return { schema_version: schema, type_definitions: defs, ...(Object.keys(conditions).length ? { conditions } : {}) };
|
|
135
|
+
}
|
|
136
|
+
// ── validation ──
|
|
137
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
138
|
+
function checkUs(node, own, all, path, errors) {
|
|
139
|
+
if (!isObj(node) || Object.keys(node).length !== 1)
|
|
140
|
+
return void errors.push(`${path}: a userset must be an object with exactly one operator.`);
|
|
141
|
+
const op = Object.keys(node)[0];
|
|
142
|
+
const v = node[op];
|
|
143
|
+
if (op === 'this')
|
|
144
|
+
return;
|
|
145
|
+
if (op === 'computedUserset') {
|
|
146
|
+
if (!isObj(v) || typeof v.relation !== 'string')
|
|
147
|
+
errors.push(`${path}.computedUserset: requires a string relation.`);
|
|
148
|
+
else if (!own.has(v.relation))
|
|
149
|
+
errors.push(`${path}.computedUserset: relation '${v.relation}' is not defined on this type.`);
|
|
150
|
+
}
|
|
151
|
+
else if (op === 'tupleToUserset') {
|
|
152
|
+
const ts = v?.tupleset?.relation;
|
|
153
|
+
const cu = v?.computedUserset?.relation;
|
|
154
|
+
if (typeof ts !== 'string' || typeof cu !== 'string')
|
|
155
|
+
errors.push(`${path}.tupleToUserset: requires tupleset and computedUserset relations.`);
|
|
156
|
+
else {
|
|
157
|
+
if (!own.has(ts))
|
|
158
|
+
errors.push(`${path}.tupleToUserset.tupleset: relation '${ts}' is not defined on this type.`);
|
|
159
|
+
if (!all.has(cu))
|
|
160
|
+
errors.push(`${path}.tupleToUserset.computedUserset: relation '${cu}' is not defined on any type.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (op === 'union' || op === 'intersection') {
|
|
164
|
+
if (!isObj(v) || !Array.isArray(v.child) || !v.child.length)
|
|
165
|
+
errors.push(`${path}.${op}: requires a non-empty child array.`);
|
|
166
|
+
else
|
|
167
|
+
v.child.forEach((c, i) => checkUs(c, own, all, `${path}.${op}.child[${i}]`, errors));
|
|
168
|
+
}
|
|
169
|
+
else if (op === 'difference') {
|
|
170
|
+
if (!isObj(v) || !('base' in v) || !('subtract' in v))
|
|
171
|
+
errors.push(`${path}.difference: requires base and subtract.`);
|
|
172
|
+
else
|
|
173
|
+
(checkUs(v.base, own, all, `${path}.difference.base`, errors), checkUs(v.subtract, own, all, `${path}.difference.subtract`, errors));
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
errors.push(`${path}: unknown userset operator '${op}'.`);
|
|
177
|
+
}
|
|
178
|
+
/** Validate and compile a JSON model. Throws ModelError with every problem found. */
|
|
179
|
+
function compileModel(model) {
|
|
180
|
+
const errors = [];
|
|
181
|
+
if (!isObj(model) || !Array.isArray(model.type_definitions) || !model.type_definitions.length)
|
|
182
|
+
throw new ModelError(['type_definitions must be a non-empty array.']);
|
|
183
|
+
const defs = model.type_definitions;
|
|
184
|
+
const typeNames = new Set();
|
|
185
|
+
for (const d of defs) {
|
|
186
|
+
if (!isObj(d) || typeof d.type !== 'string' || !isIdent(d.type))
|
|
187
|
+
errors.push('Every type definition needs a type name.');
|
|
188
|
+
else if (typeNames.has(d.type))
|
|
189
|
+
errors.push(`Type '${d.type}' is defined twice.`);
|
|
190
|
+
else
|
|
191
|
+
typeNames.add(d.type);
|
|
192
|
+
}
|
|
193
|
+
const allRel = new Set(defs.flatMap((d) => Object.keys(isObj(d?.relations) ? d.relations : {})));
|
|
194
|
+
const types = new Map();
|
|
195
|
+
const refs = new Map();
|
|
196
|
+
for (const d of defs) {
|
|
197
|
+
if (!isObj(d) || typeof d.type !== 'string')
|
|
198
|
+
continue;
|
|
199
|
+
const rels = isObj(d.relations) ? d.relations : {};
|
|
200
|
+
const own = new Set(Object.keys(rels));
|
|
201
|
+
types.set(d.type, new Map(Object.entries(rels)));
|
|
202
|
+
for (const [r, node] of Object.entries(rels))
|
|
203
|
+
checkUs(node, own, allRel, `${d.type}#${r}`, errors);
|
|
204
|
+
const meta = new Map();
|
|
205
|
+
for (const [r, m] of Object.entries(isObj(d.metadata?.relations) ? d.metadata.relations : {}))
|
|
206
|
+
meta.set(r, Array.isArray(m?.directly_related_user_types) ? m.directly_related_user_types : []);
|
|
207
|
+
refs.set(d.type, meta);
|
|
208
|
+
}
|
|
209
|
+
const conds = new Map();
|
|
210
|
+
for (const [name, c] of Object.entries(isObj(model.conditions) ? model.conditions : {})) {
|
|
211
|
+
try {
|
|
212
|
+
if (typeof c?.expression !== 'string')
|
|
213
|
+
throw new CelError('expression is required');
|
|
214
|
+
conds.set(name, { params: new Map(Object.entries(c.parameters ?? {}).map(([k, p]) => [k, String(p?.type_name ?? '')])), ast: parseCel(c.expression) });
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
errors.push(`condition '${name}': ${err instanceof Error ? err.message : 'invalid expression'}.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const [t, m] of refs) {
|
|
221
|
+
for (const [r, list] of m) {
|
|
222
|
+
for (const ref of list) {
|
|
223
|
+
if (!typeNames.has(ref.type))
|
|
224
|
+
errors.push(`${t}#${r}: unknown type '${ref.type}' in the type restriction.`);
|
|
225
|
+
else if (ref.relation && !types.get(ref.type)?.has(ref.relation))
|
|
226
|
+
errors.push(`${t}#${r}: relation '${ref.relation}' is not defined on type '${ref.type}'.`);
|
|
227
|
+
if (ref.condition && !conds.has(ref.condition))
|
|
228
|
+
errors.push(`${t}#${r}: condition '${ref.condition}' is not defined.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (errors.length)
|
|
233
|
+
throw new ModelError(errors);
|
|
234
|
+
return { types, refs, conds };
|
|
235
|
+
}
|
|
236
|
+
/** The compiled form of a stored model row's `model` value, or null for a model the strict engine does not govern (DSL text kept as-is). */
|
|
237
|
+
const compiledCache = new WeakMap();
|
|
238
|
+
function compileStored(model) {
|
|
239
|
+
if (!isObj(model) || !Array.isArray(model.type_definitions))
|
|
240
|
+
return null;
|
|
241
|
+
if (compiledCache.has(model))
|
|
242
|
+
return compiledCache.get(model);
|
|
243
|
+
let out = null;
|
|
244
|
+
try {
|
|
245
|
+
out = compileModel(model);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
out = null;
|
|
249
|
+
}
|
|
250
|
+
compiledCache.set(model, out);
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
function parseUser(s) {
|
|
254
|
+
const i = s.indexOf(':');
|
|
255
|
+
if (i <= 0 || i === s.length - 1)
|
|
256
|
+
throw new TupleError(`Malformed user '${s}': expected 'type:id', 'type:id#relation' or 'type:*'.`);
|
|
257
|
+
const rest = s.slice(i + 1);
|
|
258
|
+
const h = rest.indexOf('#');
|
|
259
|
+
if (h < 0)
|
|
260
|
+
return { type: s.slice(0, i), id: rest, rel: null, wild: rest === '*' };
|
|
261
|
+
if (h === 0 || h === rest.length - 1 || rest.startsWith('*'))
|
|
262
|
+
throw new TupleError(`Malformed userset '${s}'.`);
|
|
263
|
+
return { type: s.slice(0, i), id: rest.slice(0, h), rel: rest.slice(h + 1), wild: false };
|
|
264
|
+
}
|
|
265
|
+
function parseObject(s) {
|
|
266
|
+
const i = s.indexOf(':');
|
|
267
|
+
if (i <= 0 || i === s.length - 1 || s.includes('#'))
|
|
268
|
+
throw new TupleError(`Malformed object '${s}': expected 'type:id'.`);
|
|
269
|
+
return { type: s.slice(0, i), id: s.slice(i + 1) };
|
|
270
|
+
}
|
|
271
|
+
/** Write-time type validation: the same rules the real store applies against the latest model. Null when allowed. */
|
|
272
|
+
function validateTuple(m, t) {
|
|
273
|
+
const o = parseObject(t.object);
|
|
274
|
+
if (!m.types.has(o.type))
|
|
275
|
+
return `unknown object type '${o.type}'.`;
|
|
276
|
+
if (!m.types.get(o.type).has(t.relation))
|
|
277
|
+
return `relation '${t.relation}' is not defined on type '${o.type}'.`;
|
|
278
|
+
if (t.condition) {
|
|
279
|
+
const c = m.conds.get(t.condition.name);
|
|
280
|
+
if (!c)
|
|
281
|
+
return `condition '${t.condition.name}' is not defined in the authorization model.`;
|
|
282
|
+
for (const k of Object.keys(t.condition.context ?? {}))
|
|
283
|
+
if (!c.params.has(k))
|
|
284
|
+
return `condition '${t.condition.name}' has no parameter '${k}'.`;
|
|
285
|
+
}
|
|
286
|
+
const allowed = m.refs.get(o.type)?.get(t.relation);
|
|
287
|
+
if (!allowed?.length)
|
|
288
|
+
return null;
|
|
289
|
+
const u = parseUser(t.user);
|
|
290
|
+
const ok = allowed.some((r) => r.type === u.type && r.condition === t.condition?.name && (u.wild ? !!r.wildcard : u.rel !== null ? r.relation === u.rel : r.relation === undefined && r.wildcard === undefined));
|
|
291
|
+
return ok ? null : `user '${t.user}' is not an allowed type for '${o.type}#${t.relation}'.`;
|
|
292
|
+
}
|
|
293
|
+
// ── CEL subset ──
|
|
294
|
+
class CelError extends Error {
|
|
295
|
+
}
|
|
296
|
+
function celTokens(src) {
|
|
297
|
+
const out = [];
|
|
298
|
+
const re = /\s*(?:(\d+\.\d+|\d+)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')|([A-Za-z_][A-Za-z0-9_]*)|(==|!=|<=|>=|&&|\|\||[-+*/%<>!?:.,()[\]]))/y;
|
|
299
|
+
let pos = 0;
|
|
300
|
+
while (pos < src.length) {
|
|
301
|
+
re.lastIndex = pos;
|
|
302
|
+
const m = re.exec(src);
|
|
303
|
+
if (!m) {
|
|
304
|
+
if (/^\s*$/.test(src.slice(pos)))
|
|
305
|
+
break;
|
|
306
|
+
throw new CelError(`unexpected character at ${pos}`);
|
|
307
|
+
}
|
|
308
|
+
out.push(m[0].trim());
|
|
309
|
+
pos = re.lastIndex;
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
function parseCel(src) {
|
|
314
|
+
const t = celTokens(src);
|
|
315
|
+
let i = 0;
|
|
316
|
+
const eat = (v) => (t[i] === v ? (i++, true) : false);
|
|
317
|
+
const need = (v) => {
|
|
318
|
+
if (!eat(v))
|
|
319
|
+
throw new CelError(`expected "${v}"`);
|
|
320
|
+
};
|
|
321
|
+
const ternary = () => {
|
|
322
|
+
const c = or();
|
|
323
|
+
if (!eat('?'))
|
|
324
|
+
return c;
|
|
325
|
+
const a = ternary();
|
|
326
|
+
need(':');
|
|
327
|
+
return ['?', c, a, ternary()];
|
|
328
|
+
};
|
|
329
|
+
const or = () => {
|
|
330
|
+
let l = and();
|
|
331
|
+
while (eat('||'))
|
|
332
|
+
l = ['||', l, and()];
|
|
333
|
+
return l;
|
|
334
|
+
};
|
|
335
|
+
const and = () => {
|
|
336
|
+
let l = rel();
|
|
337
|
+
while (eat('&&'))
|
|
338
|
+
l = ['&&', l, rel()];
|
|
339
|
+
return l;
|
|
340
|
+
};
|
|
341
|
+
const rel = () => {
|
|
342
|
+
let l = add();
|
|
343
|
+
for (;;) {
|
|
344
|
+
const op = ['==', '!=', '<=', '>=', '<', '>', 'in'].find((o) => t[i] === o);
|
|
345
|
+
if (!op)
|
|
346
|
+
return l;
|
|
347
|
+
i++;
|
|
348
|
+
l = [op, l, add()];
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const add = () => {
|
|
352
|
+
let l = mul();
|
|
353
|
+
while (t[i] === '+' || t[i] === '-')
|
|
354
|
+
l = [t[i++], l, mul()];
|
|
355
|
+
return l;
|
|
356
|
+
};
|
|
357
|
+
const mul = () => {
|
|
358
|
+
let l = unary();
|
|
359
|
+
while (t[i] === '*' || t[i] === '/' || t[i] === '%')
|
|
360
|
+
l = [t[i++], l, unary()];
|
|
361
|
+
return l;
|
|
362
|
+
};
|
|
363
|
+
const unary = () => (eat('!') ? ['!', unary()] : eat('-') ? ['neg', unary()] : postfix());
|
|
364
|
+
const args = () => {
|
|
365
|
+
const a = [];
|
|
366
|
+
while (t[i] !== ')') {
|
|
367
|
+
a.push(ternary());
|
|
368
|
+
if (!eat(','))
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
need(')');
|
|
372
|
+
return a;
|
|
373
|
+
};
|
|
374
|
+
const postfix = () => {
|
|
375
|
+
let e = primary();
|
|
376
|
+
for (;;) {
|
|
377
|
+
if (eat('.')) {
|
|
378
|
+
const name = t[i++];
|
|
379
|
+
if (!name || !/^[A-Za-z_]/.test(name))
|
|
380
|
+
throw new CelError('expected a name after "."');
|
|
381
|
+
e = eat('(') ? ['call', name, e, ...args()] : ['field', e, name];
|
|
382
|
+
}
|
|
383
|
+
else if (eat('[')) {
|
|
384
|
+
const k = ternary();
|
|
385
|
+
need(']');
|
|
386
|
+
e = ['index', e, k];
|
|
387
|
+
}
|
|
388
|
+
else
|
|
389
|
+
return e;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
const primary = () => {
|
|
393
|
+
const x = t[i++];
|
|
394
|
+
if (x === undefined)
|
|
395
|
+
throw new CelError('the expression ends too early');
|
|
396
|
+
if (/^\d/.test(x))
|
|
397
|
+
return ['lit', Number(x)];
|
|
398
|
+
if (x[0] === '"' || x[0] === "'")
|
|
399
|
+
return ['lit', JSON.parse(x[0] === "'" ? `"${x.slice(1, -1).replace(/"/g, '\\"')}"` : x)];
|
|
400
|
+
if (x === '(') {
|
|
401
|
+
const e = ternary();
|
|
402
|
+
need(')');
|
|
403
|
+
return e;
|
|
404
|
+
}
|
|
405
|
+
if (x === '[') {
|
|
406
|
+
const items = [];
|
|
407
|
+
while (t[i] !== ']') {
|
|
408
|
+
items.push(ternary());
|
|
409
|
+
if (!eat(','))
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
need(']');
|
|
413
|
+
return ['list', ...items];
|
|
414
|
+
}
|
|
415
|
+
if (x === 'true' || x === 'false')
|
|
416
|
+
return ['lit', x === 'true'];
|
|
417
|
+
if (x === 'null')
|
|
418
|
+
return ['lit', null];
|
|
419
|
+
if (/^[A-Za-z_]/.test(x))
|
|
420
|
+
return eat('(') ? ['fn', x, ...args()] : ['var', x];
|
|
421
|
+
throw new CelError(`unexpected "${x}"`);
|
|
422
|
+
};
|
|
423
|
+
const out = ternary();
|
|
424
|
+
if (i < t.length)
|
|
425
|
+
throw new CelError(`unexpected "${t[i]}"`);
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
function evalCel(n, vars) {
|
|
429
|
+
const ev = (x) => evalCel(x, vars);
|
|
430
|
+
const need = (cond, msg) => {
|
|
431
|
+
if (!cond)
|
|
432
|
+
throw new CelError(msg);
|
|
433
|
+
};
|
|
434
|
+
switch (n[0]) {
|
|
435
|
+
case 'lit':
|
|
436
|
+
return n[1];
|
|
437
|
+
case 'var':
|
|
438
|
+
need(vars.has(n[1]), `parameter '${n[1]}' is missing`);
|
|
439
|
+
return vars.get(n[1]);
|
|
440
|
+
case 'list':
|
|
441
|
+
return n.slice(1).map(ev);
|
|
442
|
+
case '!':
|
|
443
|
+
return !ev(n[1]);
|
|
444
|
+
case 'neg':
|
|
445
|
+
return -ev(n[1]);
|
|
446
|
+
case '&&':
|
|
447
|
+
return ev(n[1]) && ev(n[2]);
|
|
448
|
+
case '||':
|
|
449
|
+
return ev(n[1]) || ev(n[2]);
|
|
450
|
+
case '?':
|
|
451
|
+
return ev(n[1]) ? ev(n[2]) : ev(n[3]);
|
|
452
|
+
case 'field': {
|
|
453
|
+
const o = ev(n[1]);
|
|
454
|
+
need(isObj(o) && n[2] in o, `no field '${n[2]}'`);
|
|
455
|
+
return o[n[2]];
|
|
456
|
+
}
|
|
457
|
+
case 'index': {
|
|
458
|
+
const o = ev(n[1]);
|
|
459
|
+
const k = ev(n[2]);
|
|
460
|
+
need((Array.isArray(o) || isObj(o)) && k in o, 'index out of range');
|
|
461
|
+
return o[k];
|
|
462
|
+
}
|
|
463
|
+
case 'fn': {
|
|
464
|
+
const a = n.slice(2).map(ev);
|
|
465
|
+
if (n[1] === 'size')
|
|
466
|
+
return Array.isArray(a[0]) || typeof a[0] === 'string' ? a[0].length : (need(false, 'size needs a list or string'), 0);
|
|
467
|
+
if (n[1] === 'int')
|
|
468
|
+
return Math.trunc(Number(a[0]));
|
|
469
|
+
if (n[1] === 'double')
|
|
470
|
+
return Number(a[0]);
|
|
471
|
+
if (n[1] === 'string')
|
|
472
|
+
return String(a[0]);
|
|
473
|
+
throw new CelError(`unknown function '${n[1]}'`);
|
|
474
|
+
}
|
|
475
|
+
case 'call': {
|
|
476
|
+
const recv = ev(n[2]);
|
|
477
|
+
const a = n.slice(3).map(ev);
|
|
478
|
+
need(typeof recv === 'string' && typeof a[0] === 'string', `${n[1]} needs strings`);
|
|
479
|
+
if (n[1] === 'startsWith')
|
|
480
|
+
return recv.startsWith(a[0]);
|
|
481
|
+
if (n[1] === 'endsWith')
|
|
482
|
+
return recv.endsWith(a[0]);
|
|
483
|
+
if (n[1] === 'contains')
|
|
484
|
+
return recv.includes(a[0]);
|
|
485
|
+
if (n[1] === 'matches')
|
|
486
|
+
return new RegExp(a[0]).test(recv);
|
|
487
|
+
throw new CelError(`unknown method '${n[1]}'`);
|
|
488
|
+
}
|
|
489
|
+
default: {
|
|
490
|
+
const l = ev(n[1]);
|
|
491
|
+
const r = ev(n[2]);
|
|
492
|
+
switch (n[0]) {
|
|
493
|
+
case '==':
|
|
494
|
+
return JSON.stringify(l) === JSON.stringify(r);
|
|
495
|
+
case '!=':
|
|
496
|
+
return JSON.stringify(l) !== JSON.stringify(r);
|
|
497
|
+
case 'in':
|
|
498
|
+
return Array.isArray(r) ? r.some((x) => JSON.stringify(x) === JSON.stringify(l)) : isObj(r) ? l in r : (need(false, 'in needs a list or map'), false);
|
|
499
|
+
case '+':
|
|
500
|
+
return l + r;
|
|
501
|
+
case '-':
|
|
502
|
+
return l - r;
|
|
503
|
+
case '*':
|
|
504
|
+
return l * r;
|
|
505
|
+
case '/':
|
|
506
|
+
need(r !== 0, 'division by zero');
|
|
507
|
+
return l / r;
|
|
508
|
+
case '%':
|
|
509
|
+
need(r !== 0, 'division by zero');
|
|
510
|
+
return l % r;
|
|
511
|
+
default:
|
|
512
|
+
need(typeof l === typeof r, 'comparison of different types');
|
|
513
|
+
return n[0] === '<' ? l < r : n[0] === '<=' ? l <= r : n[0] === '>' ? l > r : l >= r;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// ── resolution ──
|
|
519
|
+
const MAX_DEPTH = 25;
|
|
520
|
+
class Resolver {
|
|
521
|
+
model;
|
|
522
|
+
context;
|
|
523
|
+
by = new Map();
|
|
524
|
+
objects = new Map();
|
|
525
|
+
errors = 0;
|
|
526
|
+
constructor(model, tuples, context) {
|
|
527
|
+
this.model = model;
|
|
528
|
+
this.context = context;
|
|
529
|
+
for (const t of tuples) {
|
|
530
|
+
const key = `${t.object}#${t.relation}`;
|
|
531
|
+
(this.by.get(key) ?? this.by.set(key, []).get(key)).push({ ...t, u: parseUser(t.user) });
|
|
532
|
+
const o = parseObject(t.object);
|
|
533
|
+
(this.objects.get(o.type) ?? this.objects.set(o.type, new Set()).get(o.type)).add(o.id);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
read(object, relation) {
|
|
537
|
+
return this.by.get(`${object}#${relation}`) ?? [];
|
|
538
|
+
}
|
|
539
|
+
holds(t) {
|
|
540
|
+
if (!t.condition)
|
|
541
|
+
return true;
|
|
542
|
+
const c = this.model.conds.get(t.condition.name);
|
|
543
|
+
try {
|
|
544
|
+
if (!c)
|
|
545
|
+
throw new CelError('unknown condition');
|
|
546
|
+
const merged = { ...(this.context ?? {}), ...(t.condition.context ?? {}) };
|
|
547
|
+
const vars = new Map();
|
|
548
|
+
for (const k of c.params.keys())
|
|
549
|
+
if (k in merged)
|
|
550
|
+
vars.set(k, c.params.get(k) === 'TYPE_NAME_TIMESTAMP' && typeof merged[k] === 'string' ? Date.parse(merged[k]) : merged[k]);
|
|
551
|
+
const out = evalCel(c.ast, vars);
|
|
552
|
+
if (typeof out !== 'boolean')
|
|
553
|
+
throw new CelError('condition did not evaluate to a bool');
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
if (err instanceof CelError) {
|
|
558
|
+
this.errors++;
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
throw err;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
check(user, object, relation) {
|
|
565
|
+
const o = parseObject(object);
|
|
566
|
+
return this.relation(user, o.type, o.id, relation, 0, new Set());
|
|
567
|
+
}
|
|
568
|
+
relation(user, type, id, relation, depth, path) {
|
|
569
|
+
if (depth > MAX_DEPTH)
|
|
570
|
+
return false;
|
|
571
|
+
const frame = `${type}:${id}#${relation}`;
|
|
572
|
+
if (path.has(frame))
|
|
573
|
+
return false;
|
|
574
|
+
const rewrite = this.model.types.get(type)?.get(relation);
|
|
575
|
+
if (!rewrite)
|
|
576
|
+
return false;
|
|
577
|
+
path.add(frame);
|
|
578
|
+
try {
|
|
579
|
+
return this.node(rewrite, user, type, id, relation, depth + 1, path);
|
|
580
|
+
}
|
|
581
|
+
finally {
|
|
582
|
+
path.delete(frame);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
node(n, user, type, id, relation, depth, path) {
|
|
586
|
+
if ('this' in n) {
|
|
587
|
+
for (const r of this.read(`${type}:${id}`, relation)) {
|
|
588
|
+
if (!this.holds(r))
|
|
589
|
+
continue;
|
|
590
|
+
if (r.u.wild) {
|
|
591
|
+
if (r.u.type === user.type && !user.wild)
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
else if (r.u.rel !== null) {
|
|
595
|
+
if (this.relation(user, r.u.type, r.u.id, r.u.rel, depth, path))
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
else if (r.u.type === user.type && r.u.id === user.id && user.rel === null)
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
if ('computedUserset' in n)
|
|
604
|
+
return this.relation(user, type, id, n.computedUserset.relation, depth, path);
|
|
605
|
+
if ('tupleToUserset' in n) {
|
|
606
|
+
const cr = n.tupleToUserset.computedUserset.relation;
|
|
607
|
+
for (const r of this.read(`${type}:${id}`, n.tupleToUserset.tupleset.relation)) {
|
|
608
|
+
if (r.u.wild || r.u.rel !== null || !this.holds(r))
|
|
609
|
+
continue;
|
|
610
|
+
if (this.model.types.get(r.u.type)?.has(cr) && this.relation(user, r.u.type, r.u.id, cr, depth, path))
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
if ('union' in n)
|
|
616
|
+
return n.union.child.some((c) => this.node(c, user, type, id, relation, depth, path));
|
|
617
|
+
if ('intersection' in n)
|
|
618
|
+
return n.intersection.child.every((c) => this.node(c, user, type, id, relation, depth, path));
|
|
619
|
+
if (!this.node(n.difference.base, user, type, id, relation, depth, path))
|
|
620
|
+
return false;
|
|
621
|
+
const before = this.errors;
|
|
622
|
+
const sub = this.node(n.difference.subtract, user, type, id, relation, depth, path);
|
|
623
|
+
// An exclusion that could not be evaluated denies instead of reading as "not excluded".
|
|
624
|
+
return this.errors > before ? false : !sub;
|
|
625
|
+
}
|
|
626
|
+
/** Ids of `type` objects the user holds `relation` on (candidates are the objects named in any tuple). */
|
|
627
|
+
listObjects(user, relation, type) {
|
|
628
|
+
return [...(this.objects.get(type) ?? [])].filter((id) => this.relation(user, type, id, relation, 0, new Set())).sort();
|
|
629
|
+
}
|
|
630
|
+
/** Concrete users of `userType` that hold `relation` on the object. */
|
|
631
|
+
listUsers(object, relation, userType) {
|
|
632
|
+
const o = parseObject(object);
|
|
633
|
+
const ids = new Set();
|
|
634
|
+
for (const list of this.by.values())
|
|
635
|
+
for (const r of list)
|
|
636
|
+
if (r.u.type === userType && !r.u.wild && r.u.rel === null)
|
|
637
|
+
ids.add(r.u.id);
|
|
638
|
+
return [...ids].filter((id) => this.relation({ type: userType, id, rel: null, wild: false }, o.type, o.id, relation, 0, new Set())).sort();
|
|
639
|
+
}
|
|
640
|
+
/** The userset tree of a relation (the rewrite structure with the direct users at each `this` leaf). */
|
|
641
|
+
expand(object, relation) {
|
|
642
|
+
const o = parseObject(object);
|
|
643
|
+
const walk = (n, depth) => {
|
|
644
|
+
if (depth > MAX_DEPTH)
|
|
645
|
+
return { type: 'leaf', users: [] };
|
|
646
|
+
if ('this' in n)
|
|
647
|
+
return { type: 'leaf', users: this.read(object, relation).map((r) => r.user) };
|
|
648
|
+
if ('computedUserset' in n)
|
|
649
|
+
return { type: 'leaf', users: [], computed: n.computedUserset.relation };
|
|
650
|
+
if ('tupleToUserset' in n)
|
|
651
|
+
return { type: 'leaf', users: [], tupleToUserset: { tupleset: n.tupleToUserset.tupleset.relation, computed: n.tupleToUserset.computedUserset.relation } };
|
|
652
|
+
if ('union' in n)
|
|
653
|
+
return { type: 'union', nodes: n.union.child.map((c) => walk(c, depth + 1)) };
|
|
654
|
+
if ('intersection' in n)
|
|
655
|
+
return { type: 'intersection', nodes: n.intersection.child.map((c) => walk(c, depth + 1)) };
|
|
656
|
+
return { type: 'difference', base: walk(n.difference.base, depth + 1), subtract: walk(n.difference.subtract, depth + 1) };
|
|
657
|
+
};
|
|
658
|
+
const rewrite = this.model.types.get(o.type)?.get(relation);
|
|
659
|
+
return rewrite ? walk(rewrite, 0) : { type: 'leaf', users: [] };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
exports.Resolver = Resolver;
|
|
663
|
+
//# sourceMappingURL=fga-engine.js.map
|