@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
package/dist/handlers/authz.js
CHANGED
|
@@ -2,153 +2,254 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.authzHandlers = authzHandlers;
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
|
-
const
|
|
5
|
+
const access_signals_1 = require("./access-signals");
|
|
6
|
+
const cedar_1 = require("./cedar");
|
|
7
|
+
const authz_layers_1 = require("./authz-layers");
|
|
8
|
+
const authz_state_1 = require("./authz-state");
|
|
6
9
|
const support_1 = require("./support");
|
|
7
10
|
const util_1 = require("./util");
|
|
8
11
|
/**
|
|
9
12
|
* Authorization introspection on the engine's own state: `POST /v1/authz/explain` (why a user has or lacks a permission,
|
|
10
|
-
*
|
|
11
|
-
* `POST /v1/authz/
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* asked of all seven layers: organization and app roles, groups, resource roles, ABAC, access policies and FGA tuples),
|
|
14
|
+
* `POST /v1/authz/rbac/check` (the role check, written to the decision log), `POST /v1/authz/what_if` (replay the logged
|
|
15
|
+
* role, ABAC and FGA decisions under a candidate change and see what flips), managed FGA sync (memberships, org roles and
|
|
16
|
+
* groups become tuples in one store, live on every change, with backfill, runs and purge) and the authorization event feed.
|
|
17
|
+
* The data the layers read is written by the routes in authz-data.
|
|
15
18
|
*/
|
|
16
|
-
const LAYERS = ['rbac', 'org_roles', 'groups', 'resource_roles', 'abac', 'access_policies', 'fga'];
|
|
17
19
|
const MAX_WAIT_MS = 25_000;
|
|
18
20
|
const REPLAY_LIMIT_DEFAULT = 2000;
|
|
19
21
|
const REPLAY_LIMIT_MAX = 10_000;
|
|
22
|
+
const SAMPLE_CAP = 50;
|
|
20
23
|
const bad = (message, param) => new errors_1.EmulatorError('VALIDATION_FAILED', message, param);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const skipped = (reason) => ({ status: 'skipped', effect: 'none', reason, contributions: [] });
|
|
48
|
-
const layers = { rbac: { status: 'evaluated', effect: r.allowed ? 'allow' : 'none', contributions: r.contributions }, org_roles: skipped('No delegated roles in the emulator.') };
|
|
49
|
-
for (const l of LAYERS.slice(2))
|
|
50
|
-
layers[l] = skipped(`The emulator holds no ${l.replace('_', ' ')} data.`);
|
|
51
|
-
const deciding = r.contributions.filter((c) => c.matched);
|
|
52
|
-
const decision = r.allowed ? 'allow' : 'deny';
|
|
53
|
-
return { object: 'authz_explanation', subject: { id: userId }, permission, organization_id: organizationId ?? null, decision, combination: 'deny_overrides', summary: r.allowed ? `Allowed by ${deciding.map((c) => c.via.key).join(', ')}.` : 'No layer allows this: denied by default.', deciding, layers, evaluated_at: e.clock.now() };
|
|
54
|
-
}
|
|
55
|
-
function pushEvent(e, kind, storeId, source, data) {
|
|
56
|
-
const s = st(e);
|
|
57
|
-
s.events.push({ object: 'authz_event', id: e.db.newId('azev'), seq: ++s.seq, kind, store_id: storeId, store_revision: s.seq, source, data, created_at: e.clock.now() });
|
|
58
|
-
}
|
|
59
|
-
const parseEdits = (body) => {
|
|
60
|
-
const roles = body.candidate?.rbac?.roles;
|
|
61
|
-
if (roles === undefined)
|
|
62
|
-
return undefined;
|
|
63
|
-
if (!Array.isArray(roles) || !roles.length || roles.length > 200)
|
|
64
|
-
throw bad('candidate.rbac.roles must list 1-200 role edits.', 'candidate.rbac.roles');
|
|
65
|
-
const out = new Map();
|
|
66
|
-
roles.forEach((r, i) => {
|
|
67
|
-
if (!r || typeof r !== 'object' || (r.type !== 'org_role' && r.type !== 'app_role'))
|
|
68
|
-
throw bad('type must be org_role or app_role.', `candidate.rbac.roles[${i}].type`);
|
|
69
|
-
if (typeof r.key !== 'string' || !r.key)
|
|
70
|
-
throw bad('key is required.', `candidate.rbac.roles[${i}].key`);
|
|
71
|
-
const list = (v, f) => {
|
|
72
|
-
if (v === undefined)
|
|
73
|
-
return [];
|
|
74
|
-
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
|
|
75
|
-
throw bad(`${f} must be a list of permissions.`, `candidate.rbac.roles[${i}].${f}`);
|
|
76
|
-
return v;
|
|
77
|
-
};
|
|
78
|
-
const prev = out.get(r.key) ?? { add: [], remove: [] };
|
|
79
|
-
out.set(r.key, { add: [...prev.add, ...list(r.add_permissions, 'add_permissions')], remove: [...prev.remove, ...list(r.remove_permissions, 'remove_permissions')] });
|
|
24
|
+
const obj = (v, field) => {
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
return {};
|
|
27
|
+
if (typeof v !== 'object' || Array.isArray(v))
|
|
28
|
+
throw bad(`${field} must be an object.`, field);
|
|
29
|
+
return v;
|
|
30
|
+
};
|
|
31
|
+
const strList = (v, field) => {
|
|
32
|
+
if (v === undefined)
|
|
33
|
+
return [];
|
|
34
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string' || !x))
|
|
35
|
+
throw bad(`${field} must be a list of strings.`, field);
|
|
36
|
+
return v;
|
|
37
|
+
};
|
|
38
|
+
const tupleList = (v, field) => {
|
|
39
|
+
if (v === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
if (!Array.isArray(v))
|
|
42
|
+
throw bad(`${field} must be a list of tuples.`, field);
|
|
43
|
+
return v.map((t, i) => {
|
|
44
|
+
const o = obj(t, `${field}[${i}]`);
|
|
45
|
+
if (typeof o.user !== 'string' || typeof o.relation !== 'string' || typeof (o.object ?? o.target) !== 'string')
|
|
46
|
+
throw bad(`${field}[${i}] requires user, relation and object.`, field);
|
|
47
|
+
if (!/^[^:#]+:[^#]+(#[^#]+)?$/.test(o.user))
|
|
48
|
+
throw bad(`${field}[${i}].user is malformed.`, field);
|
|
49
|
+
return { user: o.user, relation: o.relation, object: (o.object ?? o.target) };
|
|
80
50
|
});
|
|
81
|
-
return out;
|
|
82
51
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
52
|
+
const policySources = (v, field) => {
|
|
53
|
+
if (v === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
if (!Array.isArray(v))
|
|
56
|
+
throw bad(`${field} must be a list.`, field);
|
|
57
|
+
return v.map((p, i) => {
|
|
58
|
+
const o = obj(p, `${field}[${i}]`);
|
|
59
|
+
if (typeof o.name !== 'string' || !o.name || typeof o.source !== 'string' || !o.source)
|
|
60
|
+
throw bad(`${field}[${i}] needs name and source.`, field);
|
|
61
|
+
try {
|
|
62
|
+
(0, cedar_1.parseCedar)(o.source);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err instanceof cedar_1.CedarParseError)
|
|
66
|
+
throw bad(`${field}[${i}].source: ${err.message} (at character ${err.position})`, field);
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
return { name: o.name, source: o.source };
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const time = (v, field) => {
|
|
73
|
+
if (v === undefined || v === null)
|
|
74
|
+
return undefined;
|
|
75
|
+
const t = typeof v === 'number' ? v : typeof v === 'string' ? Date.parse(v) : NaN;
|
|
76
|
+
if (Number.isNaN(t))
|
|
77
|
+
throw bad(`${field} must be a millisecond timestamp or an ISO date.`, field);
|
|
78
|
+
return t;
|
|
79
|
+
};
|
|
80
|
+
function parseCandidate(cand) {
|
|
81
|
+
const out = {};
|
|
82
|
+
if (cand.abac !== undefined) {
|
|
83
|
+
const a = obj(cand.abac, 'candidate.abac');
|
|
84
|
+
out.abac = { add: policySources(a.add, 'candidate.abac.add'), replace: policySources(a.replace, 'candidate.abac.replace'), remove: strList(a.remove, 'candidate.abac.remove') };
|
|
94
85
|
}
|
|
86
|
+
if (cand.rbac !== undefined) {
|
|
87
|
+
const r = obj(cand.rbac, 'candidate.rbac');
|
|
88
|
+
if (!Array.isArray(r.roles) || r.roles.length === 0 || r.roles.length > 200)
|
|
89
|
+
throw bad('candidate.rbac.roles must list 1-200 role edits.', 'candidate.rbac.roles');
|
|
90
|
+
out.rbac = {
|
|
91
|
+
roles: r.roles.map((x, i) => {
|
|
92
|
+
const o = obj(x, `candidate.rbac.roles[${i}]`);
|
|
93
|
+
if (o.type !== 'org_role' && o.type !== 'app_role')
|
|
94
|
+
throw bad('type must be org_role or app_role.', `candidate.rbac.roles[${i}].type`);
|
|
95
|
+
if (typeof o.key !== 'string' || !o.key)
|
|
96
|
+
throw bad('key is required.', `candidate.rbac.roles[${i}].key`);
|
|
97
|
+
return { type: o.type, key: o.key, organizationId: typeof o.organization_id === 'string' ? o.organization_id : undefined, addPermissions: strList(o.add_permissions, 'add_permissions'), removePermissions: strList(o.remove_permissions, 'remove_permissions') };
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (cand.fga !== undefined) {
|
|
102
|
+
const f = obj(cand.fga, 'candidate.fga');
|
|
103
|
+
if (typeof f.store_id !== 'string' || !f.store_id)
|
|
104
|
+
throw bad('candidate.fga.store_id is required.', 'candidate.fga.store_id');
|
|
105
|
+
out.fga = { storeId: f.store_id, writes: tupleList(f.writes, 'candidate.fga.writes'), deletes: tupleList(f.deletes, 'candidate.fga.deletes') };
|
|
106
|
+
}
|
|
107
|
+
if (cand.access_policy !== undefined)
|
|
108
|
+
out.accessPolicy = obj(obj(cand.access_policy, 'candidate.access_policy').draft ?? cand.access_policy, 'candidate.access_policy.draft');
|
|
109
|
+
if (!out.abac && !out.rbac && !out.fga && !out.accessPolicy)
|
|
110
|
+
throw bad('candidate needs at least one of abac, rbac, fga, access_policy.', 'candidate');
|
|
95
111
|
return out;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (s.managed.has(k))
|
|
107
|
-
continue;
|
|
108
|
-
s.managed.set(k, t);
|
|
109
|
-
written++;
|
|
110
|
-
pushEvent(e, 'tuple_written', s.config.store_id, 'sync', { ...t });
|
|
111
|
-
e.webhooks.emit('fga.tuple_written', { object: 'fga_tuple_batch', store_id: s.config.store_id, store_revision: null, source: 'sync', count: 1, tuples: [{ ...t }] });
|
|
112
|
+
}
|
|
113
|
+
function runWhatIf(e, body) {
|
|
114
|
+
const c = parseCandidate(obj(body.candidate, 'candidate'));
|
|
115
|
+
const until = time(body.until, 'until') ?? e.clock.now();
|
|
116
|
+
let since = time(body.since, 'since');
|
|
117
|
+
if (since === undefined) {
|
|
118
|
+
const days = body.days === undefined ? 7 : body.days;
|
|
119
|
+
if (!Number.isInteger(days) || days < 1 || days > 30)
|
|
120
|
+
throw bad('days must be an integer from 1 to 30.', 'days');
|
|
121
|
+
since = until - days * 86_400_000;
|
|
112
122
|
}
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
if (since >= until)
|
|
124
|
+
throw bad('since must be before until.', 'since');
|
|
125
|
+
if (until - since > 31 * 86_400_000)
|
|
126
|
+
throw bad('The window can be at most 31 days.', 'since');
|
|
127
|
+
const limit = body.limit === undefined ? REPLAY_LIMIT_DEFAULT : body.limit;
|
|
128
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > REPLAY_LIMIT_MAX)
|
|
129
|
+
throw bad(`limit must be an integer from 1 to ${REPLAY_LIMIT_MAX}.`, 'limit');
|
|
130
|
+
if (body.name !== undefined && (typeof body.name !== 'string' || body.name.length > 200))
|
|
131
|
+
throw bad('name must be a string of at most 200 characters.', 'name');
|
|
132
|
+
if (body.save !== undefined && typeof body.save !== 'boolean')
|
|
133
|
+
throw bad('save must be a boolean.', 'save');
|
|
134
|
+
if (c.fga && !(0, authz_state_1.storeExists)(e, c.fga.storeId))
|
|
135
|
+
throw bad('Unknown FGA store.', 'candidate.fga.store_id');
|
|
136
|
+
if (c.fga && !(0, authz_layers_1.latestModel)(e, c.fga.storeId))
|
|
137
|
+
throw bad('The FGA store has no authorization model.', 'candidate.fga.store_id');
|
|
138
|
+
const by = {};
|
|
139
|
+
const skippedBy = {};
|
|
140
|
+
const skip = (reason) => void (skippedBy[reason] = (skippedBy[reason] ?? 0) + 1);
|
|
141
|
+
const flips = [];
|
|
142
|
+
const principals = new Set();
|
|
143
|
+
let replayed = 0;
|
|
144
|
+
const note = (row, before, after, mismatch) => {
|
|
145
|
+
replayed++;
|
|
146
|
+
const s = (by[row.source] ??= { replayed: 0, unchanged: 0, allow_to_deny: 0, deny_to_allow: 0, baseline_mismatch: 0 });
|
|
147
|
+
s.replayed++;
|
|
148
|
+
if (mismatch)
|
|
149
|
+
s.baseline_mismatch++;
|
|
150
|
+
if (before === after)
|
|
151
|
+
return void s.unchanged++;
|
|
152
|
+
if (before === 'allow')
|
|
153
|
+
s.allow_to_deny++;
|
|
154
|
+
else
|
|
155
|
+
s.deny_to_allow++;
|
|
156
|
+
if (row.principal)
|
|
157
|
+
principals.add(row.principal);
|
|
158
|
+
if (flips.length < SAMPLE_CAP)
|
|
159
|
+
flips.push({ decision_id: row.id, source: row.source, principal: row.principal, action: row.action, resource: row.resource, before, after, at: row.at });
|
|
160
|
+
};
|
|
161
|
+
const pdp = c.abac ? (0, authz_layers_1.loadPdp)(e) : null;
|
|
162
|
+
const cpdp = c.abac && pdp ? (0, authz_layers_1.candidatePdp)(pdp, c.abac) : null;
|
|
163
|
+
const all = (0, authz_state_1.st)(e).decisions.all().filter((d) => d.at >= since && d.at <= until);
|
|
164
|
+
const rows = all.slice(0, limit);
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
try {
|
|
167
|
+
if (['abac', 'authzen', 'mcp_gateway', 'pdp'].includes(row.source) && pdp && cpdp) {
|
|
168
|
+
const req = row.input;
|
|
169
|
+
if (!req || typeof req !== 'object' || !req.principal || !req.resource) {
|
|
170
|
+
skip('no_input');
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const now = (0, authz_layers_1.abacDecide)(pdp, req).decision;
|
|
174
|
+
note(row, now, (0, authz_layers_1.abacDecide)(cpdp, req).decision, now !== row.decision);
|
|
175
|
+
}
|
|
176
|
+
else if (row.source === 'rbac' && c.rbac) {
|
|
177
|
+
if (row.result?.kind !== 'role' || !row.action) {
|
|
178
|
+
skip('rbac_not_role_based');
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
note(row, row.decision, (0, authz_layers_1.replayRoleDecision)(row.result, c.rbac.roles, row.action) ? 'allow' : 'deny', false);
|
|
182
|
+
}
|
|
183
|
+
else if (row.source === 'fga' && c.fga) {
|
|
184
|
+
if (row.policyVersion?.store_id !== c.fga.storeId) {
|
|
185
|
+
skip('other_store');
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const i = row.input ?? {};
|
|
189
|
+
if (!i.user || !i.relation || !i.object) {
|
|
190
|
+
skip('no_input');
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const req = { user: i.user, relation: i.relation, object: i.object, contextual: i.contextual_tuples };
|
|
194
|
+
const now = (0, authz_layers_1.fgaCheck)(e, c.fga.storeId, req) ? 'allow' : 'deny';
|
|
195
|
+
note(row, now, (0, authz_layers_1.fgaCheck)(e, c.fga.storeId, req, { writes: c.fga.writes, deletes: c.fga.deletes }) ? 'allow' : 'deny', now !== row.decision);
|
|
196
|
+
}
|
|
197
|
+
else
|
|
198
|
+
skip(row.source === 'access_policy' ? 'access_policy_replayed_separately' : 'no_matching_candidate');
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
skip('replay_error');
|
|
121
202
|
}
|
|
122
203
|
}
|
|
204
|
+
// shortcut: the emulator keeps no sampled sign-ins, so an access-policy candidate has nothing to replay against.
|
|
205
|
+
if (c.accessPolicy)
|
|
206
|
+
skip('access_policy_no_sampled_sign_ins');
|
|
207
|
+
const totals = { unchanged: 0, allow_to_deny: 0, deny_to_allow: 0 };
|
|
208
|
+
for (const s of Object.values(by)) {
|
|
209
|
+
totals.unchanged += s.unchanged;
|
|
210
|
+
totals.allow_to_deny += s.allow_to_deny;
|
|
211
|
+
totals.deny_to_allow += s.deny_to_allow;
|
|
212
|
+
}
|
|
213
|
+
const result = { object: 'authz_what_if', name: body.name ?? null, window: { since, until }, evaluated: rows.length, replayed, truncated: all.length > rows.length, skipped: skippedBy, by_source: by, totals, affected_principals: principals.size, samples: flips };
|
|
214
|
+
if (body.save === false)
|
|
215
|
+
return { ...result, status: 200 };
|
|
216
|
+
const describe = {
|
|
217
|
+
...(c.abac ? { abac: c.abac } : {}),
|
|
218
|
+
...(c.rbac ? { rbac: { roles: c.rbac.roles.map((r) => ({ type: r.type, key: r.key, organization_id: r.organizationId ?? null, add_permissions: r.addPermissions, remove_permissions: r.removePermissions })) } } : {}),
|
|
219
|
+
...(c.fga ? { fga: { store_id: c.fga.storeId, writes: c.fga.writes, deletes: c.fga.deletes } } : {}),
|
|
220
|
+
...(c.accessPolicy ? { access_policy: { draft: c.accessPolicy } } : {}),
|
|
221
|
+
};
|
|
222
|
+
const saved = (0, authz_state_1.st)(e).whatIf.insert({ name: body.name ?? null, candidate: describe, window_start: since, window_end: until, summary: { evaluated: rows.length, replayed, truncated: result.truncated, totals, affected_principals: principals.size, by_source: by }, result, created_at: e.clock.now() });
|
|
223
|
+
return { ...result, id: saved.id, created_at: saved.created_at, status: 201 };
|
|
224
|
+
}
|
|
225
|
+
const whatIfView = (r, full) => ({ object: 'authz_what_if_run', id: r.id, name: r.name, candidate: r.candidate, window: { since: r.window_start, until: r.window_end }, summary: r.summary, ...(full ? { result: r.result } : {}), created_by: null, created_at: r.created_at });
|
|
226
|
+
/** One sync run: write what should exist; with `repair`, also delete managed tuples that no longer should. Only managed tuples are ever touched. */
|
|
227
|
+
function runSync(e, kind, repair) {
|
|
228
|
+
const s = (0, authz_state_1.st)(e);
|
|
229
|
+
const now = e.clock.now();
|
|
230
|
+
const { written, deleted } = (0, authz_state_1.reconcileManaged)(e, repair);
|
|
123
231
|
const run = s.runs.insert({ kind, status: 'completed', store_id: s.config.store_id, repair, written, deleted, managed: s.managed.size, error: null, created_at: now, completed_at: now });
|
|
124
|
-
pushEvent(e, 'sync_run', s.config.store_id, 'sync', { run_id: run.id, written, deleted });
|
|
232
|
+
(0, authz_state_1.pushEvent)(e, 'sync_run', s.config.store_id, 'sync', { run_id: run.id, written, deleted });
|
|
125
233
|
e.webhooks.emit('fga_sync.completed', { object: 'fga_sync_run', id: run.id, store_id: s.config.store_id, written, deleted });
|
|
126
234
|
return run;
|
|
127
235
|
}
|
|
128
236
|
const runView = (r) => ({ object: 'fga_sync_run', id: r.id, kind: r.kind, status: r.status, store_id: r.store_id, repair: r.repair, tuples_written: r.written, tuples_deleted: r.deleted, managed_tuples: r.managed, error: r.error, created_at: r.created_at, completed_at: r.completed_at });
|
|
129
237
|
const statusView = (e) => {
|
|
130
|
-
const s = st(e);
|
|
238
|
+
const s = (0, authz_state_1.st)(e);
|
|
131
239
|
const last = s.runs.all()[0];
|
|
132
240
|
return { object: 'fga_sync', enabled: s.config.enabled, store_id: s.config.store_id, memberships: s.config.memberships, org_roles: s.config.org_roles, groups: s.config.groups, managed_tuples: s.managed.size, last_run: last ? runView(last) : null, updated_at: s.config.updated_at };
|
|
133
241
|
};
|
|
134
|
-
const storeExists = (e, id) => !!e.generic.get('fga/stores')?.get(id);
|
|
135
242
|
function authzHandlers() {
|
|
136
|
-
const whatIfView = (r, full) => ({ object: 'authz_what_if_run', id: r.id, status: 'completed', created_at: r.created_at, window: r.window, evaluated: r.evaluated, changed: r.changed, flipped_to_allow: r.flipped_to_allow, flipped_to_deny: r.flipped_to_deny, truncated: r.truncated, not_evaluated: r.not_evaluated, ...(full ? { samples: r.samples } : {}) });
|
|
137
243
|
return [
|
|
138
244
|
{
|
|
139
245
|
method: 'POST',
|
|
140
246
|
pattern: '/v1/authz/explain',
|
|
141
247
|
scope: 'authz_decisions:read',
|
|
142
248
|
handler: (c) => {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (typeof permission !== 'string' || !permission)
|
|
148
|
-
throw bad('permission (or action) is required.', 'permission');
|
|
149
|
-
if (c.body.organization_id !== undefined && typeof c.body.organization_id !== 'string')
|
|
150
|
-
throw bad('organization_id must be a string.', 'organization_id');
|
|
151
|
-
return { body: explain(c.engine, id, permission, c.body.organization_id) };
|
|
249
|
+
const input = (0, authz_layers_1.parseExplainBody)(c.body);
|
|
250
|
+
// Signals a caller proxies in headers (country, risk level, device trust) apply when the body does not say.
|
|
251
|
+
input.access = { ...input.access, signals: (0, access_signals_1.firstOf)(input.access?.signals, (0, access_signals_1.headerSignals)(c.request.headers)) };
|
|
252
|
+
return { body: (0, authz_layers_1.explainAccess)(c.engine, input) };
|
|
152
253
|
},
|
|
153
254
|
},
|
|
154
255
|
{
|
|
@@ -158,17 +259,18 @@ function authzHandlers() {
|
|
|
158
259
|
scope: 'authz_decisions:read',
|
|
159
260
|
handler: (c) => {
|
|
160
261
|
const e = c.engine;
|
|
161
|
-
const
|
|
162
|
-
if (typeof id !== 'string' || !id)
|
|
262
|
+
const subject = (c.body.subject ?? {});
|
|
263
|
+
if (typeof subject.id !== 'string' || !subject.id)
|
|
163
264
|
throw bad('subject.id is required.', 'subject.id');
|
|
164
265
|
if (typeof c.body.permission !== 'string' || !c.body.permission)
|
|
165
266
|
throw bad('permission is required.', 'permission');
|
|
166
|
-
if (
|
|
167
|
-
throw (
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
267
|
+
if (c.body.organization_id !== undefined && typeof c.body.organization_id !== 'string')
|
|
268
|
+
throw bad('organization_id must be a string.', 'organization_id');
|
|
269
|
+
const input = { userId: subject.id, permission: c.body.permission, organizationId: c.body.organization_id };
|
|
270
|
+
const r = (0, authz_layers_1.rbacCheck)(e, input);
|
|
271
|
+
(0, authz_layers_1.recordRbacCheck)(e, input, r);
|
|
272
|
+
const rules = [...r.layers.rbac.contributions, ...r.layers.org_roles.contributions, ...r.layers.groups.contributions].filter((x) => x.matched);
|
|
273
|
+
return { body: { object: 'rbac_check', allowed: r.allowed, decision: r.allowed ? 'allow' : 'deny', grants: r.grants, rules } };
|
|
172
274
|
},
|
|
173
275
|
},
|
|
174
276
|
{
|
|
@@ -176,57 +278,17 @@ function authzHandlers() {
|
|
|
176
278
|
pattern: '/v1/authz/what_if',
|
|
177
279
|
scope: 'authz_decisions:read',
|
|
178
280
|
handler: (c) => {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
if (!cand || typeof cand !== 'object' || Array.isArray(cand))
|
|
182
|
-
throw bad('candidate is required.', 'candidate');
|
|
183
|
-
const edits = parseEdits(c.body);
|
|
184
|
-
const notEvaluated = [];
|
|
185
|
-
for (const k of ['abac', 'fga', 'access_policy'])
|
|
186
|
-
if (cand[k] !== undefined)
|
|
187
|
-
notEvaluated.push(k);
|
|
188
|
-
if (cand.fga !== undefined && (!cand.fga || typeof cand.fga.store_id !== 'string' || !cand.fga.store_id))
|
|
189
|
-
throw bad('candidate.fga.store_id is required.', 'candidate.fga.store_id');
|
|
190
|
-
if (!edits && !notEvaluated.length)
|
|
191
|
-
throw bad('candidate needs at least one of abac, rbac, fga, access_policy.', 'candidate');
|
|
192
|
-
const until = c.body.until === undefined ? e.clock.now() : Date.parse(String(c.body.until));
|
|
193
|
-
const days = (0, support_1.intIn)(c.body.days, 'days', 1, 30, 7);
|
|
194
|
-
const since = c.body.since === undefined ? until - days * 86_400_000 : Date.parse(String(c.body.since));
|
|
195
|
-
if (Number.isNaN(until) || Number.isNaN(since))
|
|
196
|
-
throw bad('since and until must be ISO 8601 times.', 'since');
|
|
197
|
-
if (since >= until)
|
|
198
|
-
throw bad('since must be before until.', 'since');
|
|
199
|
-
if (until - since > 31 * 86_400_000)
|
|
200
|
-
throw bad('The window can be at most 31 days.', 'since');
|
|
201
|
-
const limit = (0, support_1.intIn)(c.body.limit, 'limit', 1, REPLAY_LIMIT_MAX, REPLAY_LIMIT_DEFAULT);
|
|
202
|
-
const all = st(e).decisions.all().filter((d) => d.at >= since && d.at <= until);
|
|
203
|
-
const rows = all.slice(0, limit);
|
|
204
|
-
const flips = [];
|
|
205
|
-
let toAllow = 0;
|
|
206
|
-
let toDeny = 0;
|
|
207
|
-
for (const d of rows) {
|
|
208
|
-
if (!edits)
|
|
209
|
-
break;
|
|
210
|
-
const next = rbac(e, d.user_id, d.permission, d.organization_id ?? undefined, edits).allowed;
|
|
211
|
-
if (next === d.allowed)
|
|
212
|
-
continue;
|
|
213
|
-
next ? toAllow++ : toDeny++;
|
|
214
|
-
flips.push({ decision_id: d.id, user_id: d.user_id, permission: d.permission, organization_id: d.organization_id, before: d.allowed ? 'allow' : 'deny', after: next ? 'allow' : 'deny' });
|
|
215
|
-
}
|
|
216
|
-
const result = { evaluated: rows.length, changed: flips.length, flipped_to_allow: toAllow, flipped_to_deny: toDeny, truncated: all.length > rows.length, not_evaluated: notEvaluated, samples: flips.slice(0, 25), window: { since, until } };
|
|
217
|
-
if (c.body.save === false)
|
|
218
|
-
return { body: { object: 'authz_what_if_run', id: null, status: 'completed', created_at: e.clock.now(), ...result } };
|
|
219
|
-
const row = st(e).whatIf.insert({ ...result, created_at: e.clock.now() });
|
|
220
|
-
return { status: 201, body: whatIfView(row, true) };
|
|
281
|
+
const { status, ...body } = runWhatIf(c.engine, c.body);
|
|
282
|
+
return { status, body };
|
|
221
283
|
},
|
|
222
284
|
},
|
|
223
|
-
{ method: 'GET', pattern: '/v1/authz/what_if', scope: 'authz_decisions:read', handler: (c) => (0, util_1.listReply)(c, st(c.engine).whatIf.all(), (r) => whatIfView(r, false)) },
|
|
285
|
+
{ method: 'GET', pattern: '/v1/authz/what_if', scope: 'authz_decisions:read', handler: (c) => (0, util_1.listReply)(c, (0, authz_state_1.st)(c.engine).whatIf.all(), (r) => whatIfView(r, false)) },
|
|
224
286
|
{
|
|
225
287
|
method: 'GET',
|
|
226
288
|
pattern: '/v1/authz/what_if/:id',
|
|
227
289
|
scope: 'authz_decisions:read',
|
|
228
290
|
handler: (c) => {
|
|
229
|
-
const r = st(c.engine).whatIf.get(c.params.id);
|
|
291
|
+
const r = (0, authz_state_1.st)(c.engine).whatIf.get(c.params.id);
|
|
230
292
|
if (!r)
|
|
231
293
|
throw (0, errors_1.notFound)('What-if run');
|
|
232
294
|
return { body: whatIfView(r, true) };
|
|
@@ -237,7 +299,7 @@ function authzHandlers() {
|
|
|
237
299
|
pattern: '/v1/authz/what_if/:id',
|
|
238
300
|
scope: 'authz_decisions:write',
|
|
239
301
|
handler: (c) => {
|
|
240
|
-
if (!st(c.engine).whatIf.delete(c.params.id ?? ''))
|
|
302
|
+
if (!(0, authz_state_1.st)(c.engine).whatIf.delete(c.params.id ?? ''))
|
|
241
303
|
throw (0, errors_1.notFound)('What-if run');
|
|
242
304
|
return { body: { object: 'authz_what_if_run', id: c.params.id, deleted: true } };
|
|
243
305
|
},
|
|
@@ -250,13 +312,13 @@ function authzHandlers() {
|
|
|
250
312
|
scope: 'fga:write',
|
|
251
313
|
handler: (c) => {
|
|
252
314
|
const e = c.engine;
|
|
253
|
-
const s = st(e);
|
|
315
|
+
const s = (0, authz_state_1.st)(e);
|
|
254
316
|
const b = c.body;
|
|
255
317
|
for (const k of ['enabled', 'memberships', 'org_roles', 'groups'])
|
|
256
318
|
if (b[k] !== undefined && typeof b[k] !== 'boolean')
|
|
257
319
|
throw bad(`${k} must be true or false.`, k);
|
|
258
320
|
const storeId = b.store_id === undefined ? s.config.store_id : b.store_id;
|
|
259
|
-
if (storeId !== null && (typeof storeId !== 'string' || !storeExists(e, storeId)))
|
|
321
|
+
if (storeId !== null && (typeof storeId !== 'string' || !(0, authz_state_1.storeExists)(e, storeId)))
|
|
260
322
|
throw bad('store_id must be an existing FGA store.', 'store_id');
|
|
261
323
|
// Owned tuples live in one store: it cannot change under them.
|
|
262
324
|
if (storeId !== s.config.store_id && s.managed.size)
|
|
@@ -279,18 +341,18 @@ function authzHandlers() {
|
|
|
279
341
|
const e = c.engine;
|
|
280
342
|
if (c.body.repair !== undefined && typeof c.body.repair !== 'boolean')
|
|
281
343
|
throw bad('repair must be a boolean.', 'repair');
|
|
282
|
-
if (!st(e).config.enabled || !st(e).config.store_id)
|
|
344
|
+
if (!(0, authz_state_1.st)(e).config.enabled || !(0, authz_state_1.st)(e).config.store_id)
|
|
283
345
|
throw bad('Enable sync with a store before running a backfill.', 'enabled');
|
|
284
346
|
return { status: 202, body: runView(runSync(e, 'backfill', c.body.repair === true)) };
|
|
285
347
|
},
|
|
286
348
|
},
|
|
287
|
-
{ method: 'GET', pattern: '/v1/fga/sync/runs', scope: 'fga:read', handler: (c) => (0, util_1.listReply)(c, st(c.engine).runs.all(), runView) },
|
|
349
|
+
{ method: 'GET', pattern: '/v1/fga/sync/runs', scope: 'fga:read', handler: (c) => (0, util_1.listReply)(c, (0, authz_state_1.st)(c.engine).runs.all(), runView) },
|
|
288
350
|
{
|
|
289
351
|
method: 'GET',
|
|
290
352
|
pattern: '/v1/fga/sync/runs/:id',
|
|
291
353
|
scope: 'fga:read',
|
|
292
354
|
handler: (c) => {
|
|
293
|
-
const r = st(c.engine).runs.get(c.params.id);
|
|
355
|
+
const r = (0, authz_state_1.st)(c.engine).runs.get(c.params.id);
|
|
294
356
|
if (!r)
|
|
295
357
|
throw (0, errors_1.notFound)('Sync run');
|
|
296
358
|
return { body: runView(r) };
|
|
@@ -303,7 +365,7 @@ function authzHandlers() {
|
|
|
303
365
|
scope: 'fga:write',
|
|
304
366
|
handler: (c) => {
|
|
305
367
|
const e = c.engine;
|
|
306
|
-
const s = st(e);
|
|
368
|
+
const s = (0, authz_state_1.st)(e);
|
|
307
369
|
if (c.body.confirm !== true)
|
|
308
370
|
throw bad('Send confirm: true to remove the tuples the sync wrote.', 'confirm');
|
|
309
371
|
if (!s.config.store_id)
|
|
@@ -311,9 +373,10 @@ function authzHandlers() {
|
|
|
311
373
|
if (s.config.enabled)
|
|
312
374
|
throw new errors_1.EmulatorError('CONFLICT', 'Disable sync before purging.');
|
|
313
375
|
const n = s.managed.size;
|
|
314
|
-
|
|
315
|
-
pushEvent(e, 'tuple_deleted', s.config.store_id, 'sync', { ...t });
|
|
376
|
+
const owned = [...s.managed.values()];
|
|
316
377
|
s.managed.clear();
|
|
378
|
+
if (owned.length)
|
|
379
|
+
(0, authz_state_1.applyTuples)(e, s.config.store_id, [], owned, 'sync');
|
|
317
380
|
return { body: { object: 'fga.sync_purge', store_id: s.config.store_id, deleted: n, released: 0 } };
|
|
318
381
|
},
|
|
319
382
|
},
|
|
@@ -324,7 +387,7 @@ function authzHandlers() {
|
|
|
324
387
|
scope: 'fga:read',
|
|
325
388
|
handler: async (c) => {
|
|
326
389
|
const e = c.engine;
|
|
327
|
-
const s = st(e);
|
|
390
|
+
const s = (0, authz_state_1.st)(e);
|
|
328
391
|
const rawAfter = c.query.get('after');
|
|
329
392
|
const after = rawAfter === 'latest' ? s.seq : rawAfter === null ? 0 : Number(rawAfter);
|
|
330
393
|
if (!Number.isInteger(after) || after < 0)
|