@checkstack/auth-backend 0.0.3 → 0.1.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/CHANGELOG.md +78 -0
- package/drizzle/0002_lowly_squirrel_girl.sql +43 -0
- package/drizzle/0003_tranquil_sally_floyd.sql +8 -0
- package/drizzle/meta/0002_snapshot.json +1017 -0
- package/drizzle/meta/0003_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/router.ts +435 -0
- package/src/schema.ts +107 -0
- package/src/teams.test.ts +1230 -0
- package/src/utils/user.test.ts +60 -41
- package/src/utils/user.ts +9 -1
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { createAuthRouter } from "./router";
|
|
3
|
+
import { createMockRpcContext } from "@checkstack/backend-api";
|
|
4
|
+
import { call } from "@orpc/server";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import * as schema from "./schema";
|
|
7
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
8
|
+
|
|
9
|
+
/** Type alias for the database type used in auth router */
|
|
10
|
+
type AuthDatabase = NodePgDatabase<typeof schema>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tests for Team and Resource-Level Access Control endpoints.
|
|
14
|
+
*
|
|
15
|
+
* These tests cover:
|
|
16
|
+
* - Team CRUD operations (getTeams, createTeam, updateTeam, deleteTeam)
|
|
17
|
+
* - Team membership management (addUserToTeam, removeUserFromTeam)
|
|
18
|
+
* - Team manager operations (addTeamManager, removeTeamManager)
|
|
19
|
+
* - Resource access grants (getResourceTeamAccess, setResourceTeamAccess, removeResourceTeamAccess)
|
|
20
|
+
* - S2S access checks (checkResourceTeamAccess, getAccessibleResourceIds)
|
|
21
|
+
*/
|
|
22
|
+
describe("Teams and Resource Access Control", () => {
|
|
23
|
+
// Mock user with admin permissions
|
|
24
|
+
const mockAdminUser = {
|
|
25
|
+
type: "user" as const,
|
|
26
|
+
id: "admin-user",
|
|
27
|
+
permissions: ["*"],
|
|
28
|
+
roles: ["admin"],
|
|
29
|
+
teamIds: ["team-alpha"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Mock regular user with limited permissions
|
|
33
|
+
const mockRegularUser = {
|
|
34
|
+
type: "user" as const,
|
|
35
|
+
id: "regular-user",
|
|
36
|
+
permissions: ["auth.teams.read"],
|
|
37
|
+
roles: ["users"],
|
|
38
|
+
teamIds: ["team-beta"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Mock service user for S2S calls
|
|
42
|
+
const mockServiceUser = {
|
|
43
|
+
type: "service" as const,
|
|
44
|
+
pluginId: "backend-api",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a chainable mock for database query operations.
|
|
49
|
+
* Allows chaining .where().innerJoin().limit().offset().orderBy()
|
|
50
|
+
*/
|
|
51
|
+
function createChain<T>(data: T[] = []): Record<string, unknown> {
|
|
52
|
+
const chain: Record<string, unknown> = {
|
|
53
|
+
where: mock(() => chain),
|
|
54
|
+
innerJoin: mock(() => chain),
|
|
55
|
+
limit: mock(() => chain),
|
|
56
|
+
offset: mock(() => chain),
|
|
57
|
+
orderBy: mock(() => chain),
|
|
58
|
+
onConflictDoUpdate: mock(() => Promise.resolve()),
|
|
59
|
+
onConflictDoNothing: mock(() => Promise.resolve()),
|
|
60
|
+
then: (resolve: (value: T[]) => void) => Promise.resolve(resolve(data)),
|
|
61
|
+
};
|
|
62
|
+
return chain;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a fresh mock database for each test.
|
|
67
|
+
* Uses type assertion to satisfy NodePgDatabase interface for testing.
|
|
68
|
+
*/
|
|
69
|
+
function createMockDb(): AuthDatabase {
|
|
70
|
+
const mockDb = {
|
|
71
|
+
select: mock(() => ({
|
|
72
|
+
from: mock(() => createChain([])),
|
|
73
|
+
})),
|
|
74
|
+
insert: mock(() => ({
|
|
75
|
+
values: mock(() => ({
|
|
76
|
+
onConflictDoNothing: mock(() => Promise.resolve()),
|
|
77
|
+
onConflictDoUpdate: mock(() => Promise.resolve()),
|
|
78
|
+
then: (resolve: (value: unknown) => void) =>
|
|
79
|
+
Promise.resolve(resolve(undefined)),
|
|
80
|
+
})),
|
|
81
|
+
})),
|
|
82
|
+
update: mock(() => ({
|
|
83
|
+
set: mock(() => ({
|
|
84
|
+
where: mock(() => Promise.resolve()),
|
|
85
|
+
})),
|
|
86
|
+
})),
|
|
87
|
+
delete: mock(() => ({
|
|
88
|
+
where: mock(() => Promise.resolve()),
|
|
89
|
+
})),
|
|
90
|
+
transaction: mock((cb: (tx: typeof mockDb) => Promise<void>) =>
|
|
91
|
+
cb(mockDb)
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
// Type assertion for mock database - only used in tests
|
|
95
|
+
return mockDb as unknown as AuthDatabase;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const mockRegistry = {
|
|
99
|
+
getStrategies: () => [
|
|
100
|
+
{
|
|
101
|
+
id: "credential",
|
|
102
|
+
displayName: "Credentials",
|
|
103
|
+
description: "Email and password authentication",
|
|
104
|
+
configSchema: z.object({ enabled: z.boolean() }),
|
|
105
|
+
configVersion: 1,
|
|
106
|
+
migrations: [],
|
|
107
|
+
requiresManualRegistration: true,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const mockConfigService = {
|
|
113
|
+
get: mock(() => Promise.resolve(undefined)),
|
|
114
|
+
getRedacted: mock(() => Promise.resolve({})),
|
|
115
|
+
set: mock(() => Promise.resolve()),
|
|
116
|
+
delete: mock(() => Promise.resolve()),
|
|
117
|
+
list: mock(() => Promise.resolve([])),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const mockPermissionRegistry = {
|
|
121
|
+
getPermissions: () => [
|
|
122
|
+
{ id: "auth.teams.read", description: "View teams" },
|
|
123
|
+
{ id: "auth.teams.manage", description: "Manage teams" },
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// ==========================================================================
|
|
128
|
+
// TEAM CRUD TESTS
|
|
129
|
+
// ==========================================================================
|
|
130
|
+
|
|
131
|
+
describe("getTeams", () => {
|
|
132
|
+
it("returns empty array when no teams exist", async () => {
|
|
133
|
+
const mockDb = createMockDb();
|
|
134
|
+
const router = createAuthRouter(
|
|
135
|
+
mockDb,
|
|
136
|
+
mockRegistry,
|
|
137
|
+
async () => {},
|
|
138
|
+
mockConfigService,
|
|
139
|
+
mockPermissionRegistry
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
143
|
+
const result = await call(router.getTeams, undefined, { context });
|
|
144
|
+
|
|
145
|
+
expect(Array.isArray(result)).toBe(true);
|
|
146
|
+
expect(result).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("returns teams with member counts", async () => {
|
|
150
|
+
const mockDb = createMockDb();
|
|
151
|
+
|
|
152
|
+
// Mock teams query
|
|
153
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
154
|
+
from: mock(() =>
|
|
155
|
+
createChain([
|
|
156
|
+
{
|
|
157
|
+
id: "team-1",
|
|
158
|
+
name: "Platform Team",
|
|
159
|
+
description: "Core platform team",
|
|
160
|
+
},
|
|
161
|
+
{ id: "team-2", name: "API Team", description: null },
|
|
162
|
+
])
|
|
163
|
+
),
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
// Mock member counts query
|
|
167
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
168
|
+
from: mock(() =>
|
|
169
|
+
createChain([
|
|
170
|
+
{ teamId: "team-1" },
|
|
171
|
+
{ teamId: "team-1" },
|
|
172
|
+
{ teamId: "team-2" },
|
|
173
|
+
])
|
|
174
|
+
),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
// Mock manager query (user is manager of team-1)
|
|
178
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
179
|
+
from: mock(() =>
|
|
180
|
+
createChain([{ teamId: "team-1", userId: "admin-user" }])
|
|
181
|
+
),
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const router = createAuthRouter(
|
|
185
|
+
mockDb,
|
|
186
|
+
mockRegistry,
|
|
187
|
+
async () => {},
|
|
188
|
+
mockConfigService,
|
|
189
|
+
mockPermissionRegistry
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
193
|
+
const result = await call(router.getTeams, undefined, { context });
|
|
194
|
+
|
|
195
|
+
expect(result).toHaveLength(2);
|
|
196
|
+
expect(result[0]).toEqual({
|
|
197
|
+
id: "team-1",
|
|
198
|
+
name: "Platform Team",
|
|
199
|
+
description: "Core platform team",
|
|
200
|
+
memberCount: 2,
|
|
201
|
+
isManager: true,
|
|
202
|
+
});
|
|
203
|
+
expect(result[1]).toEqual({
|
|
204
|
+
id: "team-2",
|
|
205
|
+
name: "API Team",
|
|
206
|
+
description: null,
|
|
207
|
+
memberCount: 1,
|
|
208
|
+
isManager: false,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("getTeam", () => {
|
|
214
|
+
it("returns undefined for non-existent team", async () => {
|
|
215
|
+
const mockDb = createMockDb();
|
|
216
|
+
|
|
217
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
218
|
+
from: mock(() => createChain([])),
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
const router = createAuthRouter(
|
|
222
|
+
mockDb,
|
|
223
|
+
mockRegistry,
|
|
224
|
+
async () => {},
|
|
225
|
+
mockConfigService,
|
|
226
|
+
mockPermissionRegistry
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
230
|
+
const result = await call(
|
|
231
|
+
router.getTeam,
|
|
232
|
+
{ teamId: "non-existent" },
|
|
233
|
+
{ context }
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect(result).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("returns team with members and managers", async () => {
|
|
240
|
+
const mockDb = createMockDb();
|
|
241
|
+
|
|
242
|
+
// Mock team query
|
|
243
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
244
|
+
from: mock(() =>
|
|
245
|
+
createChain([
|
|
246
|
+
{
|
|
247
|
+
id: "team-1",
|
|
248
|
+
name: "Platform Team",
|
|
249
|
+
description: "Core team",
|
|
250
|
+
},
|
|
251
|
+
])
|
|
252
|
+
),
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
// Mock members query
|
|
256
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
257
|
+
from: mock(() =>
|
|
258
|
+
createChain([{ userId: "user-1" }, { userId: "user-2" }])
|
|
259
|
+
),
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
// Mock managers query
|
|
263
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
264
|
+
from: mock(() => createChain([{ userId: "user-1" }])),
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
// Mock users query
|
|
268
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
269
|
+
from: mock(() =>
|
|
270
|
+
createChain([
|
|
271
|
+
{ id: "user-1", name: "Alice", email: "alice@test.com" },
|
|
272
|
+
{ id: "user-2", name: "Bob", email: "bob@test.com" },
|
|
273
|
+
])
|
|
274
|
+
),
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
const router = createAuthRouter(
|
|
278
|
+
mockDb,
|
|
279
|
+
mockRegistry,
|
|
280
|
+
async () => {},
|
|
281
|
+
mockConfigService,
|
|
282
|
+
mockPermissionRegistry
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
286
|
+
const result = await call(
|
|
287
|
+
router.getTeam,
|
|
288
|
+
{ teamId: "team-1" },
|
|
289
|
+
{ context }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result).toBeDefined();
|
|
293
|
+
expect(result?.name).toBe("Platform Team");
|
|
294
|
+
expect(result?.members).toHaveLength(2);
|
|
295
|
+
expect(result?.managers).toHaveLength(1);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("createTeam", () => {
|
|
300
|
+
it("creates team with name and description", async () => {
|
|
301
|
+
const mockDb = createMockDb();
|
|
302
|
+
let insertedData: Record<string, unknown> | undefined;
|
|
303
|
+
|
|
304
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
305
|
+
values: mock((data: Record<string, unknown>) => {
|
|
306
|
+
insertedData = data;
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}),
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
const router = createAuthRouter(
|
|
312
|
+
mockDb,
|
|
313
|
+
mockRegistry,
|
|
314
|
+
async () => {},
|
|
315
|
+
mockConfigService,
|
|
316
|
+
mockPermissionRegistry
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
320
|
+
const result = await call(
|
|
321
|
+
router.createTeam,
|
|
322
|
+
{ name: "New Team", description: "A new team" },
|
|
323
|
+
{ context }
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(result).toBeDefined();
|
|
327
|
+
expect(result.id).toBeDefined();
|
|
328
|
+
expect(typeof result.id).toBe("string");
|
|
329
|
+
expect(mockDb.insert).toHaveBeenCalled();
|
|
330
|
+
expect(insertedData?.name).toBe("New Team");
|
|
331
|
+
expect(insertedData?.description).toBe("A new team");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("creates team with minimal data", async () => {
|
|
335
|
+
const mockDb = createMockDb();
|
|
336
|
+
|
|
337
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
338
|
+
values: mock(() => Promise.resolve()),
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
const router = createAuthRouter(
|
|
342
|
+
mockDb,
|
|
343
|
+
mockRegistry,
|
|
344
|
+
async () => {},
|
|
345
|
+
mockConfigService,
|
|
346
|
+
mockPermissionRegistry
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
350
|
+
const result = await call(
|
|
351
|
+
router.createTeam,
|
|
352
|
+
{ name: "Minimal Team" },
|
|
353
|
+
{ context }
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(result).toBeDefined();
|
|
357
|
+
expect(result.id).toBeDefined();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("updateTeam", () => {
|
|
362
|
+
it("updates team name", async () => {
|
|
363
|
+
const mockDb = createMockDb();
|
|
364
|
+
let updatedData: Record<string, unknown> | undefined;
|
|
365
|
+
|
|
366
|
+
(mockDb.update as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
367
|
+
set: mock((data: Record<string, unknown>) => {
|
|
368
|
+
updatedData = data;
|
|
369
|
+
return {
|
|
370
|
+
where: mock(() => Promise.resolve()),
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
}));
|
|
374
|
+
|
|
375
|
+
const router = createAuthRouter(
|
|
376
|
+
mockDb,
|
|
377
|
+
mockRegistry,
|
|
378
|
+
async () => {},
|
|
379
|
+
mockConfigService,
|
|
380
|
+
mockPermissionRegistry
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
384
|
+
await call(
|
|
385
|
+
router.updateTeam,
|
|
386
|
+
{ id: "team-1", name: "Updated Name" },
|
|
387
|
+
{ context }
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect(mockDb.update).toHaveBeenCalled();
|
|
391
|
+
expect(updatedData?.name).toBe("Updated Name");
|
|
392
|
+
expect(updatedData?.updatedAt).toBeInstanceOf(Date);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("updates team description", async () => {
|
|
396
|
+
const mockDb = createMockDb();
|
|
397
|
+
let updatedData: Record<string, unknown> | undefined;
|
|
398
|
+
|
|
399
|
+
(mockDb.update as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
400
|
+
set: mock((data: Record<string, unknown>) => {
|
|
401
|
+
updatedData = data;
|
|
402
|
+
return {
|
|
403
|
+
where: mock(() => Promise.resolve()),
|
|
404
|
+
};
|
|
405
|
+
}),
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
const router = createAuthRouter(
|
|
409
|
+
mockDb,
|
|
410
|
+
mockRegistry,
|
|
411
|
+
async () => {},
|
|
412
|
+
mockConfigService,
|
|
413
|
+
mockPermissionRegistry
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
417
|
+
await call(
|
|
418
|
+
router.updateTeam,
|
|
419
|
+
{ id: "team-1", description: "New description" },
|
|
420
|
+
{ context }
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(updatedData?.description).toBe("New description");
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe("deleteTeam", () => {
|
|
428
|
+
it("deletes team and cascades to related tables", async () => {
|
|
429
|
+
const mockDb = createMockDb();
|
|
430
|
+
const deletedTables: unknown[] = [];
|
|
431
|
+
|
|
432
|
+
const mockTx = {
|
|
433
|
+
delete: mock((table: unknown) => {
|
|
434
|
+
deletedTables.push(table);
|
|
435
|
+
return {
|
|
436
|
+
where: mock(() => Promise.resolve()),
|
|
437
|
+
};
|
|
438
|
+
}),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
(mockDb.transaction as ReturnType<typeof mock>).mockImplementationOnce(
|
|
442
|
+
(cb: (tx: typeof mockTx) => Promise<void>) => cb(mockTx)
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const router = createAuthRouter(
|
|
446
|
+
mockDb,
|
|
447
|
+
mockRegistry,
|
|
448
|
+
async () => {},
|
|
449
|
+
mockConfigService,
|
|
450
|
+
mockPermissionRegistry
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
454
|
+
await call(router.deleteTeam, "team-1", { context });
|
|
455
|
+
|
|
456
|
+
expect(mockDb.transaction).toHaveBeenCalled();
|
|
457
|
+
expect(deletedTables).toHaveLength(5);
|
|
458
|
+
expect(deletedTables.includes(schema.userTeam)).toBe(true);
|
|
459
|
+
expect(deletedTables.includes(schema.teamManager)).toBe(true);
|
|
460
|
+
expect(deletedTables.includes(schema.applicationTeam)).toBe(true);
|
|
461
|
+
expect(deletedTables.includes(schema.resourceTeamAccess)).toBe(true);
|
|
462
|
+
expect(deletedTables.includes(schema.team)).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ==========================================================================
|
|
467
|
+
// TEAM MEMBERSHIP TESTS
|
|
468
|
+
// ==========================================================================
|
|
469
|
+
|
|
470
|
+
describe("addUserToTeam", () => {
|
|
471
|
+
it("adds user to team with conflict handling", async () => {
|
|
472
|
+
const mockDb = createMockDb();
|
|
473
|
+
let insertedData: Record<string, unknown> | undefined;
|
|
474
|
+
|
|
475
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
476
|
+
values: mock((data: Record<string, unknown>) => {
|
|
477
|
+
insertedData = data;
|
|
478
|
+
return {
|
|
479
|
+
onConflictDoNothing: mock(() => Promise.resolve()),
|
|
480
|
+
};
|
|
481
|
+
}),
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
const router = createAuthRouter(
|
|
485
|
+
mockDb,
|
|
486
|
+
mockRegistry,
|
|
487
|
+
async () => {},
|
|
488
|
+
mockConfigService,
|
|
489
|
+
mockPermissionRegistry
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
493
|
+
await call(
|
|
494
|
+
router.addUserToTeam,
|
|
495
|
+
{ teamId: "team-1", userId: "user-1" },
|
|
496
|
+
{ context }
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(mockDb.insert).toHaveBeenCalled();
|
|
500
|
+
expect(insertedData?.teamId).toBe("team-1");
|
|
501
|
+
expect(insertedData?.userId).toBe("user-1");
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe("removeUserFromTeam", () => {
|
|
506
|
+
it("removes user from team", async () => {
|
|
507
|
+
const mockDb = createMockDb();
|
|
508
|
+
|
|
509
|
+
const router = createAuthRouter(
|
|
510
|
+
mockDb,
|
|
511
|
+
mockRegistry,
|
|
512
|
+
async () => {},
|
|
513
|
+
mockConfigService,
|
|
514
|
+
mockPermissionRegistry
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
518
|
+
await call(
|
|
519
|
+
router.removeUserFromTeam,
|
|
520
|
+
{ teamId: "team-1", userId: "user-1" },
|
|
521
|
+
{ context }
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
expect(mockDb.delete).toHaveBeenCalled();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// ==========================================================================
|
|
529
|
+
// TEAM MANAGER TESTS
|
|
530
|
+
// ==========================================================================
|
|
531
|
+
|
|
532
|
+
describe("addTeamManager", () => {
|
|
533
|
+
it("grants manager privileges", async () => {
|
|
534
|
+
const mockDb = createMockDb();
|
|
535
|
+
let insertedData: Record<string, unknown> | undefined;
|
|
536
|
+
|
|
537
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
538
|
+
values: mock((data: Record<string, unknown>) => {
|
|
539
|
+
insertedData = data;
|
|
540
|
+
return {
|
|
541
|
+
onConflictDoNothing: mock(() => Promise.resolve()),
|
|
542
|
+
};
|
|
543
|
+
}),
|
|
544
|
+
}));
|
|
545
|
+
|
|
546
|
+
const router = createAuthRouter(
|
|
547
|
+
mockDb,
|
|
548
|
+
mockRegistry,
|
|
549
|
+
async () => {},
|
|
550
|
+
mockConfigService,
|
|
551
|
+
mockPermissionRegistry
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
555
|
+
await call(
|
|
556
|
+
router.addTeamManager,
|
|
557
|
+
{ teamId: "team-1", userId: "user-1" },
|
|
558
|
+
{ context }
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
expect(mockDb.insert).toHaveBeenCalled();
|
|
562
|
+
expect(insertedData?.teamId).toBe("team-1");
|
|
563
|
+
expect(insertedData?.userId).toBe("user-1");
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("removeTeamManager", () => {
|
|
568
|
+
it("revokes manager privileges", async () => {
|
|
569
|
+
const mockDb = createMockDb();
|
|
570
|
+
|
|
571
|
+
const router = createAuthRouter(
|
|
572
|
+
mockDb,
|
|
573
|
+
mockRegistry,
|
|
574
|
+
async () => {},
|
|
575
|
+
mockConfigService,
|
|
576
|
+
mockPermissionRegistry
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
580
|
+
await call(
|
|
581
|
+
router.removeTeamManager,
|
|
582
|
+
{ teamId: "team-1", userId: "user-1" },
|
|
583
|
+
{ context }
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(mockDb.delete).toHaveBeenCalled();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// ==========================================================================
|
|
591
|
+
// RESOURCE ACCESS GRANT TESTS
|
|
592
|
+
// ==========================================================================
|
|
593
|
+
|
|
594
|
+
describe("getResourceTeamAccess", () => {
|
|
595
|
+
it("returns empty array when no grants exist", async () => {
|
|
596
|
+
const mockDb = createMockDb();
|
|
597
|
+
|
|
598
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
599
|
+
from: mock(() => ({
|
|
600
|
+
innerJoin: mock(() => createChain([])),
|
|
601
|
+
})),
|
|
602
|
+
}));
|
|
603
|
+
|
|
604
|
+
const router = createAuthRouter(
|
|
605
|
+
mockDb,
|
|
606
|
+
mockRegistry,
|
|
607
|
+
async () => {},
|
|
608
|
+
mockConfigService,
|
|
609
|
+
mockPermissionRegistry
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
613
|
+
const result = await call(
|
|
614
|
+
router.getResourceTeamAccess,
|
|
615
|
+
{ resourceType: "catalog.system", resourceId: "sys-1" },
|
|
616
|
+
{ context }
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
expect(Array.isArray(result)).toBe(true);
|
|
620
|
+
expect(result).toHaveLength(0);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("returns grants with team names", async () => {
|
|
624
|
+
const mockDb = createMockDb();
|
|
625
|
+
|
|
626
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
627
|
+
from: mock(() => ({
|
|
628
|
+
innerJoin: mock(() =>
|
|
629
|
+
createChain([
|
|
630
|
+
{
|
|
631
|
+
resource_team_access: {
|
|
632
|
+
teamId: "team-1",
|
|
633
|
+
canRead: true,
|
|
634
|
+
canManage: true,
|
|
635
|
+
},
|
|
636
|
+
team: { name: "Platform Team" },
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
resource_team_access: {
|
|
640
|
+
teamId: "team-2",
|
|
641
|
+
canRead: true,
|
|
642
|
+
canManage: false,
|
|
643
|
+
},
|
|
644
|
+
team: { name: "API Team" },
|
|
645
|
+
},
|
|
646
|
+
])
|
|
647
|
+
),
|
|
648
|
+
})),
|
|
649
|
+
}));
|
|
650
|
+
|
|
651
|
+
const router = createAuthRouter(
|
|
652
|
+
mockDb,
|
|
653
|
+
mockRegistry,
|
|
654
|
+
async () => {},
|
|
655
|
+
mockConfigService,
|
|
656
|
+
mockPermissionRegistry
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
660
|
+
const result = await call(
|
|
661
|
+
router.getResourceTeamAccess,
|
|
662
|
+
{ resourceType: "catalog.system", resourceId: "sys-1" },
|
|
663
|
+
{ context }
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
expect(result).toHaveLength(2);
|
|
667
|
+
expect(result[0]).toEqual({
|
|
668
|
+
teamId: "team-1",
|
|
669
|
+
teamName: "Platform Team",
|
|
670
|
+
canRead: true,
|
|
671
|
+
canManage: true,
|
|
672
|
+
});
|
|
673
|
+
expect(result[1]).toEqual({
|
|
674
|
+
teamId: "team-2",
|
|
675
|
+
teamName: "API Team",
|
|
676
|
+
canRead: true,
|
|
677
|
+
canManage: false,
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("setResourceTeamAccess", () => {
|
|
683
|
+
it("creates new grant with default permissions", async () => {
|
|
684
|
+
const mockDb = createMockDb();
|
|
685
|
+
let insertedData: Record<string, unknown> | undefined;
|
|
686
|
+
|
|
687
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
688
|
+
values: mock((data: Record<string, unknown>) => {
|
|
689
|
+
insertedData = data;
|
|
690
|
+
return {
|
|
691
|
+
onConflictDoUpdate: mock(() => Promise.resolve()),
|
|
692
|
+
};
|
|
693
|
+
}),
|
|
694
|
+
}));
|
|
695
|
+
|
|
696
|
+
const router = createAuthRouter(
|
|
697
|
+
mockDb,
|
|
698
|
+
mockRegistry,
|
|
699
|
+
async () => {},
|
|
700
|
+
mockConfigService,
|
|
701
|
+
mockPermissionRegistry
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
705
|
+
await call(
|
|
706
|
+
router.setResourceTeamAccess,
|
|
707
|
+
{
|
|
708
|
+
resourceType: "catalog.system",
|
|
709
|
+
resourceId: "sys-1",
|
|
710
|
+
teamId: "team-1",
|
|
711
|
+
},
|
|
712
|
+
{ context }
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
expect(mockDb.insert).toHaveBeenCalled();
|
|
716
|
+
expect(insertedData?.resourceType).toBe("catalog.system");
|
|
717
|
+
expect(insertedData?.resourceId).toBe("sys-1");
|
|
718
|
+
expect(insertedData?.teamId).toBe("team-1");
|
|
719
|
+
expect(insertedData?.canRead).toBe(true);
|
|
720
|
+
expect(insertedData?.canManage).toBe(false);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("creates grant with custom permissions", async () => {
|
|
724
|
+
const mockDb = createMockDb();
|
|
725
|
+
let insertedData: Record<string, unknown> | undefined;
|
|
726
|
+
|
|
727
|
+
(mockDb.insert as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
728
|
+
values: mock((data: Record<string, unknown>) => {
|
|
729
|
+
insertedData = data;
|
|
730
|
+
return {
|
|
731
|
+
onConflictDoUpdate: mock(() => Promise.resolve()),
|
|
732
|
+
};
|
|
733
|
+
}),
|
|
734
|
+
}));
|
|
735
|
+
|
|
736
|
+
const router = createAuthRouter(
|
|
737
|
+
mockDb,
|
|
738
|
+
mockRegistry,
|
|
739
|
+
async () => {},
|
|
740
|
+
mockConfigService,
|
|
741
|
+
mockPermissionRegistry
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
745
|
+
await call(
|
|
746
|
+
router.setResourceTeamAccess,
|
|
747
|
+
{
|
|
748
|
+
resourceType: "catalog.system",
|
|
749
|
+
resourceId: "sys-1",
|
|
750
|
+
teamId: "team-1",
|
|
751
|
+
canRead: true,
|
|
752
|
+
canManage: true,
|
|
753
|
+
},
|
|
754
|
+
{ context }
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
expect(insertedData?.canRead).toBe(true);
|
|
758
|
+
expect(insertedData?.canManage).toBe(true);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe("removeResourceTeamAccess", () => {
|
|
763
|
+
it("removes grant for specific team", async () => {
|
|
764
|
+
const mockDb = createMockDb();
|
|
765
|
+
|
|
766
|
+
const router = createAuthRouter(
|
|
767
|
+
mockDb,
|
|
768
|
+
mockRegistry,
|
|
769
|
+
async () => {},
|
|
770
|
+
mockConfigService,
|
|
771
|
+
mockPermissionRegistry
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
const context = createMockRpcContext({ user: mockAdminUser });
|
|
775
|
+
await call(
|
|
776
|
+
router.removeResourceTeamAccess,
|
|
777
|
+
{
|
|
778
|
+
resourceType: "catalog.system",
|
|
779
|
+
resourceId: "sys-1",
|
|
780
|
+
teamId: "team-1",
|
|
781
|
+
},
|
|
782
|
+
{ context }
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
expect(mockDb.delete).toHaveBeenCalled();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// ==========================================================================
|
|
790
|
+
// S2S ACCESS CHECK TESTS
|
|
791
|
+
// ==========================================================================
|
|
792
|
+
|
|
793
|
+
describe("checkResourceTeamAccess (S2S)", () => {
|
|
794
|
+
it("allows access when no grants exist and user has global permission", async () => {
|
|
795
|
+
const mockDb = createMockDb();
|
|
796
|
+
|
|
797
|
+
// No grants exist
|
|
798
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
799
|
+
from: mock(() => createChain([])),
|
|
800
|
+
}));
|
|
801
|
+
|
|
802
|
+
const router = createAuthRouter(
|
|
803
|
+
mockDb,
|
|
804
|
+
mockRegistry,
|
|
805
|
+
async () => {},
|
|
806
|
+
mockConfigService,
|
|
807
|
+
mockPermissionRegistry
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
811
|
+
const result = await call(
|
|
812
|
+
router.checkResourceTeamAccess,
|
|
813
|
+
{
|
|
814
|
+
userId: "user-1",
|
|
815
|
+
userType: "user",
|
|
816
|
+
resourceType: "catalog.system",
|
|
817
|
+
resourceId: "sys-1",
|
|
818
|
+
action: "read",
|
|
819
|
+
hasGlobalPermission: true,
|
|
820
|
+
},
|
|
821
|
+
{ context }
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
expect(result.hasAccess).toBe(true);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it("denies access when no grants exist and user lacks global permission", async () => {
|
|
828
|
+
const mockDb = createMockDb();
|
|
829
|
+
|
|
830
|
+
// No grants exist
|
|
831
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
832
|
+
from: mock(() => createChain([])),
|
|
833
|
+
}));
|
|
834
|
+
|
|
835
|
+
const router = createAuthRouter(
|
|
836
|
+
mockDb,
|
|
837
|
+
mockRegistry,
|
|
838
|
+
async () => {},
|
|
839
|
+
mockConfigService,
|
|
840
|
+
mockPermissionRegistry
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
844
|
+
const result = await call(
|
|
845
|
+
router.checkResourceTeamAccess,
|
|
846
|
+
{
|
|
847
|
+
userId: "user-1",
|
|
848
|
+
userType: "user",
|
|
849
|
+
resourceType: "catalog.system",
|
|
850
|
+
resourceId: "sys-1",
|
|
851
|
+
action: "read",
|
|
852
|
+
hasGlobalPermission: false,
|
|
853
|
+
},
|
|
854
|
+
{ context }
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
expect(result.hasAccess).toBe(false);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it("allows access when user's team has grant with canRead", async () => {
|
|
861
|
+
const mockDb = createMockDb();
|
|
862
|
+
|
|
863
|
+
// Grant exists for team-1
|
|
864
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
865
|
+
from: mock(() =>
|
|
866
|
+
createChain([
|
|
867
|
+
{
|
|
868
|
+
teamId: "team-1",
|
|
869
|
+
canRead: true,
|
|
870
|
+
canManage: false,
|
|
871
|
+
},
|
|
872
|
+
])
|
|
873
|
+
),
|
|
874
|
+
}));
|
|
875
|
+
|
|
876
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
877
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
878
|
+
from: mock(() => createChain([])),
|
|
879
|
+
}));
|
|
880
|
+
|
|
881
|
+
// User is member of team-1
|
|
882
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
883
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
884
|
+
}));
|
|
885
|
+
|
|
886
|
+
const router = createAuthRouter(
|
|
887
|
+
mockDb,
|
|
888
|
+
mockRegistry,
|
|
889
|
+
async () => {},
|
|
890
|
+
mockConfigService,
|
|
891
|
+
mockPermissionRegistry
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
895
|
+
const result = await call(
|
|
896
|
+
router.checkResourceTeamAccess,
|
|
897
|
+
{
|
|
898
|
+
userId: "user-1",
|
|
899
|
+
userType: "user",
|
|
900
|
+
resourceType: "catalog.system",
|
|
901
|
+
resourceId: "sys-1",
|
|
902
|
+
action: "read",
|
|
903
|
+
hasGlobalPermission: false,
|
|
904
|
+
},
|
|
905
|
+
{ context }
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
expect(result.hasAccess).toBe(true);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it("denies access when user's team has grant but lacks canManage for manage action", async () => {
|
|
912
|
+
const mockDb = createMockDb();
|
|
913
|
+
|
|
914
|
+
// Grant exists for team-1 with only read permission
|
|
915
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
916
|
+
from: mock(() =>
|
|
917
|
+
createChain([
|
|
918
|
+
{
|
|
919
|
+
teamId: "team-1",
|
|
920
|
+
canRead: true,
|
|
921
|
+
canManage: false,
|
|
922
|
+
},
|
|
923
|
+
])
|
|
924
|
+
),
|
|
925
|
+
}));
|
|
926
|
+
|
|
927
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
928
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
929
|
+
from: mock(() => createChain([])),
|
|
930
|
+
}));
|
|
931
|
+
|
|
932
|
+
// User is member of team-1
|
|
933
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
934
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
935
|
+
}));
|
|
936
|
+
|
|
937
|
+
const router = createAuthRouter(
|
|
938
|
+
mockDb,
|
|
939
|
+
mockRegistry,
|
|
940
|
+
async () => {},
|
|
941
|
+
mockConfigService,
|
|
942
|
+
mockPermissionRegistry
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
946
|
+
const result = await call(
|
|
947
|
+
router.checkResourceTeamAccess,
|
|
948
|
+
{
|
|
949
|
+
userId: "user-1",
|
|
950
|
+
userType: "user",
|
|
951
|
+
resourceType: "catalog.system",
|
|
952
|
+
resourceId: "sys-1",
|
|
953
|
+
action: "manage",
|
|
954
|
+
hasGlobalPermission: false,
|
|
955
|
+
},
|
|
956
|
+
{ context }
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
expect(result.hasAccess).toBe(false);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it("allows access for teamOnly resource when user is in granted team", async () => {
|
|
963
|
+
const mockDb = createMockDb();
|
|
964
|
+
|
|
965
|
+
// Grant exists for team-1
|
|
966
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
967
|
+
from: mock(() =>
|
|
968
|
+
createChain([
|
|
969
|
+
{
|
|
970
|
+
teamId: "team-1",
|
|
971
|
+
canRead: true,
|
|
972
|
+
canManage: false,
|
|
973
|
+
},
|
|
974
|
+
])
|
|
975
|
+
),
|
|
976
|
+
}));
|
|
977
|
+
|
|
978
|
+
// Settings query - returns teamOnly = true
|
|
979
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
980
|
+
from: mock(() =>
|
|
981
|
+
createChain([{ teamOnly: true, resourceId: "sys-1" }])
|
|
982
|
+
),
|
|
983
|
+
}));
|
|
984
|
+
|
|
985
|
+
// User is member of team-1
|
|
986
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
987
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
988
|
+
}));
|
|
989
|
+
|
|
990
|
+
const router = createAuthRouter(
|
|
991
|
+
mockDb,
|
|
992
|
+
mockRegistry,
|
|
993
|
+
async () => {},
|
|
994
|
+
mockConfigService,
|
|
995
|
+
mockPermissionRegistry
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
999
|
+
const result = await call(
|
|
1000
|
+
router.checkResourceTeamAccess,
|
|
1001
|
+
{
|
|
1002
|
+
userId: "user-1",
|
|
1003
|
+
userType: "user",
|
|
1004
|
+
resourceType: "catalog.system",
|
|
1005
|
+
resourceId: "sys-1",
|
|
1006
|
+
action: "read",
|
|
1007
|
+
hasGlobalPermission: true, // Global permission doesn't help with teamOnly
|
|
1008
|
+
},
|
|
1009
|
+
{ context }
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
expect(result.hasAccess).toBe(true);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it("denies access for teamOnly resource when user is not in granted team", async () => {
|
|
1016
|
+
const mockDb = createMockDb();
|
|
1017
|
+
|
|
1018
|
+
// Grant exists for team-1
|
|
1019
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1020
|
+
from: mock(() =>
|
|
1021
|
+
createChain([
|
|
1022
|
+
{
|
|
1023
|
+
teamId: "team-1",
|
|
1024
|
+
canRead: true,
|
|
1025
|
+
canManage: false,
|
|
1026
|
+
},
|
|
1027
|
+
])
|
|
1028
|
+
),
|
|
1029
|
+
}));
|
|
1030
|
+
|
|
1031
|
+
// Settings query - returns teamOnly = true
|
|
1032
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1033
|
+
from: mock(() =>
|
|
1034
|
+
createChain([{ teamOnly: true, resourceId: "sys-1" }])
|
|
1035
|
+
),
|
|
1036
|
+
}));
|
|
1037
|
+
|
|
1038
|
+
// User is member of team-2 (not team-1)
|
|
1039
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1040
|
+
from: mock(() => createChain([{ teamId: "team-2" }])),
|
|
1041
|
+
}));
|
|
1042
|
+
|
|
1043
|
+
const router = createAuthRouter(
|
|
1044
|
+
mockDb,
|
|
1045
|
+
mockRegistry,
|
|
1046
|
+
async () => {},
|
|
1047
|
+
mockConfigService,
|
|
1048
|
+
mockPermissionRegistry
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1052
|
+
const result = await call(
|
|
1053
|
+
router.checkResourceTeamAccess,
|
|
1054
|
+
{
|
|
1055
|
+
userId: "user-1",
|
|
1056
|
+
userType: "user",
|
|
1057
|
+
resourceType: "catalog.system",
|
|
1058
|
+
resourceId: "sys-1",
|
|
1059
|
+
action: "read",
|
|
1060
|
+
hasGlobalPermission: true, // Global permission doesn't help with teamOnly
|
|
1061
|
+
},
|
|
1062
|
+
{ context }
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
expect(result.hasAccess).toBe(false);
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
describe("getAccessibleResourceIds (S2S)", () => {
|
|
1070
|
+
it("returns empty array for empty input", async () => {
|
|
1071
|
+
const mockDb = createMockDb();
|
|
1072
|
+
|
|
1073
|
+
const router = createAuthRouter(
|
|
1074
|
+
mockDb,
|
|
1075
|
+
mockRegistry,
|
|
1076
|
+
async () => {},
|
|
1077
|
+
mockConfigService,
|
|
1078
|
+
mockPermissionRegistry
|
|
1079
|
+
);
|
|
1080
|
+
|
|
1081
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1082
|
+
const result = await call(
|
|
1083
|
+
router.getAccessibleResourceIds,
|
|
1084
|
+
{
|
|
1085
|
+
userId: "user-1",
|
|
1086
|
+
userType: "user",
|
|
1087
|
+
resourceType: "catalog.system",
|
|
1088
|
+
resourceIds: [],
|
|
1089
|
+
action: "read",
|
|
1090
|
+
hasGlobalPermission: true,
|
|
1091
|
+
},
|
|
1092
|
+
{ context }
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
expect(result).toEqual([]);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it("returns all resources when no grants exist and user has global permission", async () => {
|
|
1099
|
+
const mockDb = createMockDb();
|
|
1100
|
+
|
|
1101
|
+
// No grants exist
|
|
1102
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1103
|
+
from: mock(() => createChain([])),
|
|
1104
|
+
}));
|
|
1105
|
+
|
|
1106
|
+
// User teams (not used when no grants)
|
|
1107
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1108
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1109
|
+
}));
|
|
1110
|
+
|
|
1111
|
+
const router = createAuthRouter(
|
|
1112
|
+
mockDb,
|
|
1113
|
+
mockRegistry,
|
|
1114
|
+
async () => {},
|
|
1115
|
+
mockConfigService,
|
|
1116
|
+
mockPermissionRegistry
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1120
|
+
const result = await call(
|
|
1121
|
+
router.getAccessibleResourceIds,
|
|
1122
|
+
{
|
|
1123
|
+
userId: "user-1",
|
|
1124
|
+
userType: "user",
|
|
1125
|
+
resourceType: "catalog.system",
|
|
1126
|
+
resourceIds: ["sys-1", "sys-2", "sys-3"],
|
|
1127
|
+
action: "read",
|
|
1128
|
+
hasGlobalPermission: true,
|
|
1129
|
+
},
|
|
1130
|
+
{ context }
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
expect(result).toEqual(["sys-1", "sys-2", "sys-3"]);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("filters resources based on team grants", async () => {
|
|
1137
|
+
const mockDb = createMockDb();
|
|
1138
|
+
|
|
1139
|
+
// Grants exist for sys-1 (team-1) and sys-2 (team-2)
|
|
1140
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1141
|
+
from: mock(() =>
|
|
1142
|
+
createChain([
|
|
1143
|
+
{
|
|
1144
|
+
resourceId: "sys-1",
|
|
1145
|
+
teamId: "team-1",
|
|
1146
|
+
canRead: true,
|
|
1147
|
+
canManage: false,
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
resourceId: "sys-2",
|
|
1151
|
+
teamId: "team-2",
|
|
1152
|
+
canRead: true,
|
|
1153
|
+
canManage: false,
|
|
1154
|
+
},
|
|
1155
|
+
])
|
|
1156
|
+
),
|
|
1157
|
+
}));
|
|
1158
|
+
|
|
1159
|
+
// Settings query - both sys-1 and sys-2 are teamOnly
|
|
1160
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1161
|
+
from: mock(() =>
|
|
1162
|
+
createChain([
|
|
1163
|
+
{ resourceId: "sys-1", teamOnly: true },
|
|
1164
|
+
{ resourceId: "sys-2", teamOnly: true },
|
|
1165
|
+
])
|
|
1166
|
+
),
|
|
1167
|
+
}));
|
|
1168
|
+
|
|
1169
|
+
// User is member of team-1 only
|
|
1170
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1171
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1172
|
+
}));
|
|
1173
|
+
|
|
1174
|
+
const router = createAuthRouter(
|
|
1175
|
+
mockDb,
|
|
1176
|
+
mockRegistry,
|
|
1177
|
+
async () => {},
|
|
1178
|
+
mockConfigService,
|
|
1179
|
+
mockPermissionRegistry
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1183
|
+
const result = await call(
|
|
1184
|
+
router.getAccessibleResourceIds,
|
|
1185
|
+
{
|
|
1186
|
+
userId: "user-1",
|
|
1187
|
+
userType: "user",
|
|
1188
|
+
resourceType: "catalog.system",
|
|
1189
|
+
resourceIds: ["sys-1", "sys-2", "sys-3"],
|
|
1190
|
+
action: "read",
|
|
1191
|
+
hasGlobalPermission: true,
|
|
1192
|
+
},
|
|
1193
|
+
{ context }
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// sys-1: user is in team-1, granted
|
|
1197
|
+
// sys-2: user is not in team-2, denied (teamOnly)
|
|
1198
|
+
// sys-3: no grants, allowed by global permission
|
|
1199
|
+
expect(result).toContain("sys-1");
|
|
1200
|
+
expect(result).not.toContain("sys-2");
|
|
1201
|
+
expect(result).toContain("sys-3");
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
describe("deleteResourceGrants (S2S)", () => {
|
|
1206
|
+
it("deletes all grants for a resource", async () => {
|
|
1207
|
+
const mockDb = createMockDb();
|
|
1208
|
+
|
|
1209
|
+
const router = createAuthRouter(
|
|
1210
|
+
mockDb,
|
|
1211
|
+
mockRegistry,
|
|
1212
|
+
async () => {},
|
|
1213
|
+
mockConfigService,
|
|
1214
|
+
mockPermissionRegistry
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1218
|
+
await call(
|
|
1219
|
+
router.deleteResourceGrants,
|
|
1220
|
+
{
|
|
1221
|
+
resourceType: "catalog.system",
|
|
1222
|
+
resourceId: "sys-1",
|
|
1223
|
+
},
|
|
1224
|
+
{ context }
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
expect(mockDb.delete).toHaveBeenCalled();
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
});
|