@atlasauth/emulator-core 0.1.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/adopt.d.ts +50 -0
- package/dist/adopt.js +57 -0
- package/dist/adopt.js.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.js +31 -0
- package/dist/coverage.js.map +1 -0
- package/dist/crypto-util.d.ts +10 -0
- package/dist/crypto-util.js +76 -0
- package/dist/crypto-util.js.map +1 -0
- 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 +34 -3
- package/dist/engine.js +57 -8
- package/dist/engine.js.map +1 -1
- package/dist/error-catalog.js +16 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/event-gate.d.ts +3 -0
- package/dist/event-gate.js +430 -0
- package/dist/event-gate.js.map +1 -0
- package/dist/generic.js +14 -2
- 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.d.ts +2 -0
- package/dist/handlers/approvals.js +418 -0
- package/dist/handlers/approvals.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/handlers/authz.js +410 -0
- package/dist/handlers/authz.js.map +1 -0
- package/dist/handlers/billing.d.ts +100 -0
- package/dist/handlers/billing.js +877 -0
- package/dist/handlers/billing.js.map +1 -0
- 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 +5 -0
- package/dist/handlers/connections.js +561 -0
- package/dist/handlers/connections.js.map +1 -0
- package/dist/handlers/control.js +6 -1
- package/dist/handlers/control.js.map +1 -1
- package/dist/handlers/device.d.ts +47 -0
- package/dist/handlers/device.js +665 -0
- package/dist/handlers/device.js.map +1 -0
- 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/federation.d.ts +2 -0
- package/dist/handlers/federation.js +481 -0
- package/dist/handlers/federation.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/governance.d.ts +2 -0
- package/dist/handlers/governance.js +460 -0
- package/dist/handlers/governance.js.map +1 -0
- package/dist/handlers/hooks.js +17 -1
- package/dist/handlers/hooks.js.map +1 -1
- package/dist/handlers/index.d.ts +5 -1
- package/dist/handlers/index.js +45 -3
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/keys.js +6 -1
- package/dist/handlers/keys.js.map +1 -1
- package/dist/handlers/machines.js +61 -8
- package/dist/handlers/machines.js.map +1 -1
- package/dist/handlers/messaging.d.ts +6 -0
- package/dist/handlers/messaging.js +893 -0
- package/dist/handlers/messaging.js.map +1 -0
- 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 +52 -0
- package/dist/handlers/oauth-flows.js +480 -0
- package/dist/handlers/oauth-flows.js.map +1 -0
- package/dist/handlers/oauth.d.ts +96 -0
- package/dist/handlers/oauth.js +793 -0
- package/dist/handlers/oauth.js.map +1 -0
- package/dist/handlers/orgs.js +6 -0
- package/dist/handlers/orgs.js.map +1 -1
- package/dist/handlers/passkeys.d.ts +3 -0
- package/dist/handlers/passkeys.js +293 -0
- package/dist/handlers/passkeys.js.map +1 -0
- package/dist/handlers/platform.d.ts +20 -0
- package/dist/handlers/platform.js +472 -0
- package/dist/handlers/platform.js.map +1 -0
- package/dist/handlers/round2.d.ts +8 -0
- package/dist/handlers/round2.js +372 -0
- package/dist/handlers/round2.js.map +1 -0
- package/dist/handlers/scim.d.ts +4 -0
- package/dist/handlers/scim.js +595 -0
- package/dist/handlers/scim.js.map +1 -0
- package/dist/handlers/sessions.d.ts +9 -3
- package/dist/handlers/sessions.js +76 -38
- package/dist/handlers/sessions.js.map +1 -1
- package/dist/handlers/support.d.ts +20 -0
- package/dist/handlers/support.js +62 -0
- package/dist/handlers/support.js.map +1 -0
- package/dist/handlers/users.js +7 -0
- package/dist/handlers/users.js.map +1 -1
- package/dist/handlers/workflows.d.ts +2 -0
- package/dist/handlers/workflows.js +428 -0
- package/dist/handlers/workflows.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +23 -1
- package/dist/index.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.d.ts +41 -0
- package/dist/jws.js +135 -0
- package/dist/jws.js.map +1 -0
- package/dist/messages.d.ts +7 -0
- package/dist/messages.js +8 -4
- package/dist/messages.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-bin.js +3 -1
- package/dist/parity-bin.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.d.ts +14 -0
- package/dist/parity-routes.js +439 -0
- package/dist/parity-routes.js.map +1 -0
- package/dist/parity.d.ts +10 -3
- package/dist/parity.js +14 -5
- package/dist/parity.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 +85 -3
- package/dist/tokens.js +196 -24
- package/dist/tokens.js.map +1 -1
- package/dist/webauthn-virtual.d.ts +70 -0
- package/dist/webauthn-virtual.js +186 -0
- package/dist/webauthn-virtual.js.map +1 -0
- package/dist/webhook-ext.d.ts +16 -0
- package/dist/webhook-ext.js +218 -0
- package/dist/webhook-ext.js.map +1 -0
- package/dist/webhooks.d.ts +3 -0
- package/dist/webhooks.js +37 -3
- package/dist/webhooks.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.latestModel = exports.abacDecide = exports.grantView = exports.ancestorsOf = exports.findResourceRef = exports.orgRolesLayer = exports.LAYERS = void 0;
|
|
4
|
+
exports.rbacLayer = rbacLayer;
|
|
5
|
+
exports.groupsLayer = groupsLayer;
|
|
6
|
+
exports.rbacCheck = rbacCheck;
|
|
7
|
+
exports.logDecision = logDecision;
|
|
8
|
+
exports.recordRbacCheck = recordRbacCheck;
|
|
9
|
+
exports.replayRoleDecision = replayRoleDecision;
|
|
10
|
+
exports.parseSubject = parseSubject;
|
|
11
|
+
exports.expandSubjects = expandSubjects;
|
|
12
|
+
exports.checkResource = checkResource;
|
|
13
|
+
exports.accessibleResources = accessibleResources;
|
|
14
|
+
exports.recordResourceCheck = recordResourceCheck;
|
|
15
|
+
exports.loadPdp = loadPdp;
|
|
16
|
+
exports.linkedPolicies = linkedPolicies;
|
|
17
|
+
exports.candidatePdp = candidatePdp;
|
|
18
|
+
exports.ipInCidr = ipInCidr;
|
|
19
|
+
exports.compareVersions = compareVersions;
|
|
20
|
+
exports.failing = failing;
|
|
21
|
+
exports.zoneMatches = zoneMatches;
|
|
22
|
+
exports.evalSet = evalSet;
|
|
23
|
+
exports.fgaCheck = fgaCheck;
|
|
24
|
+
exports.combine = combine;
|
|
25
|
+
exports.explainAccess = explainAccess;
|
|
26
|
+
exports.parseExplainBody = parseExplainBody;
|
|
27
|
+
const errors_1 = require("../errors");
|
|
28
|
+
const access_signals_1 = require("./access-signals");
|
|
29
|
+
const cedar_ext_1 = require("./cedar-ext");
|
|
30
|
+
const fga_engine_1 = require("./fga-engine");
|
|
31
|
+
const cedar_1 = require("./cedar");
|
|
32
|
+
const authz_state_1 = require("./authz-state");
|
|
33
|
+
/**
|
|
34
|
+
* The seven authorization layers `POST /v1/authz/explain` asks, each evaluated from the emulator's own data:
|
|
35
|
+
* organization roles and direct app roles, group-mapped app roles, resource roles, ABAC policies, access policies and FGA tuples.
|
|
36
|
+
*/
|
|
37
|
+
exports.LAYERS = ['rbac', 'org_roles', 'groups', 'resource_roles', 'abac', 'access_policies', 'fga'];
|
|
38
|
+
const CAP = 50;
|
|
39
|
+
const bad = (message, param) => new errors_1.EmulatorError('VALIDATION_FAILED', message, param);
|
|
40
|
+
const skipped = (reason) => ({ status: 'skipped', decision: 'no_match', reason, contributions: [] });
|
|
41
|
+
const decisionOf = (cs) => {
|
|
42
|
+
const hit = cs.filter((c) => c.matched);
|
|
43
|
+
if (hit.some((c) => c.effect === 'deny'))
|
|
44
|
+
return 'deny';
|
|
45
|
+
if (hit.some((c) => c.effect === 'step_up'))
|
|
46
|
+
return 'step_up';
|
|
47
|
+
if (hit.some((c) => c.effect === 'allow'))
|
|
48
|
+
return 'allow';
|
|
49
|
+
return 'no_match';
|
|
50
|
+
};
|
|
51
|
+
const done = (contributions, reason) => ({ status: 'evaluated', decision: decisionOf(contributions), ...(reason ? { reason } : {}), contributions });
|
|
52
|
+
const has = (perms, action) => !!perms && perms.includes(action);
|
|
53
|
+
// ── rbac, org roles, groups ──
|
|
54
|
+
function rbacLayer(e, i) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const m of e.db.memberships.filter((x) => x.user_id === i.userId && (!i.resource.organizationId || x.organization_id === i.resource.organizationId)).slice(0, CAP)) {
|
|
57
|
+
const role = e.db.roles.find((r) => r.key === m.role);
|
|
58
|
+
const hit = has(role?.permissions, i.action);
|
|
59
|
+
out.push({
|
|
60
|
+
layer: 'rbac',
|
|
61
|
+
effect: 'allow',
|
|
62
|
+
matched: hit,
|
|
63
|
+
rule: { type: 'organization_role', id: role?.id ?? null, name: m.role },
|
|
64
|
+
reason: hit ? `Holds role "${m.role}" in organization ${m.organization_id}, which grants ${i.action}.` : `Role "${m.role}" in organization ${m.organization_id} does not grant ${i.action}.`,
|
|
65
|
+
evidence: [`organization:${m.organization_id}`],
|
|
66
|
+
via: [{ kind: 'org_role', role_key: m.role, organization_id: m.organization_id }],
|
|
67
|
+
});
|
|
68
|
+
if (has(m.permissions, i.action)) {
|
|
69
|
+
out.push({ layer: 'rbac', effect: 'allow', matched: true, rule: { type: 'member_permission', id: null, name: i.action }, reason: `Granted ${i.action} directly on the membership in organization ${m.organization_id}.`, evidence: [`organization:${m.organization_id}`], via: [{ kind: 'member_permission', organization_id: m.organization_id }] });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const r of (0, authz_state_1.directAppRoles)(e, i.userId).slice(0, CAP)) {
|
|
73
|
+
const hit = has(r.permissions, i.action);
|
|
74
|
+
out.push({ layer: 'rbac', effect: 'allow', matched: hit, rule: { type: 'app_role', id: r.id, name: r.key }, reason: hit ? `Assigned app role "${r.key}", which grants ${i.action}.` : `App role "${r.key}" does not grant ${i.action}.`, via: [{ kind: 'app_role', role_key: r.key }] });
|
|
75
|
+
}
|
|
76
|
+
if (!out.length)
|
|
77
|
+
out.push({ layer: 'rbac', effect: 'allow', matched: false, rule: { type: 'none', id: null, name: null }, reason: i.resource.organizationId ? 'Not a member of that organization and holds no app role.' : 'Holds no organization role and no app role.' });
|
|
78
|
+
return done(out);
|
|
79
|
+
}
|
|
80
|
+
// shortcut: the emulator has no cross-organization role delegation, so this layer always reports "no active delegations".
|
|
81
|
+
const orgRolesLayer = () => ({ status: 'evaluated', decision: 'no_match', reason: 'No active role delegations.', contributions: [] });
|
|
82
|
+
exports.orgRolesLayer = orgRolesLayer;
|
|
83
|
+
function groupsLayer(e, i) {
|
|
84
|
+
const groups = (0, authz_state_1.effectiveGroups)(e, i.userId);
|
|
85
|
+
if (!groups.length)
|
|
86
|
+
return { status: 'evaluated', decision: 'no_match', reason: 'Not in any group.', contributions: [] };
|
|
87
|
+
const byKey = new Map(((0, authz_state_1.appRoleTable)(e)?.all() ?? []).map((r) => [r.key, r.permissions]));
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const g of groups.slice(0, CAP)) {
|
|
90
|
+
const keys = g.app_role_keys ?? [];
|
|
91
|
+
const granting = keys.filter((k) => has(byKey.get(k), i.action));
|
|
92
|
+
out.push({
|
|
93
|
+
layer: 'groups',
|
|
94
|
+
effect: 'allow',
|
|
95
|
+
matched: granting.length > 0,
|
|
96
|
+
rule: { type: 'group', id: g.id, name: g.slug },
|
|
97
|
+
reason: granting.length ? `Member of group "${g.slug}", which maps app role ${granting.map((k) => `"${k}"`).join(', ')} granting ${i.action}.` : keys.length ? `Member of group "${g.slug}"; none of its app roles grant ${i.action}.` : `Member of group "${g.slug}", which maps no app roles.`,
|
|
98
|
+
evidence: granting.map((k) => `app_role:${k}`),
|
|
99
|
+
via: keys.map((k) => ({ kind: 'app_role', role_key: k })),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return done(out);
|
|
103
|
+
}
|
|
104
|
+
const dedupe = (list) => {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
return list.filter((v) => {
|
|
107
|
+
const k = `${v.kind}|${v.role_key ?? ''}|${v.organization_id ?? ''}`;
|
|
108
|
+
if (seen.has(k))
|
|
109
|
+
return false;
|
|
110
|
+
seen.add(k);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
/** A role-based check (organization roles, member permissions, app roles, group roles), with the roles held so a what-if can replay it. */
|
|
115
|
+
function rbacCheck(e, i) {
|
|
116
|
+
const base = { userId: i.userId, action: i.permission, resource: { organizationId: i.organizationId } };
|
|
117
|
+
const layers = { rbac: rbacLayer(e, base), org_roles: (0, exports.orgRolesLayer)(), groups: groupsLayer(e, base) };
|
|
118
|
+
const all = [...layers.rbac.contributions, ...layers.org_roles.contributions, ...layers.groups.contributions];
|
|
119
|
+
return {
|
|
120
|
+
allowed: all.some((c) => c.matched && c.effect === 'allow'),
|
|
121
|
+
layers,
|
|
122
|
+
grants: dedupe(all.filter((c) => c.matched).flatMap((c) => c.via ?? [])),
|
|
123
|
+
held: dedupe(all.flatMap((c) => c.via ?? []).filter((v) => v.kind !== 'member_permission')),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ── decision log ──
|
|
127
|
+
function logDecision(e, d) {
|
|
128
|
+
(0, authz_state_1.st)(e).decisions.insert({ ...d, policyVersion: d.policyVersion ?? {}, at: e.clock.now() });
|
|
129
|
+
}
|
|
130
|
+
function recordRbacCheck(e, i, c) {
|
|
131
|
+
logDecision(e, {
|
|
132
|
+
source: 'rbac',
|
|
133
|
+
decision: c.allowed ? 'allow' : 'deny',
|
|
134
|
+
principal: `user:${i.userId}`,
|
|
135
|
+
action: i.permission,
|
|
136
|
+
resource: i.organizationId ? `organization:${i.organizationId}` : null,
|
|
137
|
+
input: { user: i.userId, permission: i.permission, organization_id: i.organizationId ?? null },
|
|
138
|
+
result: { kind: 'role', permission: i.permission, organization_id: i.organizationId ?? null, grants: c.grants, held: c.held },
|
|
139
|
+
policyVersion: { kind: 'role' },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Would a recorded role decision still be allowed after these role edits? (Same rule as the real replay.) */
|
|
143
|
+
function replayRoleDecision(result, edits, permission) {
|
|
144
|
+
const editFor = (v) => edits.find((x) => x.key === v.role_key && (v.kind === 'app_role' ? x.type === 'app_role' : x.type === 'org_role' && (!x.organizationId || x.organizationId === v.organization_id)));
|
|
145
|
+
for (const g of result.grants ?? []) {
|
|
146
|
+
if (g.kind === 'member_permission')
|
|
147
|
+
return true;
|
|
148
|
+
const x = editFor(g);
|
|
149
|
+
if (!x || !x.removePermissions.includes(permission) || x.addPermissions.includes(permission))
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
for (const h of result.held ?? [])
|
|
153
|
+
if (editFor(h)?.addPermissions.includes(permission))
|
|
154
|
+
return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/** The pure resolver of the real resource-role engine: roles reach permissions through `includes`; assignments flow down the parent chain unless a type or assignment stops them. */
|
|
158
|
+
function rolePermissions(type, roleKey, seen = new Set()) {
|
|
159
|
+
const out = new Set();
|
|
160
|
+
if (seen.has(roleKey))
|
|
161
|
+
return out;
|
|
162
|
+
seen.add(roleKey);
|
|
163
|
+
const role = type.roles.find((r) => r.key === roleKey);
|
|
164
|
+
if (!role)
|
|
165
|
+
return out;
|
|
166
|
+
for (const p of role.permissions)
|
|
167
|
+
out.add(p);
|
|
168
|
+
for (const inc of role.includes ?? [])
|
|
169
|
+
for (const p of rolePermissions(type, inc, seen))
|
|
170
|
+
out.add(p);
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
const resources = (e) => (0, authz_state_1.genericTable)(e, 'resources', 'resource');
|
|
174
|
+
const typeDef = (e, key) => {
|
|
175
|
+
const t = e.generic.get('resource_types')?.find((r) => r.key === key);
|
|
176
|
+
return { inherit: t?.inherit ?? true, roles: t?.roles ?? [] };
|
|
177
|
+
};
|
|
178
|
+
const findResourceRef = (e, ref, field) => {
|
|
179
|
+
const o = (ref ?? {});
|
|
180
|
+
const row = typeof o.id === 'string' && o.id ? resources(e).get(o.id) : typeof o.type === 'string' && typeof o.external_id === 'string' ? resources(e).find((r) => r.type === o.type && r.external_id === o.external_id) : undefined;
|
|
181
|
+
if (!row)
|
|
182
|
+
throw new errors_1.EmulatorError('NOT_FOUND', `Unknown resource in ${field}.`);
|
|
183
|
+
return row;
|
|
184
|
+
};
|
|
185
|
+
exports.findResourceRef = findResourceRef;
|
|
186
|
+
const ancestorsOf = (e, startId) => {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (let id = startId, n = 0; id && n < 16; n++) {
|
|
189
|
+
const r = resources(e).get(id);
|
|
190
|
+
if (!r || out.some((x) => x.id === r.id))
|
|
191
|
+
break;
|
|
192
|
+
out.push(r);
|
|
193
|
+
id = r.parent_id;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
};
|
|
197
|
+
exports.ancestorsOf = ancestorsOf;
|
|
198
|
+
function parseSubject(raw) {
|
|
199
|
+
const o = (raw ?? {});
|
|
200
|
+
if (!['user', 'group', 'org'].includes(o.type))
|
|
201
|
+
throw bad('subject.type must be user, group or org.', 'subject');
|
|
202
|
+
if (typeof o.id !== 'string' || !o.id.trim() || o.id.length > 500)
|
|
203
|
+
throw bad('subject.id is required (at most 500 characters).', 'subject.id');
|
|
204
|
+
return { type: o.type, id: o.id };
|
|
205
|
+
}
|
|
206
|
+
/** The subject itself, plus (for a user) their organizations and groups, plus any groups the caller names. */
|
|
207
|
+
function expandSubjects(e, subject, groups) {
|
|
208
|
+
const out = new Set([`${subject.type}:${subject.id}`]);
|
|
209
|
+
if (Array.isArray(groups))
|
|
210
|
+
for (const g of groups)
|
|
211
|
+
if (typeof g === 'string')
|
|
212
|
+
out.add(`group:${g}`);
|
|
213
|
+
if (subject.type === 'user') {
|
|
214
|
+
for (const m of e.db.memberships.filter((x) => x.user_id === subject.id))
|
|
215
|
+
out.add(`org:${m.organization_id}`);
|
|
216
|
+
for (const k of (0, authz_state_1.effectiveGroupKeys)(e, subject.id))
|
|
217
|
+
out.add(`group:${k}`);
|
|
218
|
+
}
|
|
219
|
+
return [...out];
|
|
220
|
+
}
|
|
221
|
+
function grantsOn(e, resource, subjects, permission) {
|
|
222
|
+
const held = new Set(subjects);
|
|
223
|
+
const chain = [resource, ...(0, exports.ancestorsOf)(e, resource.parent_id)];
|
|
224
|
+
const assignments = (0, authz_state_1.genericTable)(e, 'resource_role_assignments', 'resourcerole');
|
|
225
|
+
const grants = [];
|
|
226
|
+
for (let i = 0; i < chain.length; i++) {
|
|
227
|
+
const link = chain[i];
|
|
228
|
+
const type = typeDef(e, link.type);
|
|
229
|
+
for (const a of assignments.filter((x) => x.resource_id === link.id)) {
|
|
230
|
+
const subject = `${a.subject.type}:${a.subject.id}`;
|
|
231
|
+
if (!held.has(subject) || (i > 0 && !a.inherit))
|
|
232
|
+
continue;
|
|
233
|
+
const perms = rolePermissions(type, a.role);
|
|
234
|
+
if (perms.has(permission) || perms.has('*'))
|
|
235
|
+
grants.push({ resourceId: link.id, role: a.role, subject, inherited: i > 0 });
|
|
236
|
+
}
|
|
237
|
+
if (!type.inherit)
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
return grants;
|
|
241
|
+
}
|
|
242
|
+
const need = (v, field, max = 200) => {
|
|
243
|
+
if (typeof v !== 'string' || !v.trim() || v.length > max)
|
|
244
|
+
throw bad(`${field} is required (at most ${max} characters).`, field);
|
|
245
|
+
return v;
|
|
246
|
+
};
|
|
247
|
+
function checkResource(e, body) {
|
|
248
|
+
const resource = (0, exports.findResourceRef)(e, body.resource, 'resource');
|
|
249
|
+
const permission = need(body.permission, 'permission');
|
|
250
|
+
const grants = grantsOn(e, resource, expandSubjects(e, parseSubject(body.subject), body.groups), permission);
|
|
251
|
+
return { allowed: grants.length > 0, grants, resource };
|
|
252
|
+
}
|
|
253
|
+
function accessibleResources(e, q) {
|
|
254
|
+
const subjects = expandSubjects(e, q.subject, q.groups);
|
|
255
|
+
return resources(e).all().filter((r) => (!q.type || r.type === q.type) && grantsOn(e, r, subjects, q.permission).length > 0).reverse();
|
|
256
|
+
}
|
|
257
|
+
const grantView = (g) => ({ resource_id: g.resourceId, role: g.role, subject: g.subject, inherited: g.inherited });
|
|
258
|
+
exports.grantView = grantView;
|
|
259
|
+
function recordResourceCheck(e, body, r) {
|
|
260
|
+
const s = (body.subject ?? {});
|
|
261
|
+
const principal = typeof s.type === 'string' && typeof s.id === 'string' ? `${s.type}:${s.id}` : null;
|
|
262
|
+
logDecision(e, {
|
|
263
|
+
source: 'rbac',
|
|
264
|
+
decision: r.allowed ? 'allow' : 'deny',
|
|
265
|
+
principal,
|
|
266
|
+
action: typeof body.permission === 'string' ? body.permission : null,
|
|
267
|
+
resource: `${r.resource.type ?? 'resource'}:${r.resource.id}`,
|
|
268
|
+
input: { subject: body.subject, permission: body.permission, resource_id: r.resource.id },
|
|
269
|
+
result: { kind: 'resource_role', grants: r.grants.map(exports.grantView) },
|
|
270
|
+
policyVersion: { kind: 'resource_role' },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function resourceRolesLayer(e, i) {
|
|
274
|
+
const rs = i.resource;
|
|
275
|
+
const ref = rs.resourceId ? { id: rs.resourceId } : rs.type && rs.externalId ? { type: rs.type, external_id: rs.externalId } : null;
|
|
276
|
+
if (!ref)
|
|
277
|
+
return skipped('Name the resource by resource_id, or by type and external_id.');
|
|
278
|
+
let r;
|
|
279
|
+
try {
|
|
280
|
+
r = checkResource(e, { resource: ref, permission: i.action, subject: { type: 'user', id: i.userId }, groups: i.groups });
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (err instanceof errors_1.EmulatorError && err.code === 'NOT_FOUND')
|
|
284
|
+
return skipped('Unknown resource.');
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
const out = r.grants.map((g) => ({ layer: 'resource_roles', effect: 'allow', matched: true, rule: { type: 'resource_role', id: g.resourceId, name: g.role }, reason: `Role "${g.role}" on ${g.inherited ? 'ancestor ' : ''}resource ${g.resourceId} is held through ${g.subject}.`, evidence: [g.subject] }));
|
|
288
|
+
if (!out.length)
|
|
289
|
+
out.push({ layer: 'resource_roles', effect: 'allow', matched: false, rule: { type: 'resource', id: r.resource.id, name: r.resource.name ?? null }, reason: `No role assigned to the user, their groups or organizations on this resource (or its ancestors) grants ${i.action}.` });
|
|
290
|
+
return done(out);
|
|
291
|
+
}
|
|
292
|
+
/** The enabled stored policies, parsed. A policy whose source does not parse is skipped, never fail-open or fail-closed. */
|
|
293
|
+
function loadPdp(e, policyIds) {
|
|
294
|
+
const out = { policies: [], owners: [] };
|
|
295
|
+
for (const row of [...(e.generic.get('abac_policies')?.all() ?? [])].reverse()) {
|
|
296
|
+
if (row.enabled === false || typeof row.source !== 'string' || (policyIds && !policyIds.includes(row.id)))
|
|
297
|
+
continue;
|
|
298
|
+
try {
|
|
299
|
+
for (const p of (0, cedar_1.parseCedar)(row.source))
|
|
300
|
+
(out.policies.push(p), out.owners.push(row.name));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
/* skipped like the real loader */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Enabled template links, filled in; a link whose template is gone or no longer fits is skipped, never evaluated half-filled.
|
|
307
|
+
if (!policyIds) {
|
|
308
|
+
for (const l of linkedPolicies(e))
|
|
309
|
+
(out.policies.push(...l.policies), out.owners.push(...l.policies.map(() => `link ${l.name}`)));
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
for (const l of linkedPolicies(e).filter((x) => policyIds.includes(x.id)))
|
|
313
|
+
(out.policies.push(...l.policies), out.owners.push(...l.policies.map(() => `link ${l.name}`)));
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
/** The enabled `abac_policy_links`, instantiated from their templates. */
|
|
318
|
+
function linkedPolicies(e) {
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const link of [...(e.generic.get('abac_policy_links')?.all() ?? [])].reverse()) {
|
|
321
|
+
const t = e.generic.get('abac_templates')?.get(link.template_id);
|
|
322
|
+
if (link.enabled === false || !t || typeof t.source !== 'string')
|
|
323
|
+
continue;
|
|
324
|
+
try {
|
|
325
|
+
out.push({ id: link.id, name: link.name, policies: (0, cedar_1.linkCedarTemplate)((0, cedar_1.parseCedarTemplate)(t.source), { principal: link.principal ?? undefined, resource: link.resource ?? undefined }) });
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* skipped */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
/** The stored pdp with candidate edits applied: removed and replaced names dropped, added and replacement sources appended. */
|
|
334
|
+
function candidatePdp(cur, c) {
|
|
335
|
+
const drop = new Set([...c.remove, ...c.replace.map((r) => r.name)]);
|
|
336
|
+
const out = { policies: [], owners: [] };
|
|
337
|
+
cur.policies.forEach((p, i) => {
|
|
338
|
+
if (!drop.has(cur.owners[i]))
|
|
339
|
+
(out.policies.push(p), out.owners.push(cur.owners[i]));
|
|
340
|
+
});
|
|
341
|
+
for (const n of [...c.add, ...c.replace])
|
|
342
|
+
for (const p of (0, cedar_1.parseCedar)(n.source))
|
|
343
|
+
(out.policies.push(p), out.owners.push(n.name));
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
const abacDecide = (pdp, req) => (0, cedar_1.decideCedar)((0, cedar_1.traceCedar)(pdp.policies, req));
|
|
347
|
+
exports.abacDecide = abacDecide;
|
|
348
|
+
function abacLayer(e, i) {
|
|
349
|
+
const rType = i.abac?.resourceType ?? i.resource.type;
|
|
350
|
+
const rId = i.resource.id ?? i.resource.externalId;
|
|
351
|
+
if (!rType || !rId)
|
|
352
|
+
return skipped('ABAC needs resource.type and resource.id.');
|
|
353
|
+
const req = {
|
|
354
|
+
principal: { type: i.abac?.principalType ?? 'User', id: i.userId, ...(i.abac?.principalAttributes ? { attributes: i.abac.principalAttributes } : {}) },
|
|
355
|
+
action: { id: i.action },
|
|
356
|
+
resource: { type: rType, id: rId, ...(i.abac?.resourceAttributes ? { attributes: i.abac.resourceAttributes } : {}) },
|
|
357
|
+
...(i.context ? { context: i.context } : {}),
|
|
358
|
+
};
|
|
359
|
+
const pdp = loadPdp(e);
|
|
360
|
+
if (!pdp.policies.length)
|
|
361
|
+
return { status: 'evaluated', decision: 'no_match', reason: 'The instance has no ABAC policies.', contributions: [] };
|
|
362
|
+
const out = (0, cedar_1.traceCedar)(pdp.policies, req).slice(0, 200).map((o) => ({
|
|
363
|
+
layer: 'abac',
|
|
364
|
+
effect: o.effect === 'permit' ? 'allow' : 'deny',
|
|
365
|
+
matched: o.matched,
|
|
366
|
+
rule: { type: `abac_${o.effect}`, id: String(o.index), name: pdp.owners[o.index] ?? null },
|
|
367
|
+
reason: o.error ? `Policy "${pdp.owners[o.index]}" errored: ${o.error}` : o.matched ? `${o.effect === 'permit' ? 'Permit' : 'Forbid'} policy "${pdp.owners[o.index]}" matched.` : o.scoped === false ? `Policy "${pdp.owners[o.index]}" does not apply to this principal, action and resource.` : `Policy "${pdp.owners[o.index]}" applies but its conditions were not met.`,
|
|
368
|
+
}));
|
|
369
|
+
const errored = (c) => c.reason.includes('errored');
|
|
370
|
+
const kept = [...out.filter((c) => c.matched || errored(c)), ...out.filter((c) => !c.matched && !errored(c)).slice(0, 10)];
|
|
371
|
+
return { status: 'evaluated', decision: decisionOf(kept), contributions: kept };
|
|
372
|
+
}
|
|
373
|
+
// ── access policies ──
|
|
374
|
+
/** Whether an address (IPv4 or IPv6) is inside a CIDR range, or equals a bare address. */
|
|
375
|
+
function ipInCidr(ip, cidr) {
|
|
376
|
+
if (!ip)
|
|
377
|
+
return false;
|
|
378
|
+
try {
|
|
379
|
+
const a = (0, cedar_ext_1.parseIp)(ip);
|
|
380
|
+
const n = (0, cedar_ext_1.parseIp)(cidr);
|
|
381
|
+
if (a.v6 !== n.v6)
|
|
382
|
+
return false;
|
|
383
|
+
const host = BigInt(n.bits - n.prefix);
|
|
384
|
+
return a.addr >> host === n.addr >> host;
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const pathGet = (o, key) => key.split('.').reduce((c, p) => (c && typeof c === 'object' ? c[p] : undefined), o);
|
|
391
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
392
|
+
function attrMatches(a, attrs) {
|
|
393
|
+
const v = pathGet(attrs, a.key);
|
|
394
|
+
switch (a.op) {
|
|
395
|
+
case 'exists':
|
|
396
|
+
return v !== undefined && v !== null;
|
|
397
|
+
case 'eq':
|
|
398
|
+
return v !== undefined && same(v, a.value);
|
|
399
|
+
case 'neq':
|
|
400
|
+
return v !== undefined && !same(v, a.value);
|
|
401
|
+
case 'in':
|
|
402
|
+
return Array.isArray(a.value) && v !== undefined && a.value.some((x) => same(x, v));
|
|
403
|
+
case 'not_in':
|
|
404
|
+
return Array.isArray(a.value) && v !== undefined && !a.value.some((x) => same(x, v));
|
|
405
|
+
case 'contains':
|
|
406
|
+
return Array.isArray(v) ? v.some((x) => same(x, a.value)) : typeof v === 'string' && typeof a.value === 'string' && v.includes(a.value);
|
|
407
|
+
default:
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** Compare dotted versions numerically ("14.2.1" vs "14.10"); null when either is not dotted digits. */
|
|
412
|
+
function compareVersions(a, b) {
|
|
413
|
+
const parse = (v) => (/^\d+(\.\d+)*$/.test(v.trim()) ? v.trim().split('.').map(Number) : null);
|
|
414
|
+
const x = parse(a);
|
|
415
|
+
const y = parse(b);
|
|
416
|
+
if (!x || !y)
|
|
417
|
+
return null;
|
|
418
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
419
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
420
|
+
if (d !== 0)
|
|
421
|
+
return d < 0 ? -1 : 1;
|
|
422
|
+
}
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
const overlaps = (want, have) => !Array.isArray(want) || !want.length || have.some((h) => want.includes(h));
|
|
426
|
+
/** The first condition of a policy that does not hold (with the fact it needs when the emulator lacks it), or null when every condition holds. */
|
|
427
|
+
function failing(c, ctx) {
|
|
428
|
+
const no = (key, unknown = false) => ({ key, unknown });
|
|
429
|
+
if (c.applications?.length && !(ctx.applicationId && c.applications.includes(ctx.applicationId)))
|
|
430
|
+
return no('applications', !ctx.applicationId);
|
|
431
|
+
if (c.userIds?.length && !(ctx.userId && c.userIds.includes(ctx.userId)))
|
|
432
|
+
return no('userIds');
|
|
433
|
+
if (!overlaps(c.groups, ctx.groupIds))
|
|
434
|
+
return no('groups');
|
|
435
|
+
if (!overlaps(c.orgs, ctx.orgIds))
|
|
436
|
+
return no('orgs');
|
|
437
|
+
if (!overlaps(c.roles, ctx.roles))
|
|
438
|
+
return no('roles');
|
|
439
|
+
if (c.networkZones?.include?.length && !overlaps(c.networkZones.include, ctx.zoneIds))
|
|
440
|
+
return no('networkZones', !ctx.hasIp);
|
|
441
|
+
if (c.networkZones?.exclude?.length && overlaps(c.networkZones.exclude, ctx.zoneIds))
|
|
442
|
+
return no('networkZones');
|
|
443
|
+
// A signal the request, the address and the emulator settings all lack is "unknown": the policy is listed as not matched, never as a block.
|
|
444
|
+
if (c.countries?.length && !(ctx.country && c.countries.map((x) => x.toUpperCase()).includes(ctx.country.toUpperCase())))
|
|
445
|
+
return no('countries', !ctx.country);
|
|
446
|
+
if (c.riskLevels?.length && !(ctx.riskLevel && c.riskLevels.includes(ctx.riskLevel)))
|
|
447
|
+
return no('riskLevels', !ctx.riskLevel);
|
|
448
|
+
if (c.signInRiskLevels?.length && !(ctx.riskLevel && c.signInRiskLevels.includes(ctx.riskLevel)))
|
|
449
|
+
return no('signInRiskLevels', !ctx.riskLevel);
|
|
450
|
+
if (c.userRiskLevels?.length && !(ctx.userRiskLevel && c.userRiskLevels.includes(ctx.userRiskLevel)))
|
|
451
|
+
return no('userRiskLevels', !ctx.userRiskLevel);
|
|
452
|
+
if (c.deviceTrust?.length && !(ctx.deviceTrust && c.deviceTrust.includes(ctx.deviceTrust)))
|
|
453
|
+
return no('deviceTrust', !ctx.deviceTrust);
|
|
454
|
+
if (c.authMethods?.length && !overlaps(c.authMethods, ctx.authMethods))
|
|
455
|
+
return no('authMethods', !ctx.authMethods.length);
|
|
456
|
+
if (c.devicePlatforms?.length && !(ctx.devicePlatform && c.devicePlatforms.map((x) => x.toLowerCase()).includes(ctx.devicePlatform.toLowerCase())))
|
|
457
|
+
return no('devicePlatforms', !ctx.devicePlatform);
|
|
458
|
+
if (c.osVersion && (c.osVersion.min || c.osVersion.max)) {
|
|
459
|
+
const lo = c.osVersion.min && ctx.osVersion ? compareVersions(ctx.osVersion, c.osVersion.min) : null;
|
|
460
|
+
const hi = c.osVersion.max && ctx.osVersion ? compareVersions(ctx.osVersion, c.osVersion.max) : null;
|
|
461
|
+
if (!ctx.osVersion || (c.osVersion.min && !((lo ?? -1) >= 0)) || (c.osVersion.max && !((hi ?? 1) <= 0)))
|
|
462
|
+
return no('osVersion', !ctx.osVersion);
|
|
463
|
+
}
|
|
464
|
+
if (c.clientTypes?.length && !(ctx.clientType && c.clientTypes.includes(ctx.clientType)))
|
|
465
|
+
return no('clientTypes', !ctx.clientType);
|
|
466
|
+
if (c.principalIds?.length && !(ctx.principalId && c.principalIds.includes(ctx.principalId)))
|
|
467
|
+
return no('principalIds', !ctx.principalId);
|
|
468
|
+
if (c.timeWindow && !inWindow(c.timeWindow, ctx.now))
|
|
469
|
+
return no('timeWindow');
|
|
470
|
+
if (c.userAttributes?.length && !c.userAttributes.every((a) => attrMatches(a, ctx.userAttributes)))
|
|
471
|
+
return no('userAttributes');
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function inWindow(w, now) {
|
|
475
|
+
const mins = (s) => {
|
|
476
|
+
const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(s ?? '');
|
|
477
|
+
return m ? Number(m[1]) * 60 + Number(m[2]) : null;
|
|
478
|
+
};
|
|
479
|
+
const start = mins(w.start);
|
|
480
|
+
const end = mins(w.end);
|
|
481
|
+
if (start === null || end === null)
|
|
482
|
+
return false;
|
|
483
|
+
let parts;
|
|
484
|
+
try {
|
|
485
|
+
parts = new Intl.DateTimeFormat('en-US', { timeZone: w.timezone ?? 'UTC', weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(now);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const get = (t) => parts.find((p) => p.type === t)?.value ?? '';
|
|
491
|
+
const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(get('weekday'));
|
|
492
|
+
const at = Number(get('hour')) * 60 + Number(get('minute'));
|
|
493
|
+
const dayOk = (d) => !w.days?.length || w.days.includes(d);
|
|
494
|
+
if (start <= end)
|
|
495
|
+
return dayOk(day) && at >= start && at < end;
|
|
496
|
+
return at >= start ? dayOk(day) : dayOk((day + 6) % 7);
|
|
497
|
+
}
|
|
498
|
+
/** Does an address fall in a network zone: a listed range, a country or ASN the emulator knows for it, or an anonymizer flag. */
|
|
499
|
+
function zoneMatches(e, z, ip) {
|
|
500
|
+
if (!ip)
|
|
501
|
+
return false;
|
|
502
|
+
if ((z.cidrs ?? []).some((c) => ipInCidr(ip, c)))
|
|
503
|
+
return true;
|
|
504
|
+
const f = (0, access_signals_1.ipFacts)(e, ip);
|
|
505
|
+
if (f.country && (z.countries ?? []).map((c) => String(c).toUpperCase()).includes(f.country.toUpperCase()))
|
|
506
|
+
return true;
|
|
507
|
+
if (f.asn !== null && (z.asns ?? []).includes(f.asn))
|
|
508
|
+
return true;
|
|
509
|
+
return (z.anonymizers ?? []).some((k) => (k === 'tor' && f.tor) || (k === 'datacenter' && f.datacenter));
|
|
510
|
+
}
|
|
511
|
+
/** Evaluate access policies like the real evaluator: any matching block wins, step-up requirements are unioned; report-only never enforces. */
|
|
512
|
+
function evalSet(sorted, ctx) {
|
|
513
|
+
const out = { decision: 'allow', matched: [], steps: [] };
|
|
514
|
+
const add = (s) => out.steps.includes(s) || out.steps.push(s);
|
|
515
|
+
for (const p of sorted) {
|
|
516
|
+
if (failing(p.conditions ?? {}, ctx))
|
|
517
|
+
continue;
|
|
518
|
+
out.matched.push(p.id);
|
|
519
|
+
for (const g of p.grants ?? []) {
|
|
520
|
+
if (g.type === 'block')
|
|
521
|
+
out.blockedBy ??= p.id;
|
|
522
|
+
else if (g.type === 'require_mfa')
|
|
523
|
+
add('mfa');
|
|
524
|
+
else if (g.type === 'require_acr')
|
|
525
|
+
add(`acr:${g.value}`);
|
|
526
|
+
else if (g.type === 'require_policy_acceptance')
|
|
527
|
+
add(`policy_acceptance:${g.value}`);
|
|
528
|
+
else if (g.type === 'require_action')
|
|
529
|
+
add(`action:${g.value}`);
|
|
530
|
+
else if (g.type === 'require_password_change')
|
|
531
|
+
add('action:password_change');
|
|
532
|
+
else if (g.type === 'require_auth_strength')
|
|
533
|
+
add(`auth_strength:${g.value}`);
|
|
534
|
+
else if (g.type === 'sign_in_frequency')
|
|
535
|
+
add('sign_in_frequency');
|
|
536
|
+
else if (g.type === 'max_session_age')
|
|
537
|
+
add('max_session_age');
|
|
538
|
+
else if (g.type === 'require_device_bound_session')
|
|
539
|
+
add('device_bound_session');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
out.decision = out.blockedBy ? 'block' : out.steps.length ? 'step_up' : 'allow';
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
function accessPoliciesLayer(e, i) {
|
|
546
|
+
const rows = [...(e.generic.get('access_policies')?.all() ?? [])].reverse().filter((r) => r.enabled !== false);
|
|
547
|
+
if (!rows.length)
|
|
548
|
+
return { status: 'evaluated', decision: 'no_match', reason: 'The instance has no access policies.', contributions: [] };
|
|
549
|
+
const user = e.db.users.get(i.userId);
|
|
550
|
+
const mems = e.db.memberships.filter((m) => m.user_id === i.userId);
|
|
551
|
+
const zones = (e.generic.get('network_zones')?.all() ?? []).filter((z) => zoneMatches(e, z, i.access?.ip));
|
|
552
|
+
const sig = (0, access_signals_1.resolveSignals)(e, { userId: i.userId, ip: i.access?.ip, explicit: i.access?.signals });
|
|
553
|
+
const ctx = {
|
|
554
|
+
stage: 'authorize',
|
|
555
|
+
applicationId: i.access?.applicationId ?? null,
|
|
556
|
+
userId: i.userId,
|
|
557
|
+
groupIds: (0, authz_state_1.effectiveGroupKeys)(e, i.userId),
|
|
558
|
+
orgIds: [...new Set(mems.map((m) => m.organization_id))],
|
|
559
|
+
roles: [...new Set(mems.map((m) => m.role))],
|
|
560
|
+
zoneIds: zones.map((z) => z.id),
|
|
561
|
+
hasIp: !!i.access?.ip,
|
|
562
|
+
country: sig.country ?? null,
|
|
563
|
+
riskLevel: sig.riskLevel ?? null,
|
|
564
|
+
deviceTrust: sig.deviceTrust ?? null,
|
|
565
|
+
authMethods: sig.authMethods ?? [],
|
|
566
|
+
devicePlatform: sig.devicePlatform ?? null,
|
|
567
|
+
osVersion: sig.osVersion ?? null,
|
|
568
|
+
clientType: sig.clientType ?? null,
|
|
569
|
+
userRiskLevel: sig.userRiskLevel ?? null,
|
|
570
|
+
userAttributes: user?.public_metadata ?? {},
|
|
571
|
+
now: new Date(e.clock.now()),
|
|
572
|
+
};
|
|
573
|
+
// Enabled policies for this stage and target; lower priority number first.
|
|
574
|
+
const live = rows
|
|
575
|
+
.filter((p) => (!p.stages?.length || p.stages.includes('authorize')) && (p.targets?.length ? p.targets : ['user']).includes('user'))
|
|
576
|
+
.sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
577
|
+
const enforced = live.filter((p) => p.mode !== 'report_only');
|
|
578
|
+
const report = live.filter((p) => p.mode === 'report_only');
|
|
579
|
+
const dec = evalSet(enforced, ctx);
|
|
580
|
+
const would = report.length ? evalSet(live, ctx) : null;
|
|
581
|
+
const names = new Map(rows.map((r) => [r.id, r.name]));
|
|
582
|
+
// The emulator never has a second factor or auth method for an explain request, so every step-up requirement is unmet.
|
|
583
|
+
const unmet = dec.decision === 'step_up' ? dec.steps : [];
|
|
584
|
+
const out = dec.matched.map((id) => {
|
|
585
|
+
const blocks = dec.decision === 'block' && dec.blockedBy === id;
|
|
586
|
+
const steps = dec.decision === 'step_up' && unmet.length > 0;
|
|
587
|
+
return {
|
|
588
|
+
layer: 'access_policies',
|
|
589
|
+
effect: blocks ? 'deny' : steps ? 'step_up' : 'none',
|
|
590
|
+
matched: true,
|
|
591
|
+
rule: { type: 'access_policy', id, name: names.get(id) ?? null },
|
|
592
|
+
reason: blocks ? `Access policy "${names.get(id)}" blocks this request.` : steps ? `Access policy "${names.get(id)}" applies; the request has not yet met: ${unmet.join(', ')}.` : `Access policy "${names.get(id)}" applies and imposes nothing more.`,
|
|
593
|
+
evidence: steps ? unmet : undefined,
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
if (would && would.decision !== 'allow' && would.matched.some((id) => report.some((r) => r.id === id))) {
|
|
597
|
+
for (const id of would.matched.filter((m) => report.some((r) => r.id === m))) {
|
|
598
|
+
out.push({ layer: 'access_policies', effect: 'none', matched: false, rule: { type: 'access_policy_report_only', id, name: names.get(id) ?? null }, reason: `Report-only policy "${names.get(id)}" would ${would.decision === 'block' ? 'block' : 'require a step up for'} this request, but is not enforced.` });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Policies that could not match because the emulator lacks the fact (country, risk, device, network data): listed as not matched, never a block.
|
|
602
|
+
for (const p of live) {
|
|
603
|
+
const f = failing(p.conditions ?? {}, ctx);
|
|
604
|
+
if (f?.unknown)
|
|
605
|
+
out.push({ layer: 'access_policies', effect: 'none', matched: false, rule: { type: 'access_policy', id: p.id, name: p.name }, reason: `Access policy "${p.name}" needs a ${f.key} signal the emulator does not have, so it is reported as not matched.` });
|
|
606
|
+
}
|
|
607
|
+
return { status: 'evaluated', decision: decisionOf(out), ...(out.length ? {} : { reason: 'No access policy applies to this request.' }), contributions: out };
|
|
608
|
+
}
|
|
609
|
+
// ── FGA ──
|
|
610
|
+
/** Best-effort model lookup: does the store's latest model declare this relation on this type? Unknown formats are given the benefit of the doubt. */
|
|
611
|
+
function modelHasRelation(model, type, relation) {
|
|
612
|
+
if (typeof model === 'string') {
|
|
613
|
+
const t = new RegExp(`(^|\\n)\\s*type\\s+${type}\\b([\\s\\S]*?)(?=\\n\\s*type\\s|$)`).exec(model);
|
|
614
|
+
if (!t)
|
|
615
|
+
return !/(^|\n)\s*type\s/.test(model);
|
|
616
|
+
return new RegExp(`define\\s+${relation}\\s*:`).test(t[2] ?? '');
|
|
617
|
+
}
|
|
618
|
+
const defs = model?.type_definitions;
|
|
619
|
+
if (!Array.isArray(defs))
|
|
620
|
+
return true;
|
|
621
|
+
const d = defs.find((x) => x.type === type);
|
|
622
|
+
return !!d && !!(d.relations && relation in d.relations);
|
|
623
|
+
}
|
|
624
|
+
/** The newest model of a store (tables list newest first). */
|
|
625
|
+
const latestModel = (e, storeId) => e.generic.get('fga/stores/authorization-models')?.filter((m) => m._parent === storeId)[0];
|
|
626
|
+
exports.latestModel = latestModel;
|
|
627
|
+
/**
|
|
628
|
+
* Relationship check over the tuples the emulator holds: a direct tuple, a type wildcard (`user:*`), or membership in a
|
|
629
|
+
* userset (`group:eng#member`) through stored tuples, nested up to eight levels. Contextual tuples are honoured.
|
|
630
|
+
* shortcut: no model rewrites (computed or inherited relations such as `viewer from parent`) and conditional tuples never match;
|
|
631
|
+
* upgrade by running the real engine if your model depends on them.
|
|
632
|
+
*/
|
|
633
|
+
function fgaCheck(e, storeId, req, overlay) {
|
|
634
|
+
const del = new Set((overlay?.deletes ?? []).map((t) => `${t.user}|${t.relation}|${t.object}`));
|
|
635
|
+
const tuples = [...(0, authz_state_1.storeTuples)(e, storeId).filter((t) => !del.has(`${t.user}|${t.relation}|${t.object}`)), ...(overlay?.writes ?? []), ...(req.contextual ?? [])];
|
|
636
|
+
// A model written through the authorization-models route is enforced with its rewrites and conditions; a model seeded as plain text keeps the direct, wildcard and userset matching below.
|
|
637
|
+
const compiled = (0, fga_engine_1.compileStored)((req.model ?? (0, exports.latestModel)(e, storeId))?.model);
|
|
638
|
+
if (compiled) {
|
|
639
|
+
try {
|
|
640
|
+
return new fga_engine_1.Resolver(compiled, tuples, req.context).check((0, fga_engine_1.parseUser)(req.user), req.object, req.relation);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const userType = req.user.split(':')[0];
|
|
647
|
+
const seen = new Set();
|
|
648
|
+
const holds = (user, relation, object, depth) => {
|
|
649
|
+
const key = `${relation}|${object}`;
|
|
650
|
+
if (depth > 8 || seen.has(key + user))
|
|
651
|
+
return false;
|
|
652
|
+
seen.add(key + user);
|
|
653
|
+
for (const t of tuples) {
|
|
654
|
+
if (t.relation !== relation || t.object !== object || t.condition)
|
|
655
|
+
continue;
|
|
656
|
+
if (t.user === user || t.user === `${userType}:*`)
|
|
657
|
+
return true;
|
|
658
|
+
const hash = t.user.indexOf('#');
|
|
659
|
+
if (hash > 0 && holds(user, t.user.slice(hash + 1), t.user.slice(0, hash), depth + 1))
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
663
|
+
};
|
|
664
|
+
return holds(req.user, req.relation, req.object, 0);
|
|
665
|
+
}
|
|
666
|
+
function fgaLayer(e, i) {
|
|
667
|
+
const type = i.resource.type;
|
|
668
|
+
const id = i.resource.id ?? i.resource.externalId;
|
|
669
|
+
if (!type || !id)
|
|
670
|
+
return skipped('FGA needs resource.type and resource.id.');
|
|
671
|
+
const storeId = i.fga?.storeId ?? (0, authz_state_1.st)(e).config.store_id;
|
|
672
|
+
if (!storeId)
|
|
673
|
+
return skipped('No FGA store: pass fga.store_id or configure managed sync.');
|
|
674
|
+
if (!(0, authz_state_1.storeExists)(e, storeId))
|
|
675
|
+
return skipped('Unknown FGA store.');
|
|
676
|
+
const model = (0, exports.latestModel)(e, storeId);
|
|
677
|
+
if (!model)
|
|
678
|
+
return skipped('The FGA store has no authorization model.');
|
|
679
|
+
const relation = i.fga?.relation ?? i.action;
|
|
680
|
+
const userType = i.fga?.userType ?? 'user';
|
|
681
|
+
const user = `${userType}:${i.userId}`;
|
|
682
|
+
const object = `${type}:${id}`;
|
|
683
|
+
if (!modelHasRelation(model.model, type, relation))
|
|
684
|
+
return skipped(`The model has no relation ${type}#${relation}.`);
|
|
685
|
+
const allowed = fgaCheck(e, storeId, { user, relation, object });
|
|
686
|
+
const evidence = (0, authz_state_1.storeTuples)(e, storeId)
|
|
687
|
+
.filter((t) => t.object === object && t.relation === relation && (t.user === user || t.user === `${userType}:*` || t.user.includes('#')))
|
|
688
|
+
.slice(0, 25)
|
|
689
|
+
.map((t) => `${t.user}|${t.relation}|${t.object}`);
|
|
690
|
+
return done([{ layer: 'fga', effect: 'allow', matched: allowed, rule: { type: 'fga_check', id: `${object}#${relation}`, name: storeId }, reason: allowed ? `${user} has ${relation} on ${object} (model ${model.id}).` : `${user} has no ${relation} on ${object} in model ${model.id}.`, evidence }]);
|
|
691
|
+
}
|
|
692
|
+
// ── the answer ──
|
|
693
|
+
function combine(layers) {
|
|
694
|
+
const all = Object.values(layers).flatMap((l) => l?.contributions ?? []).filter((c) => c.matched);
|
|
695
|
+
const denies = all.filter((c) => c.effect === 'deny');
|
|
696
|
+
if (denies.length)
|
|
697
|
+
return { decision: 'deny', deciding: denies, summary: `Denied: ${denies.map((d) => `${d.rule.type} "${d.rule.name ?? d.rule.id}"`).join(', ')} denies it.` };
|
|
698
|
+
const allows = all.filter((c) => c.effect === 'allow');
|
|
699
|
+
if (!allows.length)
|
|
700
|
+
return { decision: 'deny', deciding: [], summary: 'Denied by default: no rule in any layer grants it.' };
|
|
701
|
+
const steps = all.filter((c) => c.effect === 'step_up');
|
|
702
|
+
if (steps.length)
|
|
703
|
+
return { decision: 'step_up', deciding: [...allows, ...steps], summary: `Allowed once the request satisfies: ${[...new Set(steps.flatMap((s) => s.evidence ?? []))].join(', ') || 'a step up'}.` };
|
|
704
|
+
return { decision: 'allow', deciding: allows, summary: `Allowed: ${allows.length} rule${allows.length === 1 ? '' : 's'} grant it.` };
|
|
705
|
+
}
|
|
706
|
+
function explainAccess(e, i) {
|
|
707
|
+
const want = new Set(i.layers?.length ? i.layers : exports.LAYERS);
|
|
708
|
+
const layers = {};
|
|
709
|
+
const run = {
|
|
710
|
+
rbac: () => rbacLayer(e, i),
|
|
711
|
+
org_roles: exports.orgRolesLayer,
|
|
712
|
+
groups: () => groupsLayer(e, i),
|
|
713
|
+
resource_roles: () => resourceRolesLayer(e, i),
|
|
714
|
+
abac: () => abacLayer(e, i),
|
|
715
|
+
access_policies: () => accessPoliciesLayer(e, i),
|
|
716
|
+
fga: () => fgaLayer(e, i),
|
|
717
|
+
};
|
|
718
|
+
for (const layer of exports.LAYERS) {
|
|
719
|
+
if (!want.has(layer))
|
|
720
|
+
continue;
|
|
721
|
+
try {
|
|
722
|
+
layers[layer] = run[layer]();
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
layers[layer] = { status: 'error', decision: 'no_match', reason: err instanceof Error ? err.message.slice(0, 200) : 'The layer failed.', contributions: [] };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const { decision, deciding, summary } = combine(layers);
|
|
729
|
+
return { object: 'authz_explanation', decision, combination: 'deny_overrides', summary, subject: { type: 'user', id: i.userId }, action: i.action, resource: { ...i.resource }, layers, deciding, contributions: Object.values(layers).flatMap((l) => l.contributions ?? []) };
|
|
730
|
+
}
|
|
731
|
+
/** Parse and validate an explain body (snake_case on the wire). `permission` is accepted as an alias of `action`. */
|
|
732
|
+
function parseExplainBody(body) {
|
|
733
|
+
const obj = (v, field) => {
|
|
734
|
+
if (v === undefined || v === null)
|
|
735
|
+
return {};
|
|
736
|
+
if (typeof v !== 'object' || Array.isArray(v))
|
|
737
|
+
throw bad(`${field} must be an object.`, field);
|
|
738
|
+
return v;
|
|
739
|
+
};
|
|
740
|
+
const str = (v, field, max = 500) => {
|
|
741
|
+
if (v === undefined || v === null)
|
|
742
|
+
return undefined;
|
|
743
|
+
if (typeof v !== 'string' || v.length === 0 || v.length > max)
|
|
744
|
+
throw bad(`${field} must be a non-empty string.`, field);
|
|
745
|
+
return v;
|
|
746
|
+
};
|
|
747
|
+
const subject = obj(body.subject, 'subject');
|
|
748
|
+
if (subject.type !== undefined && subject.type !== 'user')
|
|
749
|
+
throw bad('subject.type must be user.', 'subject.type');
|
|
750
|
+
const userId = str(subject.id, 'subject.id');
|
|
751
|
+
if (!userId)
|
|
752
|
+
throw bad('subject.id is required.', 'subject.id');
|
|
753
|
+
const action = str(body.action ?? body.permission, 'action');
|
|
754
|
+
if (!action)
|
|
755
|
+
throw bad('action is required.', 'action');
|
|
756
|
+
const r = obj(body.resource, 'resource');
|
|
757
|
+
const abac = obj(body.abac, 'abac');
|
|
758
|
+
const fga = obj(body.fga, 'fga');
|
|
759
|
+
const access = obj(body.access, 'access');
|
|
760
|
+
let layers;
|
|
761
|
+
if (body.layers !== undefined) {
|
|
762
|
+
if (!Array.isArray(body.layers) || body.layers.some((l) => !exports.LAYERS.includes(l)))
|
|
763
|
+
throw bad(`layers must be a list of: ${exports.LAYERS.join(', ')}.`, 'layers');
|
|
764
|
+
layers = body.layers;
|
|
765
|
+
}
|
|
766
|
+
let groups;
|
|
767
|
+
if (body.groups !== undefined) {
|
|
768
|
+
if (!Array.isArray(body.groups) || body.groups.some((g) => typeof g !== 'string'))
|
|
769
|
+
throw bad('groups must be a list of strings.', 'groups');
|
|
770
|
+
groups = body.groups;
|
|
771
|
+
}
|
|
772
|
+
const attrs = (v, field) => (v === undefined ? undefined : obj(v, field));
|
|
773
|
+
return {
|
|
774
|
+
userId,
|
|
775
|
+
action,
|
|
776
|
+
// The organization may ride on resource.organization_id or, as before, on a top-level organization_id.
|
|
777
|
+
resource: { type: str(r.type, 'resource.type'), id: str(r.id, 'resource.id'), resourceId: str(r.resource_id, 'resource.resource_id'), externalId: str(r.external_id, 'resource.external_id'), organizationId: str(r.organization_id ?? body.organization_id, 'resource.organization_id') },
|
|
778
|
+
groups,
|
|
779
|
+
context: body.context === undefined ? undefined : obj(body.context, 'context'),
|
|
780
|
+
abac: { principalType: str(abac.principal_type, 'abac.principal_type', 100), resourceType: str(abac.resource_type, 'abac.resource_type', 100), principalAttributes: attrs(abac.principal_attributes, 'abac.principal_attributes'), resourceAttributes: attrs(abac.resource_attributes, 'abac.resource_attributes') },
|
|
781
|
+
fga: { storeId: str(fga.store_id, 'fga.store_id'), relation: str(fga.relation, 'fga.relation', 100), userType: str(fga.user_type, 'fga.user_type', 100) },
|
|
782
|
+
access: { applicationId: str(access.application_id, 'access.application_id'), ip: str(access.ip, 'access.ip', 100), signals: (0, access_signals_1.readSignals)(access, (m, p) => { throw bad(m, p); }, 'access.') },
|
|
783
|
+
layers,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
//# sourceMappingURL=authz-layers.js.map
|