@atlasauth/emulator-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-html.d.ts +57 -0
- package/dist/custom-html.js +491 -0
- package/dist/custom-html.js.map +1 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +4 -0
- package/dist/engine.js.map +1 -1
- package/dist/error-catalog.js +8 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/event-gate.js +60 -2
- package/dist/event-gate.js.map +1 -1
- package/dist/generic.js +5 -1
- package/dist/generic.js.map +1 -1
- package/dist/handlers/access-signals.d.ts +48 -0
- package/dist/handlers/access-signals.js +76 -0
- package/dist/handlers/access-signals.js.map +1 -0
- package/dist/handlers/approvals.js +54 -1
- package/dist/handlers/approvals.js.map +1 -1
- package/dist/handlers/authz-data.d.ts +2 -0
- package/dist/handlers/authz-data.js +627 -0
- package/dist/handlers/authz-data.js.map +1 -0
- package/dist/handlers/authz-layers.d.ts +229 -0
- package/dist/handlers/authz-layers.js +786 -0
- package/dist/handlers/authz-layers.js.map +1 -0
- package/dist/handlers/authz-state.d.ts +80 -0
- package/dist/handlers/authz-state.js +175 -0
- package/dist/handlers/authz-state.js.map +1 -0
- package/dist/handlers/authz.js +240 -177
- package/dist/handlers/authz.js.map +1 -1
- package/dist/handlers/billing.d.ts +99 -1
- package/dist/handlers/billing.js +556 -77
- package/dist/handlers/billing.js.map +1 -1
- package/dist/handlers/cedar-ext.d.ts +35 -0
- package/dist/handlers/cedar-ext.js +174 -0
- package/dist/handlers/cedar-ext.js.map +1 -0
- package/dist/handlers/cedar.d.ts +146 -0
- package/dist/handlers/cedar.js +689 -0
- package/dist/handlers/cedar.js.map +1 -0
- package/dist/handlers/connections.d.ts +4 -1
- package/dist/handlers/connections.js +267 -38
- package/dist/handlers/connections.js.map +1 -1
- package/dist/handlers/control.js +6 -1
- package/dist/handlers/control.js.map +1 -1
- package/dist/handlers/device.js +18 -1
- package/dist/handlers/device.js.map +1 -1
- package/dist/handlers/emu3-misc.d.ts +38 -0
- package/dist/handlers/emu3-misc.js +1056 -0
- package/dist/handlers/emu3-misc.js.map +1 -0
- package/dist/handlers/emu4-actions-access.d.ts +46 -0
- package/dist/handlers/emu4-actions-access.js +107 -0
- package/dist/handlers/emu4-actions-access.js.map +1 -0
- package/dist/handlers/emu4-actions-catalog-extra.d.ts +3 -0
- package/dist/handlers/emu4-actions-catalog-extra.js +204 -0
- package/dist/handlers/emu4-actions-catalog-extra.js.map +1 -0
- package/dist/handlers/emu4-actions-catalog.d.ts +3 -0
- package/dist/handlers/emu4-actions-catalog.js +143 -0
- package/dist/handlers/emu4-actions-catalog.js.map +1 -0
- package/dist/handlers/emu4-actions-def.d.ts +50 -0
- package/dist/handlers/emu4-actions-def.js +250 -0
- package/dist/handlers/emu4-actions-def.js.map +1 -0
- package/dist/handlers/emu4-actions-sandbox.d.ts +36 -0
- package/dist/handlers/emu4-actions-sandbox.js +318 -0
- package/dist/handlers/emu4-actions-sandbox.js.map +1 -0
- package/dist/handlers/emu4-actions-typegen.d.ts +5 -0
- package/dist/handlers/emu4-actions-typegen.js +73 -0
- package/dist/handlers/emu4-actions-typegen.js.map +1 -0
- package/dist/handlers/emu4-actions.d.ts +2 -0
- package/dist/handlers/emu4-actions.js +506 -0
- package/dist/handlers/emu4-actions.js.map +1 -0
- package/dist/handlers/emu4-authz-trust.d.ts +40 -0
- package/dist/handlers/emu4-authz-trust.js +488 -0
- package/dist/handlers/emu4-authz-trust.js.map +1 -0
- package/dist/handlers/emu4-authz.d.ts +2 -0
- package/dist/handlers/emu4-authz.js +752 -0
- package/dist/handlers/emu4-authz.js.map +1 -0
- package/dist/handlers/emu4-billing-core.d.ts +23 -0
- package/dist/handlers/emu4-billing-core.js +22 -0
- package/dist/handlers/emu4-billing-core.js.map +1 -0
- package/dist/handlers/emu4-billing-x.d.ts +2 -0
- package/dist/handlers/emu4-billing-x.js +489 -0
- package/dist/handlers/emu4-billing-x.js.map +1 -0
- package/dist/handlers/emu4-billing.d.ts +14 -0
- package/dist/handlers/emu4-billing.js +846 -0
- package/dist/handlers/emu4-billing.js.map +1 -0
- package/dist/handlers/emu4-clones.d.ts +2 -0
- package/dist/handlers/emu4-clones.js +322 -0
- package/dist/handlers/emu4-clones.js.map +1 -0
- package/dist/handlers/emu4-compliance.d.ts +8 -0
- package/dist/handlers/emu4-compliance.js +39 -0
- package/dist/handlers/emu4-compliance.js.map +1 -0
- package/dist/handlers/emu4-config.d.ts +14 -0
- package/dist/handlers/emu4-config.js +148 -0
- package/dist/handlers/emu4-config.js.map +1 -0
- package/dist/handlers/emu4-cookies.d.ts +8 -0
- package/dist/handlers/emu4-cookies.js +248 -0
- package/dist/handlers/emu4-cookies.js.map +1 -0
- package/dist/handlers/emu4-erasure.d.ts +2 -0
- package/dist/handlers/emu4-erasure.js +587 -0
- package/dist/handlers/emu4-erasure.js.map +1 -0
- package/dist/handlers/emu4-federation.d.ts +8 -0
- package/dist/handlers/emu4-federation.js +18 -0
- package/dist/handlers/emu4-federation.js.map +1 -0
- package/dist/handlers/emu4-journey-math.d.ts +42 -0
- package/dist/handlers/emu4-journey-math.js +99 -0
- package/dist/handlers/emu4-journey-math.js.map +1 -0
- package/dist/handlers/emu4-messaging.d.ts +21 -0
- package/dist/handlers/emu4-messaging.js +595 -0
- package/dist/handlers/emu4-messaging.js.map +1 -0
- package/dist/handlers/emu4-minors.d.ts +7 -0
- package/dist/handlers/emu4-minors.js +408 -0
- package/dist/handlers/emu4-minors.js.map +1 -0
- package/dist/handlers/emu4-oauth-audit.d.ts +5 -0
- package/dist/handlers/emu4-oauth-audit.js +17 -0
- package/dist/handlers/emu4-oauth-audit.js.map +1 -0
- package/dist/handlers/emu4-oauth-auth.d.ts +83 -0
- package/dist/handlers/emu4-oauth-auth.js +363 -0
- package/dist/handlers/emu4-oauth-auth.js.map +1 -0
- package/dist/handlers/emu4-oauth-delivery.d.ts +68 -0
- package/dist/handlers/emu4-oauth-delivery.js +207 -0
- package/dist/handlers/emu4-oauth-delivery.js.map +1 -0
- package/dist/handlers/emu4-oauth-keys.d.ts +11 -0
- package/dist/handlers/emu4-oauth-keys.js +268 -0
- package/dist/handlers/emu4-oauth-keys.js.map +1 -0
- package/dist/handlers/emu4-oauth-pub.d.ts +36 -0
- package/dist/handlers/emu4-oauth-pub.js +420 -0
- package/dist/handlers/emu4-oauth-pub.js.map +1 -0
- package/dist/handlers/emu4-oauth.d.ts +10 -0
- package/dist/handlers/emu4-oauth.js +23 -0
- package/dist/handlers/emu4-oauth.js.map +1 -0
- package/dist/handlers/emu4-policies.d.ts +2 -0
- package/dist/handlers/emu4-policies.js +408 -0
- package/dist/handlers/emu4-policies.js.map +1 -0
- package/dist/handlers/emu4-region.d.ts +2 -0
- package/dist/handlers/emu4-region.js +189 -0
- package/dist/handlers/emu4-region.js.map +1 -0
- package/dist/handlers/emu4-saml.d.ts +12 -0
- package/dist/handlers/emu4-saml.js +532 -0
- package/dist/handlers/emu4-saml.js.map +1 -0
- package/dist/handlers/emu4-shared.d.ts +44 -0
- package/dist/handlers/emu4-shared.js +90 -0
- package/dist/handlers/emu4-shared.js.map +1 -0
- package/dist/handlers/emu4-snapshots.d.ts +38 -0
- package/dist/handlers/emu4-snapshots.js +231 -0
- package/dist/handlers/emu4-snapshots.js.map +1 -0
- package/dist/handlers/emu4-sns.d.ts +9 -0
- package/dist/handlers/emu4-sns.js +21 -0
- package/dist/handlers/emu4-sns.js.map +1 -0
- package/dist/handlers/emu4-trust-binding.d.ts +27 -0
- package/dist/handlers/emu4-trust-binding.js +344 -0
- package/dist/handlers/emu4-trust-binding.js.map +1 -0
- package/dist/handlers/emu4-trust.d.ts +35 -0
- package/dist/handlers/emu4-trust.js +543 -0
- package/dist/handlers/emu4-trust.js.map +1 -0
- package/dist/handlers/fga-engine.d.ts +78 -0
- package/dist/handlers/fga-engine.js +663 -0
- package/dist/handlers/fga-engine.js.map +1 -0
- package/dist/handlers/hooks.js +13 -0
- package/dist/handlers/hooks.js.map +1 -1
- package/dist/handlers/index.js +14 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/machines.js +6 -0
- package/dist/handlers/machines.js.map +1 -1
- package/dist/handlers/messaging.d.ts +3 -1
- package/dist/handlers/messaging.js +455 -48
- package/dist/handlers/messaging.js.map +1 -1
- package/dist/handlers/oauth-adv.d.ts +191 -0
- package/dist/handlers/oauth-adv.js +1305 -0
- package/dist/handlers/oauth-adv.js.map +1 -0
- package/dist/handlers/oauth-flows.d.ts +25 -1
- package/dist/handlers/oauth-flows.js +111 -50
- package/dist/handlers/oauth-flows.js.map +1 -1
- package/dist/handlers/oauth.d.ts +24 -3
- package/dist/handlers/oauth.js +245 -60
- package/dist/handlers/oauth.js.map +1 -1
- package/dist/handlers/orgs.js +6 -0
- package/dist/handlers/orgs.js.map +1 -1
- package/dist/handlers/platform.d.ts +5 -0
- package/dist/handlers/platform.js +33 -15
- package/dist/handlers/platform.js.map +1 -1
- package/dist/handlers/scim.d.ts +2 -0
- package/dist/handlers/scim.js +198 -11
- package/dist/handlers/scim.js.map +1 -1
- package/dist/handlers/sessions.d.ts +2 -0
- package/dist/handlers/sessions.js +24 -16
- package/dist/handlers/sessions.js.map +1 -1
- package/dist/handlers/users.js +3 -0
- package/dist/handlers/users.js.map +1 -1
- package/dist/jwe.d.ts +18 -0
- package/dist/jwe.js +101 -0
- package/dist/jwe.js.map +1 -0
- package/dist/jws.js +2 -0
- package/dist/jws.js.map +1 -1
- package/dist/messaging-callbacks.d.ts +100 -0
- package/dist/messaging-callbacks.js +767 -0
- package/dist/messaging-callbacks.js.map +1 -0
- package/dist/openapi.js +41 -1
- package/dist/openapi.js.map +1 -1
- package/dist/parity-emu3-authz.d.ts +3 -0
- package/dist/parity-emu3-authz.js +61 -0
- package/dist/parity-emu3-authz.js.map +1 -0
- package/dist/parity-emu3-billing.d.ts +3 -0
- package/dist/parity-emu3-billing.js +25 -0
- package/dist/parity-emu3-billing.js.map +1 -0
- package/dist/parity-emu3-messaging.d.ts +3 -0
- package/dist/parity-emu3-messaging.js +27 -0
- package/dist/parity-emu3-messaging.js.map +1 -0
- package/dist/parity-emu3-misc.d.ts +2 -0
- package/dist/parity-emu3-misc.js +63 -0
- package/dist/parity-emu3-misc.js.map +1 -0
- package/dist/parity-emu3-oauth.d.ts +2 -0
- package/dist/parity-emu3-oauth.js +39 -0
- package/dist/parity-emu3-oauth.js.map +1 -0
- package/dist/parity-emu4-actions.d.ts +2 -0
- package/dist/parity-emu4-actions.js +38 -0
- package/dist/parity-emu4-actions.js.map +1 -0
- package/dist/parity-emu4-authz.d.ts +3 -0
- package/dist/parity-emu4-authz.js +68 -0
- package/dist/parity-emu4-authz.js.map +1 -0
- package/dist/parity-emu4-billing.d.ts +6 -0
- package/dist/parity-emu4-billing.js +44 -0
- package/dist/parity-emu4-billing.js.map +1 -0
- package/dist/parity-emu4-compliance.d.ts +2 -0
- package/dist/parity-emu4-compliance.js +92 -0
- package/dist/parity-emu4-compliance.js.map +1 -0
- package/dist/parity-emu4-federation.d.ts +2 -0
- package/dist/parity-emu4-federation.js +33 -0
- package/dist/parity-emu4-federation.js.map +1 -0
- package/dist/parity-emu4-messaging.d.ts +3 -0
- package/dist/parity-emu4-messaging.js +42 -0
- package/dist/parity-emu4-messaging.js.map +1 -0
- package/dist/parity-emu4-oauth.d.ts +2 -0
- package/dist/parity-emu4-oauth.js +55 -0
- package/dist/parity-emu4-oauth.js.map +1 -0
- package/dist/parity-emu4-trust.d.ts +2 -0
- package/dist/parity-emu4-trust.js +41 -0
- package/dist/parity-emu4-trust.js.map +1 -0
- package/dist/parity-routes.js +19 -1
- package/dist/parity-routes.js.map +1 -1
- package/dist/sns-cert.d.ts +9 -0
- package/dist/sns-cert.js +101 -0
- package/dist/sns-cert.js.map +1 -0
- package/dist/tokens.d.ts +69 -4
- package/dist/tokens.js +164 -38
- package/dist/tokens.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authzDataHandlers = authzDataHandlers;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const generic_1 = require("../generic");
|
|
6
|
+
const store_1 = require("../store");
|
|
7
|
+
const cedar_1 = require("./cedar");
|
|
8
|
+
const fga_engine_1 = require("./fga-engine");
|
|
9
|
+
const authz_layers_1 = require("./authz-layers");
|
|
10
|
+
const authz_state_1 = require("./authz-state");
|
|
11
|
+
/**
|
|
12
|
+
* The data the authorization layers read, as real routes: group membership, direct app roles, resources with their role
|
|
13
|
+
* assignments and checks, the ABAC evaluate route and FGA tuple writes, reads and checks. Rows of groups, app roles, resource
|
|
14
|
+
* types, ABAC policies, access policies and FGA stores stay generic rows; the routes here add what the generic layer cannot
|
|
15
|
+
* model (membership, assignments, upserts, defaults on create, evaluation).
|
|
16
|
+
*/
|
|
17
|
+
const bad = (message, param) => new errors_1.EmulatorError('VALIDATION_FAILED', message, param);
|
|
18
|
+
const strip = (r) => {
|
|
19
|
+
const { _parent, ...out } = r;
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
const clamp = (raw, def, max) => Math.min(Math.max(Number(raw) || def, 1), max);
|
|
23
|
+
const MAX_BATCH = 500;
|
|
24
|
+
const MAX_ROLES_PER_USER = 50;
|
|
25
|
+
const KEY = /^[a-z][a-z0-9_.-]{0,62}$/;
|
|
26
|
+
const needStr = (v, field, max = 200) => {
|
|
27
|
+
if (typeof v !== 'string' || !v.trim() || v.length > max)
|
|
28
|
+
throw bad(`${field} is required (at most ${max} characters).`, field);
|
|
29
|
+
return v;
|
|
30
|
+
};
|
|
31
|
+
// ── groups ──
|
|
32
|
+
const group = (e, ref) => {
|
|
33
|
+
const g = (0, authz_state_1.findGroup)(e, ref);
|
|
34
|
+
if (!g)
|
|
35
|
+
throw new errors_1.EmulatorError('NOT_FOUND', 'Unknown group.');
|
|
36
|
+
return g;
|
|
37
|
+
};
|
|
38
|
+
function userIds(body) {
|
|
39
|
+
const ids = body.user_ids;
|
|
40
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > MAX_BATCH || ids.some((i) => typeof i !== 'string'))
|
|
41
|
+
throw bad(`user_ids is a list of 1 to ${MAX_BATCH} user ids.`, 'user_ids');
|
|
42
|
+
return [...new Set(ids)];
|
|
43
|
+
}
|
|
44
|
+
// ── app roles ──
|
|
45
|
+
function userRolesView(e, userId) {
|
|
46
|
+
const roles = (0, authz_state_1.directAppRoles)(e, userId);
|
|
47
|
+
return { object: 'user_app_roles', user_id: userId, roles: roles.map(strip), role_keys: roles.map((r) => r.key), permissions: [...new Set(roles.flatMap((r) => r.permissions ?? []))].sort() };
|
|
48
|
+
}
|
|
49
|
+
function resolveRoleIds(e, raw, field) {
|
|
50
|
+
if (!Array.isArray(raw) || raw.some((r) => typeof r !== 'string'))
|
|
51
|
+
throw bad(`${field} must be an array of role keys or ids.`, field);
|
|
52
|
+
const list = [...new Set(raw)];
|
|
53
|
+
if (list.length > MAX_ROLES_PER_USER)
|
|
54
|
+
throw bad(`At most ${MAX_ROLES_PER_USER} roles per user.`, field);
|
|
55
|
+
const ids = [];
|
|
56
|
+
const unknown = [];
|
|
57
|
+
for (const k of list) {
|
|
58
|
+
const r = (0, authz_state_1.findAppRole)(e, k);
|
|
59
|
+
if (r)
|
|
60
|
+
ids.push(r.id);
|
|
61
|
+
else
|
|
62
|
+
unknown.push(k);
|
|
63
|
+
}
|
|
64
|
+
if (unknown.length)
|
|
65
|
+
throw bad(`Unknown app role(s): ${unknown.join(', ')}.`, field);
|
|
66
|
+
return ids;
|
|
67
|
+
}
|
|
68
|
+
function emitUserRoles(e, userId) {
|
|
69
|
+
const v = userRolesView(e, userId);
|
|
70
|
+
e.webhooks.emit('user.app_roles_updated', { object: 'user_app_roles', user_id: userId, roles: v.role_keys, permissions: v.permissions });
|
|
71
|
+
}
|
|
72
|
+
// ── resources ──
|
|
73
|
+
const resourceTable = (e) => (0, authz_state_1.genericTable)(e, 'resources', 'resource');
|
|
74
|
+
const assignmentTable = (e) => (0, authz_state_1.genericTable)(e, 'resource_role_assignments', 'resourcerole');
|
|
75
|
+
const typeRow = (e, key) => e.generic.get('resource_types')?.find((r) => r.key === key);
|
|
76
|
+
/** Validate a type's role list: unique keys, string permissions, `includes` naming real roles. */
|
|
77
|
+
function parseRoles(raw) {
|
|
78
|
+
if (!Array.isArray(raw))
|
|
79
|
+
throw bad('roles must be an array.', 'roles');
|
|
80
|
+
if (raw.length > 200)
|
|
81
|
+
throw bad('A resource type may define at most 200 roles.', 'roles');
|
|
82
|
+
const roles = raw.map((r, i) => {
|
|
83
|
+
const o = (r ?? {});
|
|
84
|
+
if (typeof o.key !== 'string' || !KEY.test(o.key))
|
|
85
|
+
throw bad(`roles[${i}].key must be lowercase letters, digits, "_", "." or "-".`, 'roles');
|
|
86
|
+
if (!Array.isArray(o.permissions) || o.permissions.some((p) => typeof p !== 'string' || !p))
|
|
87
|
+
throw bad(`roles[${i}].permissions must be an array of strings.`, 'roles');
|
|
88
|
+
if (o.includes !== undefined && (!Array.isArray(o.includes) || o.includes.some((p) => typeof p !== 'string')))
|
|
89
|
+
throw bad(`roles[${i}].includes must be an array of role keys.`, 'roles');
|
|
90
|
+
return { key: o.key, ...(typeof o.name === 'string' ? { name: o.name } : {}), permissions: o.permissions, ...(o.includes ? { includes: o.includes } : {}) };
|
|
91
|
+
});
|
|
92
|
+
const keys = new Set(roles.map((r) => r.key));
|
|
93
|
+
if (keys.size !== roles.length)
|
|
94
|
+
throw bad('Role keys must be unique within a resource type.', 'roles');
|
|
95
|
+
for (const r of roles)
|
|
96
|
+
for (const inc of r.includes ?? [])
|
|
97
|
+
if (!keys.has(inc))
|
|
98
|
+
throw bad(`Role "${r.key}" includes unknown role "${inc}".`, 'roles');
|
|
99
|
+
return roles;
|
|
100
|
+
}
|
|
101
|
+
function upsertResource(e, body) {
|
|
102
|
+
const typeKey = needStr(body.type, 'type');
|
|
103
|
+
const type = typeRow(e, typeKey);
|
|
104
|
+
if (!type)
|
|
105
|
+
throw bad(`Unknown resource type "${typeKey}".`, 'type');
|
|
106
|
+
const parentType = type.parent_type ?? null;
|
|
107
|
+
let parentId;
|
|
108
|
+
if (body.parent === null)
|
|
109
|
+
parentId = null;
|
|
110
|
+
else if (body.parent !== undefined) {
|
|
111
|
+
const parent = (0, authz_layers_1.findResourceRef)(e, body.parent, 'parent');
|
|
112
|
+
if (parentType && parent.type !== parentType)
|
|
113
|
+
throw bad(`A "${typeKey}" must be nested under a "${parentType}".`, 'parent');
|
|
114
|
+
parentId = parent.id;
|
|
115
|
+
}
|
|
116
|
+
if (parentType && parentId === null)
|
|
117
|
+
throw bad(`A "${typeKey}" must be nested under a "${parentType}".`, 'parent');
|
|
118
|
+
const externalId = needStr(body.external_id, 'external_id', 500);
|
|
119
|
+
const existing = resourceTable(e).find((r) => r.type === typeKey && r.external_id === externalId);
|
|
120
|
+
if (parentId && existing && (0, authz_layers_1.ancestorsOf)(e, parentId).some((a) => a.id === existing.id))
|
|
121
|
+
throw bad('That parent would make the resource its own ancestor.', 'parent');
|
|
122
|
+
if (!existing && parentType && parentId === undefined)
|
|
123
|
+
throw bad(`A "${typeKey}" must be nested under a "${parentType}".`, 'parent');
|
|
124
|
+
const now = e.clock.now();
|
|
125
|
+
const name = body.name === undefined ? undefined : typeof body.name === 'string' ? body.name : null;
|
|
126
|
+
const metadata = body.metadata && typeof body.metadata === 'object' ? body.metadata : undefined;
|
|
127
|
+
if (existing) {
|
|
128
|
+
const patch = { updated_at: now };
|
|
129
|
+
if (name !== undefined)
|
|
130
|
+
patch.name = name;
|
|
131
|
+
if (parentId !== undefined)
|
|
132
|
+
patch.parent_id = parentId;
|
|
133
|
+
if (metadata !== undefined)
|
|
134
|
+
patch.metadata = metadata;
|
|
135
|
+
return resourceTable(e).update(existing.id, patch);
|
|
136
|
+
}
|
|
137
|
+
return resourceTable(e).insert({ object: 'resource', type: typeKey, external_id: externalId, name: name ?? null, parent_id: parentId ?? null, metadata: metadata ?? {}, created_at: now, updated_at: now });
|
|
138
|
+
}
|
|
139
|
+
function assignmentEvent(e, type, a, resource) {
|
|
140
|
+
e.webhooks.emit(`resource_role_assignment.${type}`, { object: 'resource_role_assignment', id: a.id, resource_id: a.resource_id, resource_type: resource?.type ?? null, resource_external_id: resource?.external_id ?? null, subject: a.subject, role: a.role, inherit: a.inherit, created_at: a.created_at });
|
|
141
|
+
}
|
|
142
|
+
// ── create defaults ──
|
|
143
|
+
/** Run the generic create with the defaults the real service applies when a field is omitted. */
|
|
144
|
+
const withDefaults = (c, defaults) => (0, generic_1.genericHandler)({ ...c, body: { ...defaults, ...c.body } });
|
|
145
|
+
function validateAccessPolicy(e, b) {
|
|
146
|
+
if (typeof b.name !== 'string' || !b.name.trim() || b.name.length > 100)
|
|
147
|
+
throw bad('name is required (1-100 characters).', 'name');
|
|
148
|
+
if (e.generic.get('access_policies')?.find((r) => r.name === b.name.trim()))
|
|
149
|
+
throw bad(`An access policy named "${b.name.trim()}" already exists.`, 'name');
|
|
150
|
+
if (b.mode !== undefined && !['enforce', 'report_only'].includes(b.mode))
|
|
151
|
+
throw bad('mode must be enforce or report_only.', 'mode');
|
|
152
|
+
if (b.targets !== undefined && (!Array.isArray(b.targets) || !b.targets.length || b.targets.some((t) => !['user', 'machine', 'agent'].includes(t))))
|
|
153
|
+
throw bad('targets must be a non-empty list of user, machine or agent.', 'targets');
|
|
154
|
+
if (b.priority !== undefined && (!Number.isInteger(b.priority) || b.priority < 0 || b.priority > 100000))
|
|
155
|
+
throw bad('priority must be an integer 0-100000.', 'priority');
|
|
156
|
+
if (b.stages !== undefined && (!Array.isArray(b.stages) || b.stages.some((s) => !['sign_in', 'authorize', 'token'].includes(s))))
|
|
157
|
+
throw bad('stages must be sign_in, authorize or token.', 'stages');
|
|
158
|
+
if (b.conditions !== undefined && (typeof b.conditions !== 'object' || b.conditions === null || Array.isArray(b.conditions)))
|
|
159
|
+
throw bad('conditions must be an object.', 'conditions');
|
|
160
|
+
if (!Array.isArray(b.grants))
|
|
161
|
+
throw bad('grants must be an array.', 'grants');
|
|
162
|
+
if (!b.grants.length)
|
|
163
|
+
throw bad('grants must contain at least one grant.', 'conditions');
|
|
164
|
+
const types = ['allow', 'block', 'require_mfa', 'require_acr', 'require_policy_acceptance', 'require_action', 'require_auth_strength', 'require_device_bound_session', 'require_password_change', 'sign_in_frequency', 'persistent_session', 'max_session_age'];
|
|
165
|
+
for (const g of b.grants) {
|
|
166
|
+
if (!g || typeof g !== 'object')
|
|
167
|
+
throw bad('grants must be objects.', 'conditions');
|
|
168
|
+
if (!types.includes(g.type))
|
|
169
|
+
throw bad(`Unknown grant type "${g.type}".`, 'conditions');
|
|
170
|
+
if (['require_acr', 'require_policy_acceptance', 'require_action'].includes(g.type) && !g.value)
|
|
171
|
+
throw bad(`${g.type} needs a value.`, 'conditions');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ── FGA ──
|
|
175
|
+
function parseTuples(raw, field, e, needFuture) {
|
|
176
|
+
const list = raw && typeof raw === 'object' && !Array.isArray(raw) && 'tuple_keys' in raw ? raw.tuple_keys : raw;
|
|
177
|
+
if (list === undefined)
|
|
178
|
+
return [];
|
|
179
|
+
if (!Array.isArray(list))
|
|
180
|
+
throw bad(`${field} must be an array.`, field);
|
|
181
|
+
return list.map((t, i) => {
|
|
182
|
+
if (typeof t !== 'object' || t === null)
|
|
183
|
+
throw bad(`${field}[${i}] must be an object.`, field);
|
|
184
|
+
const object = t.object ?? t.target;
|
|
185
|
+
if (typeof t.user !== 'string' || typeof t.relation !== 'string' || typeof object !== 'string')
|
|
186
|
+
throw bad(`${field}[${i}] requires string user, relation and object.`, field);
|
|
187
|
+
const out = { user: t.user, relation: t.relation, object };
|
|
188
|
+
if (t.condition !== undefined && t.condition !== null) {
|
|
189
|
+
if (typeof t.condition !== 'object' || typeof t.condition.name !== 'string' || !t.condition.name)
|
|
190
|
+
throw bad(`${field}[${i}].condition: condition requires a string name.`, field);
|
|
191
|
+
out.condition = { name: t.condition.name, ...(t.condition.context ? { context: t.condition.context } : {}) };
|
|
192
|
+
}
|
|
193
|
+
if (t.expires_at !== undefined && t.expires_at !== null) {
|
|
194
|
+
const at = new Date(t.expires_at).getTime();
|
|
195
|
+
if (typeof t.expires_at !== 'number' && typeof t.expires_at !== 'string')
|
|
196
|
+
throw bad(`${field}[${i}].expires_at must be an ISO timestamp or epoch ms.`, field);
|
|
197
|
+
if (Number.isNaN(at))
|
|
198
|
+
throw bad(`${field}[${i}].expires_at must be an ISO timestamp or epoch ms.`, field);
|
|
199
|
+
if (needFuture && at <= e.clock.now())
|
|
200
|
+
throw bad(`${field}[${i}].expires_at must be in the future.`, field);
|
|
201
|
+
out.expires_at = at;
|
|
202
|
+
}
|
|
203
|
+
// Same shape rules as the real store: user is type:id, type:id#relation or type:*; object is type:id.
|
|
204
|
+
const ui = t.user.indexOf(':');
|
|
205
|
+
const oi = object.indexOf(':');
|
|
206
|
+
const rest = ui > 0 ? t.user.slice(ui + 1) : '';
|
|
207
|
+
if (ui <= 0 || !rest || (rest.includes('#') && (rest.startsWith('#') || rest.endsWith('#') || rest.startsWith('*'))))
|
|
208
|
+
throw bad(`Malformed user '${t.user}': expected 'type:id', 'type:id#relation' or 'type:*'.`, field);
|
|
209
|
+
if (oi <= 0 || oi === object.length - 1 || object.includes('#'))
|
|
210
|
+
throw bad(`Malformed object '${object}': expected 'type:id'.`, field);
|
|
211
|
+
if (!t.relation || t.relation.includes(':') || t.relation.includes('#'))
|
|
212
|
+
throw bad(`Malformed relation '${t.relation}'.`, field);
|
|
213
|
+
return out;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const mode = (v, field) => {
|
|
217
|
+
if (v === undefined || v === null)
|
|
218
|
+
return 'ignore';
|
|
219
|
+
if (v !== 'ignore' && v !== 'error')
|
|
220
|
+
throw bad(`${field} must be "ignore" or "error".`, field);
|
|
221
|
+
return v;
|
|
222
|
+
};
|
|
223
|
+
const store = (e, id) => {
|
|
224
|
+
if (!id || !(0, authz_state_1.storeExists)(e, id))
|
|
225
|
+
throw new errors_1.EmulatorError('NOT_FOUND', 'Unknown FGA store.');
|
|
226
|
+
return id;
|
|
227
|
+
};
|
|
228
|
+
const tupleView = (t) => ({ object: 'fga.tuple', user: t.user, relation: t.relation, target: t.object, ...(t.condition ? { condition: t.condition } : {}), ...(t.expires_at ? { expires_at: t.expires_at } : {}), created_at: t.created_at });
|
|
229
|
+
function authzDataHandlers() {
|
|
230
|
+
return [
|
|
231
|
+
// ── group membership ──
|
|
232
|
+
{
|
|
233
|
+
method: 'GET',
|
|
234
|
+
pattern: '/v1/groups/:id/members',
|
|
235
|
+
scope: 'users:read',
|
|
236
|
+
handler: (c) => {
|
|
237
|
+
const g = group(c.engine, c.params.id);
|
|
238
|
+
const limit = clamp(c.query.get('limit'), 50, 100);
|
|
239
|
+
const after = c.query.get('starting_after');
|
|
240
|
+
const rows = (0, authz_state_1.groupMembers)(c.engine, g.id).sort((a, b) => (a.user_id < b.user_id ? -1 : 1)).filter((m) => !after || m.user_id > after);
|
|
241
|
+
const page = rows.slice(0, limit);
|
|
242
|
+
return { body: { object: 'list', data: page.map((r) => ({ object: 'group_member', group_id: r.group_id, user_id: r.user_id, source: r.source, source_ref: r.source_ref ?? null, created_at: r.created_at })), has_more: rows.length > limit, next_cursor: rows.length > limit ? page[page.length - 1].user_id : null } };
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
method: 'POST',
|
|
247
|
+
pattern: '/v1/groups/:id/members',
|
|
248
|
+
scope: 'users:write',
|
|
249
|
+
handler: (c) => {
|
|
250
|
+
const e = c.engine;
|
|
251
|
+
const g = group(e, c.params.id);
|
|
252
|
+
const ids = userIds(c.body);
|
|
253
|
+
const valid = ids.filter((id) => (0, authz_state_1.liveUser)(e, id));
|
|
254
|
+
const members = (0, authz_state_1.st)(e).members;
|
|
255
|
+
const had = new Map(members.filter((m) => m.group_id === g.id).map((m) => [m.user_id, m]));
|
|
256
|
+
const added = valid.filter((id) => !had.has(id));
|
|
257
|
+
const promoted = valid.filter((id) => had.has(id) && had.get(id).source !== 'manual');
|
|
258
|
+
for (const id of promoted)
|
|
259
|
+
members.update(had.get(id).id, { source: 'manual', source_ref: null });
|
|
260
|
+
for (const id of added)
|
|
261
|
+
members.insert({ group_id: g.id, user_id: id, source: 'manual', source_ref: null, created_at: e.clock.now() });
|
|
262
|
+
for (const id of added)
|
|
263
|
+
e.webhooks.emit('group.member_added', { object: 'group_member', group_id: g.id, user_id: id, source: 'manual' });
|
|
264
|
+
if (added.length)
|
|
265
|
+
(0, authz_state_1.onMembershipChanged)(e);
|
|
266
|
+
return { body: { object: 'group_membership_result', group_id: g.id, added: added.length, promoted: promoted.length, already_member: valid.length - added.length - promoted.length, skipped: ids.length - valid.length } };
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
method: 'POST',
|
|
271
|
+
pattern: '/v1/groups/:id/members/remove',
|
|
272
|
+
scope: 'users:write',
|
|
273
|
+
handler: (c) => {
|
|
274
|
+
const e = c.engine;
|
|
275
|
+
const g = group(e, c.params.id);
|
|
276
|
+
const ids = userIds(c.body);
|
|
277
|
+
const gone = (0, authz_state_1.st)(e).members.filter((m) => m.group_id === g.id && ids.includes(m.user_id));
|
|
278
|
+
for (const m of gone)
|
|
279
|
+
(0, authz_state_1.st)(e).members.delete(m.id);
|
|
280
|
+
for (const m of gone)
|
|
281
|
+
e.webhooks.emit('group.member_removed', { object: 'group_member', group_id: g.id, user_id: m.user_id });
|
|
282
|
+
if (gone.length)
|
|
283
|
+
(0, authz_state_1.onMembershipChanged)(e);
|
|
284
|
+
return { body: { object: 'group_membership_result', group_id: g.id, removed: gone.length } };
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
method: 'GET',
|
|
289
|
+
pattern: '/v1/users/:id/groups',
|
|
290
|
+
scope: 'users:read',
|
|
291
|
+
handler: (c) => {
|
|
292
|
+
const e = c.engine;
|
|
293
|
+
const rows = (0, authz_state_1.st)(e).members.filter((m) => m.user_id === c.params.id).map((m) => ({ g: e.generic.get('groups')?.get(m.group_id), source: m.source })).filter((x) => !!x.g);
|
|
294
|
+
rows.sort((a, b) => (a.g.slug < b.g.slug ? -1 : 1));
|
|
295
|
+
return { body: { object: 'list', data: rows.map((x) => ({ ...strip(x.g), source: x.source })), has_more: false } };
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
// ── a user's app roles ──
|
|
299
|
+
{
|
|
300
|
+
method: 'GET',
|
|
301
|
+
pattern: '/v1/users/:id/app_roles',
|
|
302
|
+
scope: 'users:read',
|
|
303
|
+
handler: (c) => {
|
|
304
|
+
if (!(0, authz_state_1.liveUser)(c.engine, c.params.id))
|
|
305
|
+
throw (0, errors_1.notFound)('User');
|
|
306
|
+
return { body: userRolesView(c.engine, c.params.id) };
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
method: 'POST',
|
|
311
|
+
pattern: '/v1/users/:id/app_roles',
|
|
312
|
+
scope: 'users:write',
|
|
313
|
+
handler: (c) => {
|
|
314
|
+
const e = c.engine;
|
|
315
|
+
const userId = c.params.id;
|
|
316
|
+
if (!(0, authz_state_1.liveUser)(e, userId))
|
|
317
|
+
throw (0, errors_1.notFound)('User');
|
|
318
|
+
const b = c.body;
|
|
319
|
+
const replacing = b.roles !== undefined;
|
|
320
|
+
if (replacing && (b.add !== undefined || b.remove !== undefined))
|
|
321
|
+
throw bad('Send roles (replace) or add/remove, not both.', 'roles');
|
|
322
|
+
if (!replacing && b.add === undefined && b.remove === undefined)
|
|
323
|
+
throw bad('Send roles, add or remove.', 'roles');
|
|
324
|
+
const have = (0, authz_state_1.st)(e).userRoles.get(userId) ?? new Set();
|
|
325
|
+
let next;
|
|
326
|
+
if (replacing)
|
|
327
|
+
next = new Set(resolveRoleIds(e, b.roles, 'roles'));
|
|
328
|
+
else {
|
|
329
|
+
const add = b.add !== undefined ? resolveRoleIds(e, b.add, 'add') : [];
|
|
330
|
+
const remove = b.remove !== undefined ? resolveRoleIds(e, b.remove, 'remove') : [];
|
|
331
|
+
next = new Set([...have, ...add.filter((id) => !remove.includes(id))].filter((id) => !remove.includes(id)));
|
|
332
|
+
if (next.size > MAX_ROLES_PER_USER)
|
|
333
|
+
throw bad(`At most ${MAX_ROLES_PER_USER} roles per user.`, 'add');
|
|
334
|
+
}
|
|
335
|
+
const changed = next.size !== have.size || [...next].some((id) => !have.has(id));
|
|
336
|
+
(0, authz_state_1.st)(e).userRoles.set(userId, next);
|
|
337
|
+
if (changed)
|
|
338
|
+
emitUserRoles(e, userId);
|
|
339
|
+
return { body: userRolesView(e, userId) };
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
method: 'DELETE',
|
|
344
|
+
pattern: '/v1/users/:id/app_roles/:roleId',
|
|
345
|
+
scope: 'users:write',
|
|
346
|
+
handler: (c) => {
|
|
347
|
+
const e = c.engine;
|
|
348
|
+
if (!(0, authz_state_1.liveUser)(e, c.params.id))
|
|
349
|
+
throw (0, errors_1.notFound)('User');
|
|
350
|
+
const role = (0, authz_state_1.findAppRole)(e, c.params.roleId);
|
|
351
|
+
if (!role)
|
|
352
|
+
throw new errors_1.EmulatorError('NOT_FOUND', 'Unknown app role.');
|
|
353
|
+
if ((0, authz_state_1.st)(e).userRoles.get(c.params.id)?.delete(role.id))
|
|
354
|
+
emitUserRoles(e, c.params.id);
|
|
355
|
+
return { body: userRolesView(e, c.params.id) };
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
// ── resource types, resources, assignments, checks ──
|
|
359
|
+
{
|
|
360
|
+
method: 'POST',
|
|
361
|
+
pattern: '/v1/resource_types',
|
|
362
|
+
scope: 'resources:write',
|
|
363
|
+
handler: (c) => {
|
|
364
|
+
const e = c.engine;
|
|
365
|
+
const b = c.body;
|
|
366
|
+
const key = typeof b.key === 'string' && KEY.test(b.key) ? b.key : undefined;
|
|
367
|
+
if (!key)
|
|
368
|
+
throw bad('key must be lowercase letters, digits, "_", "." or "-".', 'key');
|
|
369
|
+
if (typeRow(e, key))
|
|
370
|
+
throw bad(`A resource type "${key}" already exists.`, 'key');
|
|
371
|
+
const parentType = b.parent_type === undefined || b.parent_type === null ? null : needStr(b.parent_type, 'parent_type');
|
|
372
|
+
if (parentType && !typeRow(e, parentType))
|
|
373
|
+
throw bad(`Unknown parent_type "${parentType}".`, 'parent_type');
|
|
374
|
+
const now = e.clock.now();
|
|
375
|
+
// The row id is the key, so the generic read, update and delete by key keep working.
|
|
376
|
+
const row = (0, authz_state_1.genericTable)(e, 'resource_types', 'resourcetype').insert({ id: key, object: 'resource_type', key, name: needStr(b.name ?? key, 'name'), description: typeof b.description === 'string' ? b.description : null, parent_type: parentType, inherit: typeof b.inherit === 'boolean' ? b.inherit : true, roles: parseRoles(b.roles ?? []), created_at: now, updated_at: now });
|
|
377
|
+
return { status: 201, body: strip(row) };
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
method: 'PUT',
|
|
382
|
+
pattern: '/v1/resources',
|
|
383
|
+
scope: 'resources:write',
|
|
384
|
+
handler: (c) => ({ body: strip(upsertResource(c.engine, c.body)) }),
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
method: 'POST',
|
|
388
|
+
pattern: '/v1/resources/sync',
|
|
389
|
+
scope: 'resources:write',
|
|
390
|
+
handler: (c) => {
|
|
391
|
+
const list = c.body.resources;
|
|
392
|
+
if (!Array.isArray(list) || list.length === 0 || list.length > 500)
|
|
393
|
+
throw bad('resources must be an array of 1–500 items.', 'resources');
|
|
394
|
+
return { body: { object: 'list', data: list.map((item) => strip(upsertResource(c.engine, (item ?? {})))), has_more: false } };
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
method: 'POST',
|
|
399
|
+
pattern: '/v1/resource_role_assignments',
|
|
400
|
+
scope: 'resources:write',
|
|
401
|
+
handler: (c) => {
|
|
402
|
+
const e = c.engine;
|
|
403
|
+
const resource = (0, authz_layers_1.findResourceRef)(e, c.body.resource, 'resource');
|
|
404
|
+
const subject = (0, authz_layers_1.parseSubject)(c.body.subject);
|
|
405
|
+
const role = needStr(c.body.role, 'role');
|
|
406
|
+
if (!typeRow(e, resource.type)?.roles?.some((r) => r.key === role))
|
|
407
|
+
throw bad(`"${resource.type}" has no role "${role}".`, 'role');
|
|
408
|
+
const inherit = typeof c.body.inherit === 'boolean' ? c.body.inherit : true;
|
|
409
|
+
const t = assignmentTable(e);
|
|
410
|
+
const same = t.find((a) => a.resource_id === resource.id && a.subject.type === subject.type && a.subject.id === subject.id && a.role === role);
|
|
411
|
+
// Re-assigning the same role updates `inherit`.
|
|
412
|
+
const row = same ? t.update(same.id, { inherit }) : t.insert({ object: 'resource_role_assignment', resource_id: resource.id, subject, role, inherit, created_at: e.clock.now() });
|
|
413
|
+
assignmentEvent(e, 'created', row, resource);
|
|
414
|
+
return { status: 201, body: row };
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
method: 'DELETE',
|
|
419
|
+
pattern: '/v1/resource_role_assignments/:id',
|
|
420
|
+
scope: 'resources:write',
|
|
421
|
+
handler: (c) => {
|
|
422
|
+
const e = c.engine;
|
|
423
|
+
const row = assignmentTable(e).get(c.params.id);
|
|
424
|
+
if (!row)
|
|
425
|
+
throw new errors_1.EmulatorError('NOT_FOUND', 'Unknown assignment.');
|
|
426
|
+
assignmentTable(e).delete(row.id);
|
|
427
|
+
assignmentEvent(e, 'deleted', row, resourceTable(e).get(row.resource_id));
|
|
428
|
+
return { body: { object: 'resource_role_assignment', id: row.id, deleted: true } };
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
method: 'POST',
|
|
433
|
+
pattern: '/v1/resource_checks',
|
|
434
|
+
scope: 'resources:check',
|
|
435
|
+
handler: (c) => {
|
|
436
|
+
const r = (0, authz_layers_1.checkResource)(c.engine, c.body);
|
|
437
|
+
(0, authz_layers_1.recordResourceCheck)(c.engine, c.body, r);
|
|
438
|
+
return { body: { object: 'resource_check', allowed: r.allowed, resource_id: r.resource.id, grants: r.grants.map(authz_layers_1.grantView) } };
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
method: 'POST',
|
|
443
|
+
pattern: '/v1/resource_checks/batch',
|
|
444
|
+
scope: 'resources:check',
|
|
445
|
+
handler: (c) => {
|
|
446
|
+
const list = c.body.checks;
|
|
447
|
+
if (!Array.isArray(list) || list.length === 0 || list.length > 100)
|
|
448
|
+
throw bad('checks must be an array of 1–100 items.', 'checks');
|
|
449
|
+
const data = list.map((item) => {
|
|
450
|
+
const b = (item ?? {});
|
|
451
|
+
const r = (0, authz_layers_1.checkResource)(c.engine, b);
|
|
452
|
+
(0, authz_layers_1.recordResourceCheck)(c.engine, b, r);
|
|
453
|
+
return { object: 'resource_check', allowed: r.allowed, resource_id: r.resource.id };
|
|
454
|
+
});
|
|
455
|
+
return { body: { object: 'list', data, has_more: false } };
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
method: 'POST',
|
|
460
|
+
pattern: '/v1/resource_checks/list',
|
|
461
|
+
scope: 'resources:check',
|
|
462
|
+
handler: (c) => {
|
|
463
|
+
const b = c.body;
|
|
464
|
+
if (typeof b.permission !== 'string' || !b.permission)
|
|
465
|
+
throw bad('permission is required.', 'permission');
|
|
466
|
+
const rows = (0, authz_layers_1.accessibleResources)(c.engine, { subject: (0, authz_layers_1.parseSubject)(b.subject), groups: b.groups, permission: b.permission, type: typeof b.type === 'string' ? b.type : undefined });
|
|
467
|
+
const limit = Math.min(Math.max(Number(b.limit) || 100, 1), 1000);
|
|
468
|
+
const page = (0, store_1.paginate)(rows, { limit, startingAfter: typeof b.starting_after === 'string' ? b.starting_after : undefined });
|
|
469
|
+
return { body: { object: 'list', data: page.data.map(strip), has_more: page.has_more, next_cursor: page.next_cursor } };
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
// ── creates that carry real defaults ──
|
|
473
|
+
{
|
|
474
|
+
method: 'POST',
|
|
475
|
+
pattern: '/v1/abac_policies',
|
|
476
|
+
scope: 'abac:write',
|
|
477
|
+
handler: (c) => {
|
|
478
|
+
const b = c.body;
|
|
479
|
+
if (typeof b.name !== 'string' || !b.name.trim() || b.name.length > 100)
|
|
480
|
+
throw bad('name is required (1-100 characters).', 'name');
|
|
481
|
+
if (c.engine.generic.get('abac_policies')?.find((r) => r.name === b.name))
|
|
482
|
+
throw bad(`A policy named "${b.name}" already exists.`, 'name');
|
|
483
|
+
if (typeof b.source !== 'string' || !b.source.trim())
|
|
484
|
+
throw bad('source is required.', 'source');
|
|
485
|
+
try {
|
|
486
|
+
(0, cedar_1.parseCedar)(b.source);
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
if (err instanceof cedar_1.CedarParseError)
|
|
490
|
+
throw bad(`${err.message} (at character ${err.position})`, 'source');
|
|
491
|
+
throw err;
|
|
492
|
+
}
|
|
493
|
+
return withDefaults(c, { enabled: true, description: null, fga_store_id: null, tests: [], current_version: 1 });
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
method: 'POST',
|
|
498
|
+
pattern: '/v1/access_policies',
|
|
499
|
+
scope: 'access_policies:write',
|
|
500
|
+
handler: (c) => {
|
|
501
|
+
validateAccessPolicy(c.engine, c.body);
|
|
502
|
+
return withDefaults(c, { enabled: true, description: null, priority: 100, stages: [], conditions: {}, mode: 'enforce', targets: ['user'] });
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
// ── ABAC evaluate ──
|
|
506
|
+
{
|
|
507
|
+
method: 'POST',
|
|
508
|
+
pattern: '/v1/abac/evaluate',
|
|
509
|
+
scope: 'abac:evaluate',
|
|
510
|
+
handler: (c) => {
|
|
511
|
+
const e = c.engine;
|
|
512
|
+
const b = c.body;
|
|
513
|
+
const actionId = typeof b.action === 'string' ? b.action : b.action?.id;
|
|
514
|
+
if (typeof actionId !== 'string' || !actionId)
|
|
515
|
+
throw bad('action is required.', 'action');
|
|
516
|
+
const entity = (v, field) => {
|
|
517
|
+
const o = (v ?? {});
|
|
518
|
+
if (typeof o.type !== 'string' || !o.type || typeof o.id !== 'string' || !o.id)
|
|
519
|
+
throw bad(`${field}.type and ${field}.id are required.`, field);
|
|
520
|
+
return { type: o.type, id: o.id, ...(o.attributes && typeof o.attributes === 'object' ? { attributes: o.attributes } : {}), ...(Array.isArray(o.parents) ? { parents: o.parents } : {}) };
|
|
521
|
+
};
|
|
522
|
+
const req = { principal: entity(b.principal, 'principal'), action: { id: actionId }, resource: entity(b.resource, 'resource'), context: b.context && typeof b.context === 'object' ? b.context : undefined, entities: Array.isArray(b.entities) ? b.entities.map((x, i) => entity(x, `entities[${i}]`)) : undefined };
|
|
523
|
+
const pdp = (0, authz_layers_1.loadPdp)(e, Array.isArray(b.policy_ids) ? b.policy_ids : undefined);
|
|
524
|
+
const d = (0, authz_layers_1.abacDecide)(pdp, req);
|
|
525
|
+
const reasons = [...new Set(d.determining.map((i) => pdp.owners[i]))];
|
|
526
|
+
(0, authz_layers_1.logDecision)(e, { source: 'abac', decision: d.decision, principal: `${req.principal.type}::${req.principal.id}`, action: actionId, resource: `${req.resource.type}::${req.resource.id}`, input: { ...req }, result: { reasons, errors: d.errors.length } });
|
|
527
|
+
return { body: { object: 'abac_decision', decision: d.decision, reasons, errors: d.errors.map((x) => x.message) } };
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
// ── FGA tuples ──
|
|
531
|
+
{
|
|
532
|
+
method: 'POST',
|
|
533
|
+
pattern: '/v1/fga/stores/:id/write',
|
|
534
|
+
scope: 'fga:write',
|
|
535
|
+
handler: (c) => {
|
|
536
|
+
const e = c.engine;
|
|
537
|
+
const storeId = store(e, c.params.id);
|
|
538
|
+
const writes = parseTuples(c.body.writes, 'writes', e, true);
|
|
539
|
+
const deletes = parseTuples(c.body.deletes, 'deletes', e, false);
|
|
540
|
+
if (!writes.length && !deletes.length)
|
|
541
|
+
throw bad('Provide at least one tuple to write or delete.', 'writes');
|
|
542
|
+
if (!(0, authz_layers_1.latestModel)(e, storeId) && writes.some((w) => w.condition))
|
|
543
|
+
throw bad('Write an authorization model before writing conditional tuples.', 'writes');
|
|
544
|
+
const w = c.body.writes && typeof c.body.writes === 'object' && !Array.isArray(c.body.writes) ? c.body.writes : {};
|
|
545
|
+
const d = c.body.deletes && typeof c.body.deletes === 'object' && !Array.isArray(c.body.deletes) ? c.body.deletes : {};
|
|
546
|
+
const onDuplicate = mode(w.on_duplicate ?? c.body.on_duplicate, 'on_duplicate');
|
|
547
|
+
const onMissing = mode(d.on_missing ?? c.body.on_missing, 'on_missing');
|
|
548
|
+
const have = (0, authz_state_1.st)(e).tuples.get(storeId) ?? new Map();
|
|
549
|
+
if (onDuplicate === 'error') {
|
|
550
|
+
const dup = writes.find((t) => have.has((0, authz_state_1.tupleKey)(t)));
|
|
551
|
+
if (dup)
|
|
552
|
+
throw bad(`write_failed_due_to_invalid_input: tuple ${(0, authz_state_1.tupleKey)(dup)} already exists.`, 'writes');
|
|
553
|
+
}
|
|
554
|
+
if (onMissing === 'error') {
|
|
555
|
+
const missing = deletes.find((t) => !have.has((0, authz_state_1.tupleKey)(t)));
|
|
556
|
+
if (missing)
|
|
557
|
+
throw bad(`write_failed_due_to_invalid_input: tuple ${(0, authz_state_1.tupleKey)(missing)} does not exist.`, 'deletes');
|
|
558
|
+
}
|
|
559
|
+
const strict = (0, fga_engine_1.compileStored)((0, authz_layers_1.latestModel)(e, storeId)?.model);
|
|
560
|
+
if (strict) {
|
|
561
|
+
for (const t of writes) {
|
|
562
|
+
const problem = (0, fga_engine_1.validateTuple)(strict, { user: t.user, relation: t.relation, object: t.object, condition: t.condition });
|
|
563
|
+
if (problem)
|
|
564
|
+
throw bad(problem, 'writes');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const revision = (0, authz_state_1.applyTuples)(e, storeId, writes, deletes, 'api');
|
|
568
|
+
// A tuple the API deletes or rewrites is no longer the sync's to manage.
|
|
569
|
+
for (const t of [...deletes, ...writes])
|
|
570
|
+
(0, authz_state_1.st)(e).managed.delete((0, authz_state_1.tupleKey)(t));
|
|
571
|
+
return { body: { object: 'fga.write_result', writes: writes.length, deletes: deletes.length, consistency_token: `${storeId}:${revision}` } };
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
method: 'POST',
|
|
576
|
+
pattern: '/v1/fga/stores/:id/read',
|
|
577
|
+
scope: 'fga:read',
|
|
578
|
+
handler: (c) => {
|
|
579
|
+
const e = c.engine;
|
|
580
|
+
const storeId = store(e, c.params.id);
|
|
581
|
+
const b = c.body;
|
|
582
|
+
const [ot, ...orest] = typeof b.object === 'string' && b.object ? b.object.split(':') : [];
|
|
583
|
+
const oid = orest.join(':');
|
|
584
|
+
const [ut, ...urest] = typeof b.user === 'string' && b.user ? b.user.split(':') : [];
|
|
585
|
+
const uid = urest.join(':');
|
|
586
|
+
const rows = (0, authz_state_1.storeTuples)(e, storeId)
|
|
587
|
+
.filter((t) => {
|
|
588
|
+
const [tot, ...tor] = t.object.split(':');
|
|
589
|
+
const [tut, ...tur] = t.user.split(':');
|
|
590
|
+
return (!b.relation || t.relation === b.relation) && (!ot || tot === ot) && (!oid || tor.join(':') === oid) && (!ut || tut === ut) && (!uid || uid === '*' || uid.includes('#') || tur.join(':') === uid);
|
|
591
|
+
})
|
|
592
|
+
.reverse();
|
|
593
|
+
const limit = b.limit === undefined || b.limit === null || b.limit === '' || !Number.isFinite(Number(b.limit)) ? 1000 : Math.min(Math.max(Math.trunc(Number(b.limit)), 1), 1000);
|
|
594
|
+
return { body: { object: 'list', data: rows.slice(0, limit).map(tupleView), has_more: rows.length > limit, next_cursor: null } };
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
method: 'POST',
|
|
599
|
+
pattern: '/v1/fga/stores/:id/check',
|
|
600
|
+
scope: 'fga:read',
|
|
601
|
+
handler: (c) => {
|
|
602
|
+
const e = c.engine;
|
|
603
|
+
const storeId = store(e, c.params.id);
|
|
604
|
+
const b = c.body;
|
|
605
|
+
if (typeof b.user !== 'string' || typeof b.relation !== 'string' || typeof b.object !== 'string')
|
|
606
|
+
throw bad('check requires string user, relation and object.', 'user');
|
|
607
|
+
const model = typeof b.authorization_model_id === 'string' && b.authorization_model_id ? e.generic.get('fga/stores/authorization-models')?.find((m) => m._parent === storeId && m.id === b.authorization_model_id) : (0, authz_layers_1.latestModel)(e, storeId);
|
|
608
|
+
if (!model) {
|
|
609
|
+
if (b.authorization_model_id)
|
|
610
|
+
throw new errors_1.EmulatorError('NOT_FOUND', 'Unknown authorization model for this store.');
|
|
611
|
+
throw bad('This store has no authorization model yet; write one before running a check.', 'authorization_model_id');
|
|
612
|
+
}
|
|
613
|
+
const contextual = parseTuples(b.contextual_tuples, 'contextual_tuples', e, false);
|
|
614
|
+
const allowed = fgaCheckLogged(e, storeId, model, { user: b.user, relation: b.relation, object: b.object, contextual, context: b.context });
|
|
615
|
+
return { body: { object: 'fga.check', allowed, authorization_model_id: model.id, consistency_token: `${storeId}:${(0, authz_state_1.st)(e).revisions.get(storeId) ?? 0}` } };
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
/** Check, then write the decision to the log the way the real route does so a what-if can replay it. */
|
|
621
|
+
function fgaCheckLogged(e, storeId, modelRow, req) {
|
|
622
|
+
const modelId = modelRow.id;
|
|
623
|
+
const allowed = (0, authz_layers_1.fgaCheck)(e, storeId, { user: req.user, relation: req.relation, object: req.object, contextual: req.contextual, context: req.context && typeof req.context === 'object' ? req.context : undefined, model: modelRow });
|
|
624
|
+
(0, authz_layers_1.logDecision)(e, { source: 'fga', decision: allowed ? 'allow' : 'deny', principal: req.user, action: req.relation, resource: req.object, input: { user: req.user, relation: req.relation, object: req.object, contextual_tuples: req.contextual, context: req.context ?? null }, result: { authorization_model_id: modelId }, policyVersion: { store_id: storeId, authorization_model_id: modelId } });
|
|
625
|
+
return allowed;
|
|
626
|
+
}
|
|
627
|
+
//# sourceMappingURL=authz-data.js.map
|