@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 @@
|
|
|
1
|
+
{"version":3,"file":"admin-scope-query-string.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/admin-scope-query-string.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* Admin-scope enforcement must depend on the resolved request PATH only, not on
|
|
7
|
+
* the raw URL. A query string appended to an admin-only route must not change
|
|
8
|
+
* which scope the route requires.
|
|
9
|
+
*/
|
|
10
|
+
(0, vitest_1.describe)("admin scope is enforced independent of the query string", () => {
|
|
11
|
+
let app;
|
|
12
|
+
let stratum;
|
|
13
|
+
const id = test_helpers_js_1.SAMPLE_TENANT.id;
|
|
14
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
15
|
+
stratum = (0, test_helpers_js_1.createMockStratum)();
|
|
16
|
+
stratum.getTenant.mockResolvedValue(test_helpers_js_1.SAMPLE_TENANT);
|
|
17
|
+
stratum.purgeTenant.mockResolvedValue(undefined);
|
|
18
|
+
// getTenantContext is not stubbed by the shared helper; add it locally.
|
|
19
|
+
stratum.getTenantContext = vitest_1.vi
|
|
20
|
+
.fn()
|
|
21
|
+
.mockResolvedValue({ tenant: test_helpers_js_1.SAMPLE_TENANT });
|
|
22
|
+
app = await (0, test_helpers_js_1.buildTestApp)(stratum);
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.afterEach)(async () => {
|
|
25
|
+
await app.close();
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.it)("write-scope key is refused on an admin route (baseline, no query string)", async () => {
|
|
28
|
+
(0, test_helpers_js_1.setupWriteApiKey)(stratum);
|
|
29
|
+
const res = await app.inject({
|
|
30
|
+
method: "POST",
|
|
31
|
+
url: `/api/v1/tenants/${id}/purge`,
|
|
32
|
+
headers: (0, test_helpers_js_1.authHeaders)(),
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
35
|
+
(0, vitest_1.expect)(stratum.purgeTenant).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)("write-scope key stays refused on an admin route carrying a query string", async () => {
|
|
38
|
+
(0, test_helpers_js_1.setupWriteApiKey)(stratum);
|
|
39
|
+
const res = await app.inject({
|
|
40
|
+
method: "POST",
|
|
41
|
+
url: `/api/v1/tenants/${id}/purge?probe=1`,
|
|
42
|
+
headers: (0, test_helpers_js_1.authHeaders)(),
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
45
|
+
(0, vitest_1.expect)(stratum.purgeTenant).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)("read-scope key stays refused on an admin GET route carrying a query string", async () => {
|
|
48
|
+
(0, test_helpers_js_1.setupReadOnlyApiKey)(stratum);
|
|
49
|
+
const res = await app.inject({
|
|
50
|
+
method: "GET",
|
|
51
|
+
url: `/api/v1/tenants/${id}/context?probe=1`,
|
|
52
|
+
headers: (0, test_helpers_js_1.authHeaders)(),
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.expect)(res.statusCode).toBe(403);
|
|
55
|
+
(0, vitest_1.expect)(stratum
|
|
56
|
+
.getTenantContext).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("admin-scope key is still allowed on an admin route with a query string", async () => {
|
|
59
|
+
(0, test_helpers_js_1.setupAdminApiKey)(stratum);
|
|
60
|
+
const res = await app.inject({
|
|
61
|
+
method: "POST",
|
|
62
|
+
url: `/api/v1/tenants/${id}/purge?probe=1`,
|
|
63
|
+
headers: (0, test_helpers_js_1.authHeaders)(),
|
|
64
|
+
});
|
|
65
|
+
(0, vitest_1.expect)(res.statusCode).toBe(204);
|
|
66
|
+
(0, vitest_1.expect)(stratum.purgeTenant).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=admin-scope-query-string.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-scope-query-string.test.js","sourceRoot":"","sources":["../../src/__tests__/admin-scope-query-string.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configdiff-roles-scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/configdiff-roles-scope.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configdiff-roles-scope.test.js","sourceRoot":"","sources":["../../src/__tests__/configdiff-roles-scope.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-deny.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/default-deny.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-deny.test.js","sourceRoot":"","sources":["../../src/__tests__/default-deny.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-create-scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tenant-create-scope.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-create-scope.test.js","sourceRoot":"","sources":["../../src/__tests__/tenant-create-scope.test.ts"],"names":[],"mappings":";;AAAA,mCAA8D;AAG9D,uDAAiF;AAEjF;;;;;;;GAOG;AAEH,MAAM,KAAK,GAAG,sCAAsC,CAAC;AACrD,MAAM,WAAW,GAAG,sCAAsC,CAAC;AAC3D,MAAM,QAAQ,GAAG,sCAAsC,CAAC;AAIxD,+EAA+E;AAC/E,MAAM,OAAO,GAAwB;IACnC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;IAC3D,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE;IAC1E,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;CAClE,CAAC;AAEF,IAAI,GAAoB,CAAC;AACzB,IAAI,OAAgB,CAAC;AAErB,SAAS,YAAY;IAClB,OAAO,CAAC,cAA2C,CAAC,iBAAiB,CAAC;QACrE,MAAM,EAAE,YAAY;QACpB,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;QAClC,cAAc,EAAE,IAAI;QACpB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY;IAClB,OAAO,CAAC,cAA2C,CAAC,iBAAiB,CAAC;QACrE,MAAM,EAAE,YAAY;QACpB,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;QAClC,cAAc,EAAE,IAAI;QACpB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC;AACL,CAAC;AAED,IAAA,mBAAU,EAAC,KAAK,IAAI,EAAE;IACpB,OAAO,GAAG,IAAA,mCAAiB,GAAE,CAAC;IAE7B,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,YAAyC,CAAC,iBAAiB,CAAC;QACnE,EAAE,EAAE,YAAY;QAChB,IAAI,EAAE,OAAO;QACb,kBAAkB,EAAE,YAAY;KACjC,CAAC,CAAC;IACF,OAAO,CAAC,kBAA+C,CAAC,iBAAiB,CAAC;QACzE,OAAO,EAAE,EAAE;KACZ,CAAC,CAAC;IAEH,GAAG,GAAG,MAAM,IAAA,8BAAY,EAAC,OAAO,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,MAAM,CAAC,IAA6B;IACjD,OAAO,GAAG,CAAC,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,iBAAiB;QACtB,OAAO,EAAE,EAAE,GAAG,IAAA,6BAAW,GAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QACjE,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;AACL,CAAC;AAED,IAAA,iBAAQ,EAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,IAAA,mBAAU,EAAC,YAAY,CAAC,CAAC;IAEzB,IAAA,WAAE,EAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEjF,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE5D,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE7E,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAE9E,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;QAEpF,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,uBAAuB;YAC5B,OAAO,EAAE,EAAE,GAAG,IAAA,6BAAW,GAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YACjE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE;SAC/E,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,MAAM;YACd,GAAG,EAAE,uBAAuB;YAC5B,OAAO,EAAE,EAAE,GAAG,IAAA,6BAAW,GAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YACjE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE;SACjH,CAAC,CAAC;QAEH,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,IAAA,mBAAU,EAAC,YAAY,CAAC,CAAC;IAEzB,IAAA,WAAE,EAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAE9D,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEjF,IAAA,eAAM,EAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,IAAA,eAAM,EAAC,OAAO,CAAC,YAAY,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-move-scope.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tenant-move-scope.test.ts"],"names":[],"mappings":""}
|