@stratum-hq/control-plane 0.3.0 → 0.4.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/__tests__/admin-scope-query-string.test.d.ts +2 -0
- package/dist/__tests__/admin-scope-query-string.test.d.ts.map +1 -0
- package/dist/__tests__/admin-scope-query-string.test.js +69 -0
- package/dist/__tests__/admin-scope-query-string.test.js.map +1 -0
- package/dist/__tests__/configdiff-roles-scope.test.d.ts +2 -0
- package/dist/__tests__/configdiff-roles-scope.test.d.ts.map +1 -0
- package/dist/__tests__/configdiff-roles-scope.test.js +246 -0
- package/dist/__tests__/configdiff-roles-scope.test.js.map +1 -0
- package/dist/__tests__/default-deny.test.d.ts +2 -0
- package/dist/__tests__/default-deny.test.d.ts.map +1 -0
- package/dist/__tests__/default-deny.test.js +92 -0
- package/dist/__tests__/default-deny.test.js.map +1 -0
- package/dist/__tests__/tenant-create-scope.test.d.ts +2 -0
- package/dist/__tests__/tenant-create-scope.test.d.ts.map +1 -0
- package/dist/__tests__/tenant-create-scope.test.js +129 -0
- package/dist/__tests__/tenant-create-scope.test.js.map +1 -0
- package/dist/__tests__/tenant-move-scope.test.d.ts +2 -0
- package/dist/__tests__/tenant-move-scope.test.d.ts.map +1 -0
- package/dist/__tests__/tenant-move-scope.test.js +113 -0
- package/dist/__tests__/tenant-move-scope.test.js.map +1 -0
- package/dist/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/test-helpers.js +13 -0
- package/dist/__tests__/test-helpers.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +3 -0
- package/dist/app.js.map +1 -1
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +4 -1
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/tenant-scope.d.ts +70 -6
- package/dist/middleware/tenant-scope.d.ts.map +1 -1
- package/dist/middleware/tenant-scope.js +160 -30
- package/dist/middleware/tenant-scope.js.map +1 -1
- package/dist/routes/abac.js +1 -1
- package/dist/routes/abac.js.map +1 -1
- package/dist/routes/api-keys.d.ts.map +1 -1
- package/dist/routes/api-keys.js +5 -1
- package/dist/routes/api-keys.js.map +1 -1
- package/dist/routes/audit-logs.d.ts.map +1 -1
- package/dist/routes/audit-logs.js +3 -0
- package/dist/routes/audit-logs.js.map +1 -1
- package/dist/routes/config-diff.d.ts.map +1 -1
- package/dist/routes/config-diff.js +8 -1
- package/dist/routes/config-diff.js.map +1 -1
- package/dist/routes/config.js +1 -1
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/consent.js +1 -1
- package/dist/routes/consent.js.map +1 -1
- package/dist/routes/maintenance.d.ts.map +1 -1
- package/dist/routes/maintenance.js +3 -0
- package/dist/routes/maintenance.js.map +1 -1
- package/dist/routes/permissions.js +1 -1
- package/dist/routes/permissions.js.map +1 -1
- package/dist/routes/regions.d.ts.map +1 -1
- package/dist/routes/regions.js +3 -0
- package/dist/routes/regions.js.map +1 -1
- package/dist/routes/roles.d.ts.map +1 -1
- package/dist/routes/roles.js +49 -6
- package/dist/routes/roles.js.map +1 -1
- package/dist/routes/tenants.d.ts.map +1 -1
- package/dist/routes/tenants.js +14 -4
- package/dist/routes/tenants.js.map +1 -1
- package/dist/routes/webhooks.js +1 -1
- package/dist/routes/webhooks.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const test_helpers_js_1 = require("./test-helpers.js");
|
|
5
|
+
/**
|
|
6
|
+
* Regression tests for a critical cross-tenant authorization flaw in the move route.
|
|
7
|
+
*
|
|
8
|
+
* The bug: createTenantScopeGuard was registered with the `fromParamId` extractor,
|
|
9
|
+
* which reads only `:id`, the tenant being MOVED. Nothing authorized
|
|
10
|
+
* `body.new_parent_id`, the move DESTINATION.
|
|
11
|
+
*
|
|
12
|
+
* A key scoped to its own tenant therefore passed the guard on the fast path
|
|
13
|
+
* (`apiKey.tenant_id === targetTenantId`) and could graft itself under any tenant
|
|
14
|
+
* in the system. Because Stratum resolves config by walking UP the ancestry path,
|
|
15
|
+
* the attacker's tenant then inherits the victim's config, including `sensitive`
|
|
16
|
+
* values, which are decrypted on resolve, plus the victim's delegated permissions.
|
|
17
|
+
*
|
|
18
|
+
* The library's moveTenant is not at fault: it locks both rows, rejects archived
|
|
19
|
+
* tenants and detects cycles. Authorization is the control plane's job.
|
|
20
|
+
*/
|
|
21
|
+
const ATTACKER = "11111111-1111-4111-8111-111111111111";
|
|
22
|
+
const ATTACKER_CHILD = "22222222-2222-4222-8222-222222222222";
|
|
23
|
+
const ATTACKER_GRANDCHILD = "33333333-3333-4333-8333-333333333333";
|
|
24
|
+
const VICTIM = "99999999-9999-4999-8999-999999999999";
|
|
25
|
+
// ancestry_path holds a tenant's ANCESTORS and excludes self, so a root is "".
|
|
26
|
+
const TENANTS = {
|
|
27
|
+
[ATTACKER]: { id: ATTACKER, ancestry_path: "", status: "active" },
|
|
28
|
+
[ATTACKER_CHILD]: {
|
|
29
|
+
id: ATTACKER_CHILD,
|
|
30
|
+
ancestry_path: ATTACKER,
|
|
31
|
+
status: "active",
|
|
32
|
+
},
|
|
33
|
+
[ATTACKER_GRANDCHILD]: {
|
|
34
|
+
id: ATTACKER_GRANDCHILD,
|
|
35
|
+
ancestry_path: `${ATTACKER}/${ATTACKER_CHILD}`,
|
|
36
|
+
status: "active",
|
|
37
|
+
},
|
|
38
|
+
[VICTIM]: { id: VICTIM, ancestry_path: "", status: "active" },
|
|
39
|
+
};
|
|
40
|
+
let app;
|
|
41
|
+
let stratum;
|
|
42
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
43
|
+
stratum = (0, test_helpers_js_1.createMockStratum)();
|
|
44
|
+
// A key scoped to the attacker's own tenant. Not a global key.
|
|
45
|
+
stratum.validateApiKey.mockResolvedValue({
|
|
46
|
+
key_id: "attacker-key",
|
|
47
|
+
tenant_id: ATTACKER,
|
|
48
|
+
scopes: ["read", "write", "admin"],
|
|
49
|
+
rate_limit_max: null,
|
|
50
|
+
rate_limit_window: null,
|
|
51
|
+
});
|
|
52
|
+
stratum.getTenant.mockImplementation(async (id) => {
|
|
53
|
+
const row = TENANTS[id];
|
|
54
|
+
if (!row)
|
|
55
|
+
throw new Error(`Tenant ${id} not found`);
|
|
56
|
+
return row;
|
|
57
|
+
});
|
|
58
|
+
stratum.moveTenant.mockImplementation(async (id) => TENANTS[id]);
|
|
59
|
+
app = await (0, test_helpers_js_1.buildTestApp)(stratum);
|
|
60
|
+
});
|
|
61
|
+
async function move(id, newParentId) {
|
|
62
|
+
return app.inject({
|
|
63
|
+
method: "POST",
|
|
64
|
+
url: `/api/v1/tenants/${id}/move`,
|
|
65
|
+
headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
|
|
66
|
+
payload: { new_parent_id: newParentId },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
(0, vitest_1.describe)("move route authorizes the destination parent, not just the moved tenant", () => {
|
|
70
|
+
(0, vitest_1.it)("refuses grafting your own tenant under an unrelated tenant", async () => {
|
|
71
|
+
const res = await move(ATTACKER, VICTIM);
|
|
72
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
73
|
+
(0, vitest_1.expect)(stratum.moveTenant).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)("refuses grafting a tenant you do own under a tenant you do not", async () => {
|
|
76
|
+
const res = await move(ATTACKER_CHILD, VICTIM);
|
|
77
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
78
|
+
(0, vitest_1.expect)(stratum.moveTenant).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)("allows reorganizing entirely inside your own subtree", async () => {
|
|
81
|
+
const res = await move(ATTACKER_GRANDCHILD, ATTACKER_CHILD);
|
|
82
|
+
(0, vitest_1.expect)(res.statusCode).toBe(200);
|
|
83
|
+
(0, vitest_1.expect)(stratum.moveTenant).toHaveBeenCalledWith(ATTACKER_GRANDCHILD, ATTACKER_CHILD, vitest_1.expect.anything());
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)("allows moving a descendant up to your own root", async () => {
|
|
86
|
+
const res = await move(ATTACKER_GRANDCHILD, ATTACKER);
|
|
87
|
+
(0, vitest_1.expect)(res.statusCode).toBe(200);
|
|
88
|
+
(0, vitest_1.expect)(stratum.moveTenant).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)("fails closed when the destination tenant cannot be loaded", async () => {
|
|
91
|
+
const res = await move(ATTACKER, "44444444-4444-4444-8444-444444444444");
|
|
92
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
93
|
+
(0, vitest_1.expect)(stratum.moveTenant).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.describe)("global keys are unaffected", () => {
|
|
97
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
98
|
+
stratum.validateApiKey.mockResolvedValue({
|
|
99
|
+
key_id: "global-key",
|
|
100
|
+
tenant_id: null,
|
|
101
|
+
scopes: ["read", "write", "admin"],
|
|
102
|
+
rate_limit_max: null,
|
|
103
|
+
rate_limit_window: null,
|
|
104
|
+
});
|
|
105
|
+
app = await (0, test_helpers_js_1.buildTestApp)(stratum);
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.it)("still permits an operator key to move a tenant anywhere", async () => {
|
|
108
|
+
const res = await move(ATTACKER, VICTIM);
|
|
109
|
+
(0, vitest_1.expect)(res.statusCode).toBe(200);
|
|
110
|
+
(0, vitest_1.expect)(stratum.moveTenant).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=tenant-move-scope.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-move-scope.test.js","sourceRoot":"","sources":["../../src/__tests__/tenant-move-scope.test.ts"],"names":[],"mappings":";;AAAA,mCAA8D;AAG9D,uDAAiF;AAEjF;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,QAAQ,GAAG,sCAAsC,CAAC;AACxD,MAAM,cAAc,GAAG,sCAAsC,CAAC;AAC9D,MAAM,mBAAmB,GAAG,sCAAsC,CAAC;AACnE,MAAM,MAAM,GAAG,sCAAsC,CAAC;AAItD,+EAA+E;AAC/E,MAAM,OAAO,GAAwB;IACnC,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;IACjE,CAAC,cAAc,CAAC,EAAE;QAChB,EAAE,EAAE,cAAc;QAClB,aAAa,EAAE,QAAQ;QACvB,MAAM,EAAE,QAAQ;KACjB;IACD,CAAC,mBAAmB,CAAC,EAAE;QACrB,EAAE,EAAE,mBAAmB;QACvB,aAAa,EAAE,GAAG,QAAQ,IAAI,cAAc,EAAE;QAC9C,MAAM,EAAE,QAAQ;KACjB;IACD,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;CAC9D,CAAC;AAEF,IAAI,GAAoB,CAAC;AACzB,IAAI,OAAgB,CAAC;AAErB,IAAA,mBAAU,EAAC,KAAK,IAAI,EAAE;IACpB,OAAO,GAAG,IAAA,mCAAiB,GAAE,CAAC;IAE9B,+DAA+D;IAC9D,OAAO,CAAC,cAA2C,CAAC,iBAAiB,CAAC;QACrE,MAAM,EAAE,cAAc;QACtB,SAAS,EAAE,QAAQ;QACnB,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;QAClC,cAAc,EAAE,IAAI;QACpB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC;IAEF,OAAO,CAAC,SAAsC,CAAC,kBAAkB,CAChE,KAAK,EAAE,EAAU,EAAE,EAAE;QACnB,MAAM,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACpD,OAAO,GAAG,CAAC;IACb,CAAC,CACF,CAAC;IAED,OAAO,CAAC,UAAuC,CAAC,kBAAkB,CACjE,KAAK,EAAE,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAClC,CAAC;IAEF,GAAG,GAAG,MAAM,IAAA,8BAAY,EAAC,OAAO,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI,CAAC,EAAU,EAAE,WAAmB;IACjD,OAAO,GAAG,CAAC,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,mBAAmB,EAAE,OAAO;QACjC,OAAO,EAAE,EAAE,GAAG,IAAA,6BAAW,GAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QACjE,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;KACxC,CAAC,CAAC;AACL,CAAC;AAED,IAAA,iBAAQ,EAAC,yEAAyE,EAAE,GAAG,EAAE;IACvF,IAAA,WAAE,EAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC1E,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAEzC,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAE/C,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC;QAE5D,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,oBAAoB,CAC7C,mBAAmB,EACnB,cAAc,EACd,eAAM,CAAC,QAAQ,EAAE,CAClB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;QAEtD,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,sCAAsC,CAAC,CAAC;QAEzE,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,IAAA,mBAAU,EAAC,KAAK,IAAI,EAAE;QACnB,OAAO,CAAC,cAA2C,CAAC,iBAAiB,CAAC;YACrE,MAAM,EAAE,YAAY;YACpB,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;YAClC,cAAc,EAAE,IAAI;YACpB,iBAAiB,EAAE,IAAI;SACxB,CAAC,CAAC;QACH,GAAG,GAAG,MAAM,IAAA,8BAAY,EAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAEzC,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,UAAU,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../src/__tests__/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAanD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,eAAO,MAAM,eAAe,mCAAmC,CAAC;AAEhE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CA8C3C;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAoB7E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,GAAE,MAA4B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAExF;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,MAAM,GAAE,MAAwB,GAC/B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYxB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQvD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQ1D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQvD;AAED,iDAAiD;AACjD,eAAO,MAAM,aAAa;;;;;;;;;;;;;CAazB,CAAC;AAEF,qCAAqC;AACrC,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;CAa/B,CAAC"}
|
|
@@ -17,11 +17,13 @@ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
|
17
17
|
const error_handler_js_1 = require("../middleware/error-handler.js");
|
|
18
18
|
const auth_js_1 = require("../middleware/auth.js");
|
|
19
19
|
const authorize_js_1 = require("../middleware/authorize.js");
|
|
20
|
+
const tenant_scope_js_1 = require("../middleware/tenant-scope.js");
|
|
20
21
|
const health_js_1 = require("../routes/health.js");
|
|
21
22
|
const tenants_js_1 = require("../routes/tenants.js");
|
|
22
23
|
const config_js_1 = require("../routes/config.js");
|
|
23
24
|
const api_keys_js_1 = require("../routes/api-keys.js");
|
|
24
25
|
const config_diff_js_1 = require("../routes/config-diff.js");
|
|
26
|
+
const roles_js_1 = require("../routes/roles.js");
|
|
25
27
|
// Must match the JWT_SECRET env var set in vitest.config.ts
|
|
26
28
|
exports.TEST_JWT_SECRET = "test-jwt-secret-for-unit-tests";
|
|
27
29
|
/**
|
|
@@ -59,7 +61,16 @@ function createMockStratum() {
|
|
|
59
61
|
revokeApiKey: vitest_1.vi.fn(),
|
|
60
62
|
rotateApiKey: vitest_1.vi.fn(),
|
|
61
63
|
listApiKeys: vitest_1.vi.fn(),
|
|
64
|
+
getApiKey: vitest_1.vi.fn(),
|
|
62
65
|
listDormantKeys: vitest_1.vi.fn(),
|
|
66
|
+
// Role methods
|
|
67
|
+
createRole: vitest_1.vi.fn(),
|
|
68
|
+
getRole: vitest_1.vi.fn(),
|
|
69
|
+
listRoles: vitest_1.vi.fn(),
|
|
70
|
+
updateRole: vitest_1.vi.fn(),
|
|
71
|
+
deleteRole: vitest_1.vi.fn(),
|
|
72
|
+
assignRoleToKey: vitest_1.vi.fn(),
|
|
73
|
+
removeRoleFromKey: vitest_1.vi.fn(),
|
|
63
74
|
};
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
@@ -74,6 +85,7 @@ async function buildTestApp(stratum) {
|
|
|
74
85
|
// Wire up the same middleware chain as the real app
|
|
75
86
|
app.addHook("preHandler", (0, auth_js_1.createAuthMiddleware)(stratum));
|
|
76
87
|
app.addHook("preHandler", (0, authorize_js_1.createAuthorizeMiddleware)());
|
|
88
|
+
app.addHook("preHandler", (0, tenant_scope_js_1.createTenantScopeEnforcer)(stratum));
|
|
77
89
|
app.setErrorHandler(error_handler_js_1.errorHandler);
|
|
78
90
|
// Register routes (pass a stub Redis health checker — Redis is not used in tests)
|
|
79
91
|
const noopRedisHealth = async () => "not_configured";
|
|
@@ -82,6 +94,7 @@ async function buildTestApp(stratum) {
|
|
|
82
94
|
await app.register((0, config_js_1.createConfigRoutes)(stratum), { prefix: "/api/v1/tenants/:id/config" });
|
|
83
95
|
await app.register((0, api_keys_js_1.createApiKeyRoutes)(stratum), { prefix: "/api/v1/api-keys" });
|
|
84
96
|
await app.register((0, config_diff_js_1.createConfigDiffRoutes)(stratum), { prefix: "/api/v1/config" });
|
|
97
|
+
await app.register((0, roles_js_1.createRoleRoutes)(stratum), { prefix: "/api/v1/roles" });
|
|
85
98
|
await app.ready();
|
|
86
99
|
return app;
|
|
87
100
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../../src/__tests__/test-helpers.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../../src/__tests__/test-helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AA4CnD,wBAAsB,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC,CA4EzD"}
|
package/dist/app.js
CHANGED
|
@@ -14,6 +14,7 @@ const openapi_js_1 = require("./openapi.js");
|
|
|
14
14
|
const error_handler_js_1 = require("./middleware/error-handler.js");
|
|
15
15
|
const auth_js_1 = require("./middleware/auth.js");
|
|
16
16
|
const authorize_js_1 = require("./middleware/authorize.js");
|
|
17
|
+
const tenant_scope_js_1 = require("./middleware/tenant-scope.js");
|
|
17
18
|
const per_key_rate_limit_js_1 = require("./middleware/per-key-rate-limit.js");
|
|
18
19
|
const rate_limit_redis_js_1 = require("./middleware/rate-limit-redis.js");
|
|
19
20
|
const redis_js_1 = require("./redis.js");
|
|
@@ -82,6 +83,8 @@ async function buildApp() {
|
|
|
82
83
|
await (0, openapi_js_1.registerOpenApi)(app);
|
|
83
84
|
app.addHook("preHandler", (0, auth_js_1.createAuthMiddleware)(stratum));
|
|
84
85
|
app.addHook("preHandler", (0, authorize_js_1.createAuthorizeMiddleware)());
|
|
86
|
+
// Default-deny: a route that declares no tenant scope is refused.
|
|
87
|
+
app.addHook("preHandler", (0, tenant_scope_js_1.createTenantScopeEnforcer)(stratum));
|
|
85
88
|
// Use Redis-backed rate limiting when REDIS_URL is configured, otherwise in-memory
|
|
86
89
|
const redisClient = (0, redis_js_1.createRedisClient)();
|
|
87
90
|
if (redisClient) {
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAwCvD,wBAAgB,yBAAyB,KAErC,SAAS,cAAc,EACvB,OAAO,YAAY,KAClB,OAAO,CAAC,IAAI,CAAC,CAsBjB"}
|
|
@@ -24,8 +24,11 @@ const ADMIN_ROUTES = [
|
|
|
24
24
|
/^\/api\/v1\/tenants\/[^/]+\/context$/,
|
|
25
25
|
];
|
|
26
26
|
function getRequiredScope(method, url) {
|
|
27
|
+
// Match on the request path. Take everything before the first "?" so the
|
|
28
|
+
// route match is evaluated on the normalized path alone.
|
|
29
|
+
const path = url.split("?", 1)[0];
|
|
27
30
|
for (const pattern of ADMIN_ROUTES) {
|
|
28
|
-
if (pattern.test(
|
|
31
|
+
if (pattern.test(path)) {
|
|
29
32
|
return "admin";
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/middleware/authorize.ts"],"names":[],"mappings":";;AAwCA,8DA0BC;AAjED,2CAAqE;AAIrE,SAAS,aAAa,CAAC,MAAc;IACnC,QAAQ,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;QAC7B,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,YAAY,GAAG;IACnB,sBAAsB;IACtB,wBAAwB;IACxB,yBAAyB;IACzB,qBAAqB;IACrB,oCAAoC;IACpC,qCAAqC;IACrC,6CAA6C;IAC7C,sCAAsC;CACvC,CAAC;AAEF,SAAS,gBAAgB,CAAC,MAAc,EAAE,GAAW;IACnD,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO,KAAK,UAAU,mBAAmB,CACvC,OAAuB,EACvB,KAAmB;QAEnB,8CAA8C;QAC9C,IACE,OAAO,CAAC,GAAG,KAAK,gBAAgB;YAChC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,iBAAiB,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,EACnC,CAAC;YACD,OAAO;QACT,CAAC;QAED,mEAAmE;QACnE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAiB,CAAC,yBAAyB,CAAC,CAAC;QACzD,CAAC;QAED,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,qBAAc,CAAC,6CAA6C,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1,20 +1,84 @@
|
|
|
1
|
-
import { FastifyRequest, FastifyReply } from "fastify";
|
|
1
|
+
import { FastifyRequest, FastifyReply, FastifyInstance } from "fastify";
|
|
2
2
|
import { Stratum } from "@stratum-hq/lib";
|
|
3
|
+
/** Reads the target tenant id from a request, or null when there is none to check. */
|
|
4
|
+
export type TenantIdExtractor = (req: FastifyRequest) => string | null;
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* A route's tenant-scope declaration:
|
|
7
|
+
* - an extractor: enforce that the caller's key may reach the extracted tenant;
|
|
8
|
+
* - "global": operator-level route with no single tenant target, or one that
|
|
9
|
+
* scopes itself in its handler. The middleware performs no tenant check.
|
|
10
|
+
*/
|
|
11
|
+
export type TenantScopeDeclaration = TenantIdExtractor | "global";
|
|
12
|
+
declare module "fastify" {
|
|
13
|
+
interface FastifyContextConfig {
|
|
14
|
+
tenantScope?: TenantScopeDeclaration;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Core tenant-scope check for a single, already-resolved tenant id.
|
|
19
|
+
*
|
|
20
|
+
* Ensures that tenant-scoped API keys can only access data belonging to their
|
|
21
|
+
* own tenant or its descendants in the hierarchy. Global keys (tenant_id ===
|
|
22
|
+
* null) have unrestricted access. A null `tenantId` means there is no tenant to
|
|
23
|
+
* check (for example a global role, or an omitted optional filter) and passes.
|
|
24
|
+
*
|
|
25
|
+
* Exported for routes whose target tenant is not present in the request and
|
|
26
|
+
* must be resolved first (for example a role's owning tenant, looked up by id).
|
|
27
|
+
*/
|
|
28
|
+
export declare function assertTenantInScope(stratum: Stratum, request: FastifyRequest, tenantId: string | null): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Tenant-scope enforcement middleware for a single extractor.
|
|
31
|
+
*
|
|
32
|
+
* Retained for routes that must authorize a second tenant beyond the one the
|
|
33
|
+
* global enforcer already covers (for example, a move's destination parent).
|
|
34
|
+
*/
|
|
35
|
+
export declare function createTenantScopeGuard(stratum: Stratum, extractTenantId: TenantIdExtractor): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Guard for POST /tenants: constrain a scoped key's new tenant to its subtree.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createTenantCreateGuard(stratum: Stratum): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Guard for POST /tenants/batch: every tenant in the batch is authorized the
|
|
42
|
+
* same way a single create is, so the batch route cannot sidestep the subtree
|
|
43
|
+
* constraint.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createTenantBatchCreateGuard(stratum: Stratum): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Declare the tenant scope for every route registered in the current plugin.
|
|
5
48
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
49
|
+
* Stamps the declaration onto each route's config so the global enforcer can
|
|
50
|
+
* read it. A route may still set its own `config.tenantScope`, which wins.
|
|
51
|
+
*/
|
|
52
|
+
export declare function declareTenantScope(app: FastifyInstance, declaration: TenantScopeDeclaration): void;
|
|
53
|
+
/**
|
|
54
|
+
* Default-deny authorization enforcer, registered once as a global preHandler.
|
|
8
55
|
*
|
|
9
|
-
*
|
|
56
|
+
* Every route must declare its tenant scope (see {@link declareTenantScope}). A
|
|
57
|
+
* route that declares none is refused, so a route added without a guard fails
|
|
58
|
+
* closed rather than silently serving data.
|
|
10
59
|
*/
|
|
11
|
-
export declare function
|
|
60
|
+
export declare function createTenantScopeEnforcer(stratum: Stratum): (request: FastifyRequest, _reply: FastifyReply) => Promise<void>;
|
|
12
61
|
/** Extract :id param (tenant routes, config routes, permission routes) */
|
|
13
62
|
export declare function fromParamId(req: FastifyRequest): string | null;
|
|
14
63
|
/** Extract :tenantId param (consent routes) */
|
|
15
64
|
export declare function fromParamTenantId(req: FastifyRequest): string | null;
|
|
16
65
|
/** Extract tenant_id from query string */
|
|
17
66
|
export declare function fromQueryTenantId(req: FastifyRequest): string | null;
|
|
67
|
+
/** Extract tenant_a from query string (config diff, first operand) */
|
|
68
|
+
export declare function fromQueryTenantA(req: FastifyRequest): string | null;
|
|
69
|
+
/** Extract tenant_b from query string (config diff, second operand) */
|
|
70
|
+
export declare function fromQueryTenantB(req: FastifyRequest): string | null;
|
|
18
71
|
/** Extract tenant_id from request body */
|
|
19
72
|
export declare function fromBodyTenantId(req: FastifyRequest): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Extract new_parent_id from a move request body: the move DESTINATION.
|
|
75
|
+
*
|
|
76
|
+
* A move has two tenants to authorize, not one. The route-level `:id` guard
|
|
77
|
+
* only covers the tenant being moved, which a scoped key already owns, so it
|
|
78
|
+
* passes on the fast path. Without authorizing the destination as well, a
|
|
79
|
+
* tenant can graft itself under any other tenant and inherit that tenant's
|
|
80
|
+
* resolved config (including decrypted `sensitive` values) and delegated
|
|
81
|
+
* permissions, because resolution walks UP the ancestry path.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fromBodyNewParentId(req: FastifyRequest): string | null;
|
|
20
84
|
//# sourceMappingURL=tenant-scope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-scope.d.ts","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"tenant-scope.d.ts","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE1C,sFAAsF;AACtF,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,GAAG,QAAQ,CAAC;AAElE,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,oBAAoB;QAC5B,WAAW,CAAC,EAAE,sBAAsB,CAAC;KACtC;CACF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,IAAI,CAAC,CAgCf;AAaD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,iBAAiB,IAGhC,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAGjB;AA6BD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,IAEpD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAIjB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,IAEzD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CASjB;AAYD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,sBAAsB,GAClC,IAAI,CAON;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,IAEtD,SAAS,cAAc,EACvB,QAAQ,YAAY,KACnB,OAAO,CAAC,IAAI,CAAC,CAWjB;AAID,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAE9D;AAED,+CAA+C;AAC/C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,0CAA0C;AAC1C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAEtE"}
|
|
@@ -1,50 +1,159 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertTenantInScope = assertTenantInScope;
|
|
3
4
|
exports.createTenantScopeGuard = createTenantScopeGuard;
|
|
5
|
+
exports.createTenantCreateGuard = createTenantCreateGuard;
|
|
6
|
+
exports.createTenantBatchCreateGuard = createTenantBatchCreateGuard;
|
|
7
|
+
exports.declareTenantScope = declareTenantScope;
|
|
8
|
+
exports.createTenantScopeEnforcer = createTenantScopeEnforcer;
|
|
4
9
|
exports.fromParamId = fromParamId;
|
|
5
10
|
exports.fromParamTenantId = fromParamTenantId;
|
|
6
11
|
exports.fromQueryTenantId = fromQueryTenantId;
|
|
12
|
+
exports.fromQueryTenantA = fromQueryTenantA;
|
|
13
|
+
exports.fromQueryTenantB = fromQueryTenantB;
|
|
7
14
|
exports.fromBodyTenantId = fromBodyTenantId;
|
|
15
|
+
exports.fromBodyNewParentId = fromBodyNewParentId;
|
|
8
16
|
const core_1 = require("@stratum-hq/core");
|
|
9
17
|
/**
|
|
10
|
-
*
|
|
18
|
+
* Core tenant-scope check for a single, already-resolved tenant id.
|
|
11
19
|
*
|
|
12
|
-
* Ensures that tenant-scoped API keys can only access data belonging
|
|
13
|
-
*
|
|
20
|
+
* Ensures that tenant-scoped API keys can only access data belonging to their
|
|
21
|
+
* own tenant or its descendants in the hierarchy. Global keys (tenant_id ===
|
|
22
|
+
* null) have unrestricted access. A null `tenantId` means there is no tenant to
|
|
23
|
+
* check (for example a global role, or an omitted optional filter) and passes.
|
|
14
24
|
*
|
|
15
|
-
*
|
|
25
|
+
* Exported for routes whose target tenant is not present in the request and
|
|
26
|
+
* must be resolved first (for example a role's owning tenant, looked up by id).
|
|
27
|
+
*/
|
|
28
|
+
async function assertTenantInScope(stratum, request, tenantId) {
|
|
29
|
+
const apiKey = request.apiKey;
|
|
30
|
+
if (!apiKey)
|
|
31
|
+
return;
|
|
32
|
+
// Only API keys may have global (null tenant_id) access.
|
|
33
|
+
// JWT auth should never reach here with null tenant_id after the auth middleware
|
|
34
|
+
// fix, but guard defensively anyway.
|
|
35
|
+
if (apiKey.tenant_id === null) {
|
|
36
|
+
if (request.authMethod === "api_key")
|
|
37
|
+
return;
|
|
38
|
+
throw new core_1.ForbiddenError("JWT authentication requires a tenant scope");
|
|
39
|
+
}
|
|
40
|
+
if (!tenantId)
|
|
41
|
+
return;
|
|
42
|
+
// Fast path: exact match
|
|
43
|
+
if (apiKey.tenant_id === tenantId)
|
|
44
|
+
return;
|
|
45
|
+
// Hierarchy check: target must be a descendant of the key's tenant
|
|
46
|
+
try {
|
|
47
|
+
const target = await stratum.getTenant(tenantId);
|
|
48
|
+
const ancestorIds = target.ancestry_path.split("/").filter(Boolean);
|
|
49
|
+
if (ancestorIds.includes(apiKey.tenant_id))
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Tenant not found: fail closed for scoped keys
|
|
54
|
+
throw new core_1.ForbiddenError("API key tenant scope does not grant access to this tenant");
|
|
55
|
+
}
|
|
56
|
+
throw new core_1.ForbiddenError("API key tenant scope does not grant access to this tenant");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Core tenant-scope check for a tenant id read from the request by `extractTenantId`.
|
|
60
|
+
*/
|
|
61
|
+
async function checkTenantScope(stratum, request, extractTenantId) {
|
|
62
|
+
await assertTenantInScope(stratum, request, extractTenantId(request));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Tenant-scope enforcement middleware for a single extractor.
|
|
66
|
+
*
|
|
67
|
+
* Retained for routes that must authorize a second tenant beyond the one the
|
|
68
|
+
* global enforcer already covers (for example, a move's destination parent).
|
|
16
69
|
*/
|
|
17
70
|
function createTenantScopeGuard(stratum, extractTenantId) {
|
|
18
71
|
return async function tenantScopeGuard(request, _reply) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
72
|
+
await checkTenantScope(stratum, request, extractTenantId);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Authorize a single tenant CREATE against the caller's key scope.
|
|
77
|
+
*
|
|
78
|
+
* A scoped key may only create tenants within its own subtree, so the new
|
|
79
|
+
* tenant's parent must be the key's own tenant or a descendant of it. A scoped
|
|
80
|
+
* key may not create a new root (a tenant with no parent). Global (operator)
|
|
81
|
+
* keys are unrestricted and may create roots anywhere.
|
|
82
|
+
*/
|
|
83
|
+
async function authorizeCreateUnder(stratum, request, parentId) {
|
|
84
|
+
const apiKey = request.apiKey;
|
|
85
|
+
if (!apiKey)
|
|
86
|
+
return;
|
|
87
|
+
// Only global API keys (null tenant_id) may create roots or create anywhere.
|
|
88
|
+
if (apiKey.tenant_id === null && request.authMethod === "api_key")
|
|
89
|
+
return;
|
|
90
|
+
if (!parentId) {
|
|
91
|
+
throw new core_1.ForbiddenError("API key tenant scope may not create root tenants");
|
|
92
|
+
}
|
|
93
|
+
await checkTenantScope(stratum, request, () => parentId);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Guard for POST /tenants: constrain a scoped key's new tenant to its subtree.
|
|
97
|
+
*/
|
|
98
|
+
function createTenantCreateGuard(stratum) {
|
|
99
|
+
return async function tenantCreateGuard(request, _reply) {
|
|
100
|
+
const body = request.body;
|
|
101
|
+
await authorizeCreateUnder(stratum, request, body?.parent_id ?? null);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Guard for POST /tenants/batch: every tenant in the batch is authorized the
|
|
106
|
+
* same way a single create is, so the batch route cannot sidestep the subtree
|
|
107
|
+
* constraint.
|
|
108
|
+
*/
|
|
109
|
+
function createTenantBatchCreateGuard(stratum) {
|
|
110
|
+
return async function tenantBatchCreateGuard(request, _reply) {
|
|
111
|
+
const body = request.body;
|
|
112
|
+
const tenants = Array.isArray(body?.tenants) ? body.tenants : [];
|
|
113
|
+
for (const tenant of tenants) {
|
|
114
|
+
await authorizeCreateUnder(stratum, request, tenant?.parent_id ?? null);
|
|
29
115
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Endpoints that carry no authentication and therefore no tenant scope.
|
|
119
|
+
// Kept in step with the same skip list in the auth and authorize middleware.
|
|
120
|
+
function isUnauthenticatedPath(url) {
|
|
121
|
+
return (url === "/api/v1/health" ||
|
|
122
|
+
url.startsWith("/api/v1/health?") ||
|
|
123
|
+
url.startsWith("/api/docs"));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Declare the tenant scope for every route registered in the current plugin.
|
|
127
|
+
*
|
|
128
|
+
* Stamps the declaration onto each route's config so the global enforcer can
|
|
129
|
+
* read it. A route may still set its own `config.tenantScope`, which wins.
|
|
130
|
+
*/
|
|
131
|
+
function declareTenantScope(app, declaration) {
|
|
132
|
+
app.addHook("onRoute", (routeOptions) => {
|
|
133
|
+
routeOptions.config = {
|
|
134
|
+
...routeOptions.config,
|
|
135
|
+
tenantScope: routeOptions.config?.tenantScope ?? declaration,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Default-deny authorization enforcer, registered once as a global preHandler.
|
|
141
|
+
*
|
|
142
|
+
* Every route must declare its tenant scope (see {@link declareTenantScope}). A
|
|
143
|
+
* route that declares none is refused, so a route added without a guard fails
|
|
144
|
+
* closed rather than silently serving data.
|
|
145
|
+
*/
|
|
146
|
+
function createTenantScopeEnforcer(stratum) {
|
|
147
|
+
return async function tenantScopeEnforcer(request, _reply) {
|
|
148
|
+
if (isUnauthenticatedPath(request.url))
|
|
35
149
|
return;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const ancestorIds = target.ancestry_path.split("/").filter(Boolean);
|
|
40
|
-
if (ancestorIds.includes(apiKey.tenant_id))
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Tenant not found — fail closed for scoped keys
|
|
45
|
-
throw new core_1.ForbiddenError("API key tenant scope does not grant access to this tenant");
|
|
150
|
+
const declaration = request.routeOptions?.config?.tenantScope;
|
|
151
|
+
if (declaration === undefined) {
|
|
152
|
+
throw new core_1.ForbiddenError("Route has no tenant-scope declaration");
|
|
46
153
|
}
|
|
47
|
-
|
|
154
|
+
if (declaration === "global")
|
|
155
|
+
return;
|
|
156
|
+
await checkTenantScope(stratum, request, declaration);
|
|
48
157
|
};
|
|
49
158
|
}
|
|
50
159
|
// --- Extractors ---
|
|
@@ -60,8 +169,29 @@ function fromParamTenantId(req) {
|
|
|
60
169
|
function fromQueryTenantId(req) {
|
|
61
170
|
return req.query.tenant_id ?? null;
|
|
62
171
|
}
|
|
172
|
+
/** Extract tenant_a from query string (config diff, first operand) */
|
|
173
|
+
function fromQueryTenantA(req) {
|
|
174
|
+
return req.query.tenant_a ?? null;
|
|
175
|
+
}
|
|
176
|
+
/** Extract tenant_b from query string (config diff, second operand) */
|
|
177
|
+
function fromQueryTenantB(req) {
|
|
178
|
+
return req.query.tenant_b ?? null;
|
|
179
|
+
}
|
|
63
180
|
/** Extract tenant_id from request body */
|
|
64
181
|
function fromBodyTenantId(req) {
|
|
65
182
|
return req.body?.tenant_id ?? null;
|
|
66
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Extract new_parent_id from a move request body: the move DESTINATION.
|
|
186
|
+
*
|
|
187
|
+
* A move has two tenants to authorize, not one. The route-level `:id` guard
|
|
188
|
+
* only covers the tenant being moved, which a scoped key already owns, so it
|
|
189
|
+
* passes on the fast path. Without authorizing the destination as well, a
|
|
190
|
+
* tenant can graft itself under any other tenant and inherit that tenant's
|
|
191
|
+
* resolved config (including decrypted `sensitive` values) and delegated
|
|
192
|
+
* permissions, because resolution walks UP the ancestry path.
|
|
193
|
+
*/
|
|
194
|
+
function fromBodyNewParentId(req) {
|
|
195
|
+
return req.body?.new_parent_id ?? null;
|
|
196
|
+
}
|
|
67
197
|
//# sourceMappingURL=tenant-scope.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-scope.js","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"tenant-scope.js","sourceRoot":"","sources":["../../src/middleware/tenant-scope.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/abac.js
CHANGED
|
@@ -6,7 +6,7 @@ const tenant_scope_js_1 = require("../middleware/tenant-scope.js");
|
|
|
6
6
|
function createAbacRoutes(stratum) {
|
|
7
7
|
return async function abacRoutes(app) {
|
|
8
8
|
// Tenant-scoped keys can only access ABAC policies for their own tenant subtree
|
|
9
|
-
|
|
9
|
+
(0, tenant_scope_js_1.declareTenantScope)(app, tenant_scope_js_1.fromParamTenantId);
|
|
10
10
|
// POST /api/v1/tenants/:tenantId/abac-policies — Create ABAC policy
|
|
11
11
|
app.post("/", async (request, reply) => {
|
|
12
12
|
const input = core_1.CreateAbacPolicyInputSchema.parse(request.body);
|