@stratum-hq/control-plane 0.4.0 → 1.0.1

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.
Files changed (52) hide show
  1. package/dist/app.js.map +1 -1
  2. package/dist/middleware/auth.d.ts +1 -1
  3. package/dist/middleware/auth.d.ts.map +1 -1
  4. package/dist/middleware/auth.js +1 -1
  5. package/dist/middleware/auth.js.map +1 -1
  6. package/dist/middleware/authorize.d.ts +1 -1
  7. package/dist/middleware/authorize.d.ts.map +1 -1
  8. package/dist/middleware/authorize.js +4 -2
  9. package/dist/middleware/authorize.js.map +1 -1
  10. package/dist/openapi.js.map +1 -1
  11. package/dist/routes/tenants.js.map +1 -1
  12. package/package.json +16 -3
  13. package/dist/__tests__/admin-scope-query-string.test.d.ts +0 -2
  14. package/dist/__tests__/admin-scope-query-string.test.d.ts.map +0 -1
  15. package/dist/__tests__/admin-scope-query-string.test.js +0 -69
  16. package/dist/__tests__/admin-scope-query-string.test.js.map +0 -1
  17. package/dist/__tests__/api-keys.test.d.ts +0 -2
  18. package/dist/__tests__/api-keys.test.d.ts.map +0 -1
  19. package/dist/__tests__/api-keys.test.js +0 -240
  20. package/dist/__tests__/api-keys.test.js.map +0 -1
  21. package/dist/__tests__/auth.test.d.ts +0 -2
  22. package/dist/__tests__/auth.test.d.ts.map +0 -1
  23. package/dist/__tests__/auth.test.js +0 -229
  24. package/dist/__tests__/auth.test.js.map +0 -1
  25. package/dist/__tests__/config.test.d.ts +0 -2
  26. package/dist/__tests__/config.test.d.ts.map +0 -1
  27. package/dist/__tests__/config.test.js +0 -256
  28. package/dist/__tests__/config.test.js.map +0 -1
  29. package/dist/__tests__/configdiff-roles-scope.test.d.ts +0 -2
  30. package/dist/__tests__/configdiff-roles-scope.test.d.ts.map +0 -1
  31. package/dist/__tests__/configdiff-roles-scope.test.js +0 -246
  32. package/dist/__tests__/configdiff-roles-scope.test.js.map +0 -1
  33. package/dist/__tests__/default-deny.test.d.ts +0 -2
  34. package/dist/__tests__/default-deny.test.d.ts.map +0 -1
  35. package/dist/__tests__/default-deny.test.js +0 -92
  36. package/dist/__tests__/default-deny.test.js.map +0 -1
  37. package/dist/__tests__/tenant-create-scope.test.d.ts +0 -2
  38. package/dist/__tests__/tenant-create-scope.test.d.ts.map +0 -1
  39. package/dist/__tests__/tenant-create-scope.test.js +0 -129
  40. package/dist/__tests__/tenant-create-scope.test.js.map +0 -1
  41. package/dist/__tests__/tenant-move-scope.test.d.ts +0 -2
  42. package/dist/__tests__/tenant-move-scope.test.d.ts.map +0 -1
  43. package/dist/__tests__/tenant-move-scope.test.js +0 -113
  44. package/dist/__tests__/tenant-move-scope.test.js.map +0 -1
  45. package/dist/__tests__/tenants.test.d.ts +0 -2
  46. package/dist/__tests__/tenants.test.d.ts.map +0 -1
  47. package/dist/__tests__/tenants.test.js +0 -214
  48. package/dist/__tests__/tenants.test.js.map +0 -1
  49. package/dist/__tests__/test-helpers.d.ts +0 -71
  50. package/dist/__tests__/test-helpers.d.ts.map +0 -1
  51. package/dist/__tests__/test-helpers.js +0 -189
  52. package/dist/__tests__/test-helpers.js.map +0 -1
@@ -1,246 +0,0 @@
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 cross-tenant authorization on the config-diff and role
7
- * administration routes.
8
- *
9
- * Both route groups were declared "global" for the default-deny enforcer, which
10
- * is behavior-preserving but performs no per-tenant check. That left a
11
- * tenant-scoped API key able to reach tenants outside its own subtree:
12
- * - config diff compares two tenants read from the query string;
13
- * - role routes read/mutate roles owned by an arbitrary tenant.
14
- *
15
- * A scoped key must be confined to its own tenant and descendants. Global
16
- * operator keys (tenant_id === null) keep unrestricted access.
17
- */
18
- const ATTACKER = "11111111-1111-4111-8111-111111111111";
19
- const ATTACKER_CHILD = "22222222-2222-4222-8222-222222222222";
20
- const VICTIM = "99999999-9999-4999-8999-999999999999";
21
- // ancestry_path holds a tenant's ANCESTORS and excludes self, so a root is "".
22
- const TENANTS = {
23
- [ATTACKER]: { id: ATTACKER, ancestry_path: "", status: "active" },
24
- [ATTACKER_CHILD]: { id: ATTACKER_CHILD, ancestry_path: ATTACKER, status: "active" },
25
- [VICTIM]: { id: VICTIM, ancestry_path: "", status: "active" },
26
- };
27
- const ROLE_OWN = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
28
- const ROLE_FOREIGN = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
29
- const ROLE_GLOBAL = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
30
- const ROLES = {
31
- [ROLE_OWN]: { id: ROLE_OWN, name: "own", description: null, scopes: ["read"], tenant_id: ATTACKER },
32
- [ROLE_FOREIGN]: { id: ROLE_FOREIGN, name: "foreign", description: null, scopes: ["read"], tenant_id: VICTIM },
33
- [ROLE_GLOBAL]: { id: ROLE_GLOBAL, name: "global", description: null, scopes: ["read"], tenant_id: null },
34
- };
35
- const OWN_KEY = "own-key-id";
36
- const FOREIGN_KEY = "foreign-key-id";
37
- const KEYS = {
38
- [OWN_KEY]: { id: OWN_KEY, tenant_id: ATTACKER },
39
- [FOREIGN_KEY]: { id: FOREIGN_KEY, tenant_id: VICTIM },
40
- };
41
- let app;
42
- let stratum;
43
- function wireCommonMocks() {
44
- stratum.getTenant.mockImplementation(async (id) => {
45
- const row = TENANTS[id];
46
- if (!row)
47
- throw new Error(`Tenant ${id} not found`);
48
- return row;
49
- });
50
- stratum.diffConfig.mockResolvedValue({ added: {}, removed: {}, changed: {} });
51
- stratum.getRole.mockImplementation(async (id) => ROLES[id] ?? null);
52
- // Without a tenant filter, listing returns every tenant's roles: this is the leak.
53
- stratum.listRoles.mockImplementation(async (tenantId) => {
54
- const all = Object.values(ROLES);
55
- if (!tenantId)
56
- return all;
57
- return all.filter((r) => r.tenant_id === tenantId || r.tenant_id === null);
58
- });
59
- stratum.createRole.mockImplementation(async (input) => ({
60
- id: "new-role",
61
- description: null,
62
- ...input,
63
- }));
64
- stratum.updateRole.mockImplementation(async (id) => ROLES[id] ?? null);
65
- stratum.deleteRole.mockResolvedValue(true);
66
- stratum.assignRoleToKey.mockResolvedValue(true);
67
- stratum.removeRoleFromKey.mockResolvedValue(true);
68
- stratum.getApiKey.mockImplementation(async (id) => KEYS[id] ?? null);
69
- }
70
- async function diff(tenantA, tenantB) {
71
- return app.inject({
72
- method: "GET",
73
- url: `/api/v1/config/diff?tenant_a=${tenantA}&tenant_b=${tenantB}`,
74
- headers: (0, test_helpers_js_1.authHeaders)(),
75
- });
76
- }
77
- (0, vitest_1.describe)("config diff scopes both compared tenants to the caller's subtree", () => {
78
- (0, vitest_1.beforeEach)(async () => {
79
- stratum = (0, test_helpers_js_1.createMockStratum)();
80
- stratum.validateApiKey.mockResolvedValue({
81
- key_id: "attacker-key",
82
- tenant_id: ATTACKER,
83
- scopes: ["read", "write", "admin"],
84
- rate_limit_max: null,
85
- rate_limit_window: null,
86
- });
87
- wireCommonMocks();
88
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
89
- });
90
- (0, vitest_1.it)("refuses diffing against an unrelated tenant", async () => {
91
- const res = await diff(ATTACKER, VICTIM);
92
- (0, vitest_1.expect)(res.statusCode).toBe(403);
93
- (0, vitest_1.expect)(stratum.diffConfig).not.toHaveBeenCalled();
94
- });
95
- (0, vitest_1.it)("refuses when the first tenant is unrelated", async () => {
96
- const res = await diff(VICTIM, ATTACKER);
97
- (0, vitest_1.expect)(res.statusCode).toBe(403);
98
- (0, vitest_1.expect)(stratum.diffConfig).not.toHaveBeenCalled();
99
- });
100
- (0, vitest_1.it)("allows diffing entirely within the caller's subtree", async () => {
101
- const res = await diff(ATTACKER, ATTACKER_CHILD);
102
- (0, vitest_1.expect)(res.statusCode).toBe(200);
103
- (0, vitest_1.expect)(stratum.diffConfig).toHaveBeenCalledWith(ATTACKER, ATTACKER_CHILD);
104
- });
105
- });
106
- (0, vitest_1.describe)("role routes scope to the caller's subtree", () => {
107
- (0, vitest_1.beforeEach)(async () => {
108
- stratum = (0, test_helpers_js_1.createMockStratum)();
109
- stratum.validateApiKey.mockResolvedValue({
110
- key_id: "attacker-key",
111
- tenant_id: ATTACKER,
112
- scopes: ["read", "write", "admin"],
113
- rate_limit_max: null,
114
- rate_limit_window: null,
115
- });
116
- wireCommonMocks();
117
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
118
- });
119
- (0, vitest_1.it)("does not enumerate other tenants' roles when listing without a filter", async () => {
120
- const res = await app.inject({ method: "GET", url: "/api/v1/roles", headers: (0, test_helpers_js_1.authHeaders)() });
121
- (0, vitest_1.expect)(res.statusCode).toBe(200);
122
- const ids = res.json().map((r) => r.id);
123
- (0, vitest_1.expect)(ids).not.toContain(ROLE_FOREIGN);
124
- (0, vitest_1.expect)(ids).toContain(ROLE_OWN);
125
- });
126
- (0, vitest_1.it)("refuses listing a foreign tenant's roles by explicit filter", async () => {
127
- const res = await app.inject({
128
- method: "GET",
129
- url: `/api/v1/roles?tenant_id=${VICTIM}`,
130
- headers: (0, test_helpers_js_1.authHeaders)(),
131
- });
132
- (0, vitest_1.expect)(res.statusCode).toBe(403);
133
- (0, vitest_1.expect)(stratum.listRoles).not.toHaveBeenCalled();
134
- });
135
- (0, vitest_1.it)("refuses reading a foreign tenant's role", async () => {
136
- const res = await app.inject({ method: "GET", url: `/api/v1/roles/${ROLE_FOREIGN}`, headers: (0, test_helpers_js_1.authHeaders)() });
137
- (0, vitest_1.expect)(res.statusCode).toBe(403);
138
- });
139
- (0, vitest_1.it)("allows reading a role in the caller's own tenant", async () => {
140
- const res = await app.inject({ method: "GET", url: `/api/v1/roles/${ROLE_OWN}`, headers: (0, test_helpers_js_1.authHeaders)() });
141
- (0, vitest_1.expect)(res.statusCode).toBe(200);
142
- });
143
- (0, vitest_1.it)("refuses updating a foreign tenant's role", async () => {
144
- const res = await app.inject({
145
- method: "PATCH",
146
- url: `/api/v1/roles/${ROLE_FOREIGN}`,
147
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
148
- payload: { name: "hijacked" },
149
- });
150
- (0, vitest_1.expect)(res.statusCode).toBe(403);
151
- (0, vitest_1.expect)(stratum.updateRole).not.toHaveBeenCalled();
152
- });
153
- (0, vitest_1.it)("refuses deleting a foreign tenant's role", async () => {
154
- const res = await app.inject({ method: "DELETE", url: `/api/v1/roles/${ROLE_FOREIGN}`, headers: (0, test_helpers_js_1.authHeaders)() });
155
- (0, vitest_1.expect)(res.statusCode).toBe(403);
156
- (0, vitest_1.expect)(stratum.deleteRole).not.toHaveBeenCalled();
157
- });
158
- (0, vitest_1.it)("refuses creating a role in a foreign tenant", async () => {
159
- const res = await app.inject({
160
- method: "POST",
161
- url: "/api/v1/roles",
162
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
163
- payload: { name: "planted", scopes: ["read"], tenant_id: VICTIM },
164
- });
165
- (0, vitest_1.expect)(res.statusCode).toBe(403);
166
- (0, vitest_1.expect)(stratum.createRole).not.toHaveBeenCalled();
167
- });
168
- (0, vitest_1.it)("refuses assigning a foreign tenant's role to an own key", async () => {
169
- const res = await app.inject({
170
- method: "POST",
171
- url: `/api/v1/roles/assign/${OWN_KEY}`,
172
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
173
- payload: { role_id: ROLE_FOREIGN },
174
- });
175
- (0, vitest_1.expect)(res.statusCode).toBe(403);
176
- (0, vitest_1.expect)(stratum.assignRoleToKey).not.toHaveBeenCalled();
177
- });
178
- (0, vitest_1.it)("refuses assigning a role to a key in another tenant", async () => {
179
- const res = await app.inject({
180
- method: "POST",
181
- url: `/api/v1/roles/assign/${FOREIGN_KEY}`,
182
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
183
- payload: { role_id: ROLE_OWN },
184
- });
185
- (0, vitest_1.expect)(res.statusCode).toBe(403);
186
- (0, vitest_1.expect)(stratum.assignRoleToKey).not.toHaveBeenCalled();
187
- });
188
- (0, vitest_1.it)("refuses removing a role from a key in another tenant", async () => {
189
- const res = await app.inject({
190
- method: "DELETE",
191
- url: `/api/v1/roles/assign/${FOREIGN_KEY}`,
192
- headers: (0, test_helpers_js_1.authHeaders)(),
193
- });
194
- (0, vitest_1.expect)(res.statusCode).toBe(403);
195
- (0, vitest_1.expect)(stratum.removeRoleFromKey).not.toHaveBeenCalled();
196
- });
197
- (0, vitest_1.it)("allows assigning an own role to an own key", async () => {
198
- const res = await app.inject({
199
- method: "POST",
200
- url: `/api/v1/roles/assign/${OWN_KEY}`,
201
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
202
- payload: { role_id: ROLE_OWN },
203
- });
204
- (0, vitest_1.expect)(res.statusCode).toBe(200);
205
- (0, vitest_1.expect)(stratum.assignRoleToKey).toHaveBeenCalledWith(OWN_KEY, ROLE_OWN);
206
- });
207
- });
208
- (0, vitest_1.describe)("global operator keys keep full access", () => {
209
- (0, vitest_1.beforeEach)(async () => {
210
- stratum = (0, test_helpers_js_1.createMockStratum)();
211
- stratum.validateApiKey.mockResolvedValue({
212
- key_id: "global-key",
213
- tenant_id: null,
214
- scopes: ["read", "write", "admin"],
215
- rate_limit_max: null,
216
- rate_limit_window: null,
217
- });
218
- wireCommonMocks();
219
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
220
- });
221
- (0, vitest_1.it)("still diffs any two tenants", async () => {
222
- const res = await diff(ATTACKER, VICTIM);
223
- (0, vitest_1.expect)(res.statusCode).toBe(200);
224
- (0, vitest_1.expect)(stratum.diffConfig).toHaveBeenCalledWith(ATTACKER, VICTIM);
225
- });
226
- (0, vitest_1.it)("still enumerates every tenant's roles", async () => {
227
- const res = await app.inject({ method: "GET", url: "/api/v1/roles", headers: (0, test_helpers_js_1.authHeaders)() });
228
- (0, vitest_1.expect)(res.statusCode).toBe(200);
229
- (0, vitest_1.expect)(res.json().map((r) => r.id)).toContain(ROLE_FOREIGN);
230
- });
231
- (0, vitest_1.it)("still reads any tenant's role", async () => {
232
- const res = await app.inject({ method: "GET", url: `/api/v1/roles/${ROLE_FOREIGN}`, headers: (0, test_helpers_js_1.authHeaders)() });
233
- (0, vitest_1.expect)(res.statusCode).toBe(200);
234
- });
235
- (0, vitest_1.it)("still assigns any role to any key", async () => {
236
- const res = await app.inject({
237
- method: "POST",
238
- url: `/api/v1/roles/assign/${FOREIGN_KEY}`,
239
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
240
- payload: { role_id: ROLE_FOREIGN },
241
- });
242
- (0, vitest_1.expect)(res.statusCode).toBe(200);
243
- (0, vitest_1.expect)(stratum.assignRoleToKey).toHaveBeenCalledWith(FOREIGN_KEY, ROLE_FOREIGN);
244
- });
245
- });
246
- //# sourceMappingURL=configdiff-roles-scope.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"configdiff-roles-scope.test.js","sourceRoot":"","sources":["../../src/__tests__/configdiff-roles-scope.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=default-deny.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"default-deny.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/default-deny.test.ts"],"names":[],"mappings":""}
@@ -1,92 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const vitest_1 = require("vitest");
7
- const fastify_1 = __importDefault(require("fastify"));
8
- const auth_js_1 = require("../middleware/auth.js");
9
- const authorize_js_1 = require("../middleware/authorize.js");
10
- const tenant_scope_js_1 = require("../middleware/tenant-scope.js");
11
- const error_handler_js_1 = require("../middleware/error-handler.js");
12
- const test_helpers_js_1 = require("./test-helpers.js");
13
- /**
14
- * Control-plane authorization must be default-deny: a route that does not
15
- * declare a tenant scope is refused, so a route added without a guard fails
16
- * closed instead of silently serving data.
17
- */
18
- const SCOPED_TENANT = "11111111-1111-4111-8111-111111111111";
19
- const OTHER_TENANT = "99999999-9999-4999-8999-999999999999";
20
- let stratum;
21
- (0, vitest_1.beforeEach)(() => {
22
- stratum = (0, test_helpers_js_1.createMockStratum)();
23
- stratum.validateApiKey.mockResolvedValue({
24
- key_id: "scoped-key",
25
- tenant_id: SCOPED_TENANT,
26
- scopes: ["read", "write", "admin"],
27
- rate_limit_max: null,
28
- rate_limit_window: null,
29
- });
30
- // Both tenants are roots (empty ancestry), so neither is a descendant of the
31
- // other: a scoped key may reach only its own.
32
- stratum.getTenant.mockImplementation(async (id) => ({ id, ancestry_path: "", status: "active" }));
33
- });
34
- /**
35
- * Build an app wired exactly like the production control plane: authenticate,
36
- * authorize by scope, then enforce tenant scope. The caller registers routes.
37
- */
38
- async function buildApp(register) {
39
- const app = (0, fastify_1.default)({ logger: false });
40
- app.addHook("preHandler", (0, auth_js_1.createAuthMiddleware)(stratum));
41
- app.addHook("preHandler", (0, authorize_js_1.createAuthorizeMiddleware)());
42
- app.addHook("preHandler", (0, tenant_scope_js_1.createTenantScopeEnforcer)(stratum));
43
- app.setErrorHandler(error_handler_js_1.errorHandler);
44
- await register(app);
45
- await app.ready();
46
- return app;
47
- }
48
- (0, vitest_1.describe)("default-deny authorization", () => {
49
- (0, vitest_1.it)("denies a route that declares no tenant scope", async () => {
50
- const app = await buildApp((app) => {
51
- // A control-plane route that ships without any tenant-scope declaration.
52
- app.get("/api/v1/unguarded/:id", async (_req, reply) => reply.status(200).send({ reached: true }));
53
- });
54
- const res = await app.inject({
55
- method: "GET",
56
- url: `/api/v1/unguarded/${OTHER_TENANT}`,
57
- headers: (0, test_helpers_js_1.authHeaders)(),
58
- });
59
- (0, vitest_1.expect)(res.statusCode).toBe(403);
60
- });
61
- (0, vitest_1.it)("admits a route explicitly declared as global", async () => {
62
- const app = await buildApp((app) => {
63
- (0, tenant_scope_js_1.declareTenantScope)(app, "global");
64
- app.get("/api/v1/ops", async (_req, reply) => reply.status(200).send({ ok: true }));
65
- });
66
- const res = await app.inject({
67
- method: "GET",
68
- url: "/api/v1/ops",
69
- headers: (0, test_helpers_js_1.authHeaders)(),
70
- });
71
- (0, vitest_1.expect)(res.statusCode).toBe(200);
72
- });
73
- (0, vitest_1.it)("enforces a declared extractor: own tenant allowed, others denied", async () => {
74
- const app = await buildApp((app) => {
75
- (0, tenant_scope_js_1.declareTenantScope)(app, tenant_scope_js_1.fromParamId);
76
- app.get("/api/v1/tenants/:id/thing", async (_req, reply) => reply.status(200).send({ ok: true }));
77
- });
78
- const own = await app.inject({
79
- method: "GET",
80
- url: `/api/v1/tenants/${SCOPED_TENANT}/thing`,
81
- headers: (0, test_helpers_js_1.authHeaders)(),
82
- });
83
- const other = await app.inject({
84
- method: "GET",
85
- url: `/api/v1/tenants/${OTHER_TENANT}/thing`,
86
- headers: (0, test_helpers_js_1.authHeaders)(),
87
- });
88
- (0, vitest_1.expect)(own.statusCode).toBe(200);
89
- (0, vitest_1.expect)(other.statusCode).toBe(403);
90
- });
91
- });
92
- //# sourceMappingURL=default-deny.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"default-deny.test.js","sourceRoot":"","sources":["../../src/__tests__/default-deny.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=tenant-create-scope.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tenant-create-scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tenant-create-scope.test.ts"],"names":[],"mappings":""}
@@ -1,129 +0,0 @@
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
- * Tenant creation must respect the caller's key scope.
7
- *
8
- * A tenant-scoped key may only create tenants WITHIN its own subtree: the new
9
- * tenant's parent must be the key's own tenant or a descendant of it. A scoped
10
- * key may not create a new root (a tenant with no parent). Only global
11
- * (operator) keys may create roots or create anywhere in the tree.
12
- */
13
- const OWNER = "11111111-1111-4111-8111-111111111111";
14
- const OWNER_CHILD = "22222222-2222-4222-8222-222222222222";
15
- const OUTSIDER = "99999999-9999-4999-8999-999999999999";
16
- // ancestry_path holds a tenant's ANCESTORS and excludes self, so a root is "".
17
- const TENANTS = {
18
- [OWNER]: { id: OWNER, ancestry_path: "", status: "active" },
19
- [OWNER_CHILD]: { id: OWNER_CHILD, ancestry_path: OWNER, status: "active" },
20
- [OUTSIDER]: { id: OUTSIDER, ancestry_path: "", status: "active" },
21
- };
22
- let app;
23
- let stratum;
24
- function useScopedKey() {
25
- stratum.validateApiKey.mockResolvedValue({
26
- key_id: "scoped-key",
27
- tenant_id: OWNER,
28
- scopes: ["read", "write", "admin"],
29
- rate_limit_max: null,
30
- rate_limit_window: null,
31
- });
32
- }
33
- function useGlobalKey() {
34
- stratum.validateApiKey.mockResolvedValue({
35
- key_id: "global-key",
36
- tenant_id: null,
37
- scopes: ["read", "write", "admin"],
38
- rate_limit_max: null,
39
- rate_limit_window: null,
40
- });
41
- }
42
- (0, vitest_1.beforeEach)(async () => {
43
- stratum = (0, test_helpers_js_1.createMockStratum)();
44
- stratum.getTenant.mockImplementation(async (id) => {
45
- const row = TENANTS[id];
46
- if (!row)
47
- throw new Error(`Tenant ${id} not found`);
48
- return row;
49
- });
50
- stratum.createTenant.mockResolvedValue({
51
- id: "created-id",
52
- slug: "newco",
53
- isolation_strategy: "SHARED_RLS",
54
- });
55
- stratum.batchCreateTenants.mockResolvedValue({
56
- created: [],
57
- });
58
- app = await (0, test_helpers_js_1.buildTestApp)(stratum);
59
- });
60
- async function create(body) {
61
- return app.inject({
62
- method: "POST",
63
- url: "/api/v1/tenants",
64
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
65
- payload: body,
66
- });
67
- }
68
- (0, vitest_1.describe)("scoped key create is confined to its own subtree", () => {
69
- (0, vitest_1.beforeEach)(useScopedKey);
70
- (0, vitest_1.it)("refuses creating a tenant under a parent outside the key subtree", async () => {
71
- const res = await create({ slug: "newco", name: "New Co", parent_id: OUTSIDER });
72
- (0, vitest_1.expect)(res.statusCode).toBe(403);
73
- (0, vitest_1.expect)(stratum.createTenant).not.toHaveBeenCalled();
74
- });
75
- (0, vitest_1.it)("refuses creating a new root tenant (no parent)", async () => {
76
- const res = await create({ slug: "newco", name: "New Co" });
77
- (0, vitest_1.expect)(res.statusCode).toBe(403);
78
- (0, vitest_1.expect)(stratum.createTenant).not.toHaveBeenCalled();
79
- });
80
- (0, vitest_1.it)("refuses an explicit null parent (root)", async () => {
81
- const res = await create({ slug: "newco", name: "New Co", parent_id: null });
82
- (0, vitest_1.expect)(res.statusCode).toBe(403);
83
- (0, vitest_1.expect)(stratum.createTenant).not.toHaveBeenCalled();
84
- });
85
- (0, vitest_1.it)("allows creating directly under the key's own tenant", async () => {
86
- const res = await create({ slug: "newco", name: "New Co", parent_id: OWNER });
87
- (0, vitest_1.expect)(res.statusCode).toBe(201);
88
- (0, vitest_1.expect)(stratum.createTenant).toHaveBeenCalled();
89
- });
90
- (0, vitest_1.it)("allows creating under a descendant of the key's tenant", async () => {
91
- const res = await create({ slug: "newco", name: "New Co", parent_id: OWNER_CHILD });
92
- (0, vitest_1.expect)(res.statusCode).toBe(201);
93
- (0, vitest_1.expect)(stratum.createTenant).toHaveBeenCalled();
94
- });
95
- (0, vitest_1.it)("refuses a batch create that targets a parent outside the subtree", async () => {
96
- const res = await app.inject({
97
- method: "POST",
98
- url: "/api/v1/tenants/batch",
99
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
100
- payload: { tenants: [{ slug: "newco", name: "New Co", parent_id: OUTSIDER }] },
101
- });
102
- (0, vitest_1.expect)(res.statusCode).toBe(403);
103
- (0, vitest_1.expect)(stratum.batchCreateTenants).not.toHaveBeenCalled();
104
- });
105
- (0, vitest_1.it)("refuses a batch create that includes a new root", async () => {
106
- const res = await app.inject({
107
- method: "POST",
108
- url: "/api/v1/tenants/batch",
109
- headers: { ...(0, test_helpers_js_1.authHeaders)(), "content-type": "application/json" },
110
- payload: { tenants: [{ slug: "newco", name: "New Co", parent_id: OWNER }, { slug: "rootco", name: "Root Co" }] },
111
- });
112
- (0, vitest_1.expect)(res.statusCode).toBe(403);
113
- (0, vitest_1.expect)(stratum.batchCreateTenants).not.toHaveBeenCalled();
114
- });
115
- });
116
- (0, vitest_1.describe)("global key create is unrestricted", () => {
117
- (0, vitest_1.beforeEach)(useGlobalKey);
118
- (0, vitest_1.it)("allows creating a new root tenant", async () => {
119
- const res = await create({ slug: "rootco", name: "Root Co" });
120
- (0, vitest_1.expect)(res.statusCode).toBe(201);
121
- (0, vitest_1.expect)(stratum.createTenant).toHaveBeenCalled();
122
- });
123
- (0, vitest_1.it)("allows creating anywhere in the tree", async () => {
124
- const res = await create({ slug: "newco", name: "New Co", parent_id: OUTSIDER });
125
- (0, vitest_1.expect)(res.statusCode).toBe(201);
126
- (0, vitest_1.expect)(stratum.createTenant).toHaveBeenCalled();
127
- });
128
- });
129
- //# sourceMappingURL=tenant-create-scope.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tenant-create-scope.test.js","sourceRoot":"","sources":["../../src/__tests__/tenant-create-scope.test.ts"],"names":[],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=tenant-move-scope.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"tenant-move-scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tenant-move-scope.test.ts"],"names":[],"mappings":""}
@@ -1,113 +0,0 @@
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