@checkstack/auth-backend 0.1.0 → 0.2.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 +96 -0
- package/drizzle/0004_lucky_power_man.sql +21 -0
- package/drizzle/meta/0004_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/index.ts +166 -162
- package/src/router.test.ts +11 -11
- package/src/router.ts +98 -98
- package/src/schema.ts +20 -20
- package/src/teams.test.ts +836 -81
- package/src/utils/user.test.ts +10 -10
- package/src/utils/user.ts +13 -13
package/src/teams.test.ts
CHANGED
|
@@ -20,20 +20,21 @@ type AuthDatabase = NodePgDatabase<typeof schema>;
|
|
|
20
20
|
* - S2S access checks (checkResourceTeamAccess, getAccessibleResourceIds)
|
|
21
21
|
*/
|
|
22
22
|
describe("Teams and Resource Access Control", () => {
|
|
23
|
-
// Mock user with admin
|
|
23
|
+
// Mock user with admin accesss
|
|
24
24
|
const mockAdminUser = {
|
|
25
25
|
type: "user" as const,
|
|
26
26
|
id: "admin-user",
|
|
27
|
-
|
|
27
|
+
accessRules: ["*"],
|
|
28
28
|
roles: ["admin"],
|
|
29
29
|
teamIds: ["team-alpha"],
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// Mock regular user with limited
|
|
32
|
+
// Mock regular user with limited access
|
|
33
|
+
// Note: Uses test-plugin prefix to match createMockRpcContext's pluginMetadata
|
|
33
34
|
const mockRegularUser = {
|
|
34
35
|
type: "user" as const,
|
|
35
36
|
id: "regular-user",
|
|
36
|
-
|
|
37
|
+
accessRules: ["test-plugin.teams.read"],
|
|
37
38
|
roles: ["users"],
|
|
38
39
|
teamIds: ["team-beta"],
|
|
39
40
|
};
|
|
@@ -117,8 +118,8 @@ describe("Teams and Resource Access Control", () => {
|
|
|
117
118
|
list: mock(() => Promise.resolve([])),
|
|
118
119
|
};
|
|
119
120
|
|
|
120
|
-
const
|
|
121
|
-
|
|
121
|
+
const mockAccessRuleRegistry = {
|
|
122
|
+
getAccessRules: () => [
|
|
122
123
|
{ id: "auth.teams.read", description: "View teams" },
|
|
123
124
|
{ id: "auth.teams.manage", description: "Manage teams" },
|
|
124
125
|
],
|
|
@@ -136,7 +137,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
136
137
|
mockRegistry,
|
|
137
138
|
async () => {},
|
|
138
139
|
mockConfigService,
|
|
139
|
-
|
|
140
|
+
mockAccessRuleRegistry
|
|
140
141
|
);
|
|
141
142
|
|
|
142
143
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -186,7 +187,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
186
187
|
mockRegistry,
|
|
187
188
|
async () => {},
|
|
188
189
|
mockConfigService,
|
|
189
|
-
|
|
190
|
+
mockAccessRuleRegistry
|
|
190
191
|
);
|
|
191
192
|
|
|
192
193
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -208,6 +209,115 @@ describe("Teams and Resource Access Control", () => {
|
|
|
208
209
|
isManager: false,
|
|
209
210
|
});
|
|
210
211
|
});
|
|
212
|
+
|
|
213
|
+
it("returns teams for regular user with read-only access", async () => {
|
|
214
|
+
const mockDb = createMockDb();
|
|
215
|
+
|
|
216
|
+
// Mock teams query
|
|
217
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
218
|
+
from: mock(() =>
|
|
219
|
+
createChain([
|
|
220
|
+
{
|
|
221
|
+
id: "team-alpha",
|
|
222
|
+
name: "Alpha Team",
|
|
223
|
+
description: "First team",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "team-beta",
|
|
227
|
+
name: "Beta Team",
|
|
228
|
+
description: "Second team",
|
|
229
|
+
},
|
|
230
|
+
])
|
|
231
|
+
),
|
|
232
|
+
}));
|
|
233
|
+
|
|
234
|
+
// Mock member counts query
|
|
235
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
236
|
+
from: mock(() =>
|
|
237
|
+
createChain([
|
|
238
|
+
{ teamId: "team-alpha" },
|
|
239
|
+
{ teamId: "team-beta" },
|
|
240
|
+
{ teamId: "team-beta" },
|
|
241
|
+
])
|
|
242
|
+
),
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
// Mock manager query (regular-user is not a manager of any team)
|
|
246
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
247
|
+
from: mock(() => createChain([])),
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const router = createAuthRouter(
|
|
251
|
+
mockDb,
|
|
252
|
+
mockRegistry,
|
|
253
|
+
async () => {},
|
|
254
|
+
mockConfigService,
|
|
255
|
+
mockAccessRuleRegistry
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Use mockRegularUser who has only auth.teams.read access
|
|
259
|
+
const context = createMockRpcContext({ user: mockRegularUser });
|
|
260
|
+
const result = await call(router.getTeams, undefined, { context });
|
|
261
|
+
|
|
262
|
+
expect(result).toHaveLength(2);
|
|
263
|
+
expect(result[0]).toEqual({
|
|
264
|
+
id: "team-alpha",
|
|
265
|
+
name: "Alpha Team",
|
|
266
|
+
description: "First team",
|
|
267
|
+
memberCount: 1,
|
|
268
|
+
isManager: false,
|
|
269
|
+
});
|
|
270
|
+
expect(result[1]).toEqual({
|
|
271
|
+
id: "team-beta",
|
|
272
|
+
name: "Beta Team",
|
|
273
|
+
description: "Second team",
|
|
274
|
+
memberCount: 2,
|
|
275
|
+
isManager: false,
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("shows correct manager status for regular user who is a team manager", async () => {
|
|
280
|
+
const mockDb = createMockDb();
|
|
281
|
+
|
|
282
|
+
// Mock teams query
|
|
283
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
284
|
+
from: mock(() =>
|
|
285
|
+
createChain([
|
|
286
|
+
{
|
|
287
|
+
id: "team-beta",
|
|
288
|
+
name: "Beta Team",
|
|
289
|
+
description: "User's team",
|
|
290
|
+
},
|
|
291
|
+
])
|
|
292
|
+
),
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
// Mock member counts query
|
|
296
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
297
|
+
from: mock(() => createChain([{ teamId: "team-beta" }])),
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
// Mock manager query (regular-user IS a manager of team-beta)
|
|
301
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
302
|
+
from: mock(() =>
|
|
303
|
+
createChain([{ teamId: "team-beta", userId: "regular-user" }])
|
|
304
|
+
),
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const router = createAuthRouter(
|
|
308
|
+
mockDb,
|
|
309
|
+
mockRegistry,
|
|
310
|
+
async () => {},
|
|
311
|
+
mockConfigService,
|
|
312
|
+
mockAccessRuleRegistry
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const context = createMockRpcContext({ user: mockRegularUser });
|
|
316
|
+
const result = await call(router.getTeams, undefined, { context });
|
|
317
|
+
|
|
318
|
+
expect(result).toHaveLength(1);
|
|
319
|
+
expect(result[0].isManager).toBe(true);
|
|
320
|
+
});
|
|
211
321
|
});
|
|
212
322
|
|
|
213
323
|
describe("getTeam", () => {
|
|
@@ -223,7 +333,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
223
333
|
mockRegistry,
|
|
224
334
|
async () => {},
|
|
225
335
|
mockConfigService,
|
|
226
|
-
|
|
336
|
+
mockAccessRuleRegistry
|
|
227
337
|
);
|
|
228
338
|
|
|
229
339
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -279,7 +389,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
279
389
|
mockRegistry,
|
|
280
390
|
async () => {},
|
|
281
391
|
mockConfigService,
|
|
282
|
-
|
|
392
|
+
mockAccessRuleRegistry
|
|
283
393
|
);
|
|
284
394
|
|
|
285
395
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -313,7 +423,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
313
423
|
mockRegistry,
|
|
314
424
|
async () => {},
|
|
315
425
|
mockConfigService,
|
|
316
|
-
|
|
426
|
+
mockAccessRuleRegistry
|
|
317
427
|
);
|
|
318
428
|
|
|
319
429
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -343,7 +453,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
343
453
|
mockRegistry,
|
|
344
454
|
async () => {},
|
|
345
455
|
mockConfigService,
|
|
346
|
-
|
|
456
|
+
mockAccessRuleRegistry
|
|
347
457
|
);
|
|
348
458
|
|
|
349
459
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -377,7 +487,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
377
487
|
mockRegistry,
|
|
378
488
|
async () => {},
|
|
379
489
|
mockConfigService,
|
|
380
|
-
|
|
490
|
+
mockAccessRuleRegistry
|
|
381
491
|
);
|
|
382
492
|
|
|
383
493
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -410,7 +520,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
410
520
|
mockRegistry,
|
|
411
521
|
async () => {},
|
|
412
522
|
mockConfigService,
|
|
413
|
-
|
|
523
|
+
mockAccessRuleRegistry
|
|
414
524
|
);
|
|
415
525
|
|
|
416
526
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -447,7 +557,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
447
557
|
mockRegistry,
|
|
448
558
|
async () => {},
|
|
449
559
|
mockConfigService,
|
|
450
|
-
|
|
560
|
+
mockAccessRuleRegistry
|
|
451
561
|
);
|
|
452
562
|
|
|
453
563
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -486,7 +596,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
486
596
|
mockRegistry,
|
|
487
597
|
async () => {},
|
|
488
598
|
mockConfigService,
|
|
489
|
-
|
|
599
|
+
mockAccessRuleRegistry
|
|
490
600
|
);
|
|
491
601
|
|
|
492
602
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -511,7 +621,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
511
621
|
mockRegistry,
|
|
512
622
|
async () => {},
|
|
513
623
|
mockConfigService,
|
|
514
|
-
|
|
624
|
+
mockAccessRuleRegistry
|
|
515
625
|
);
|
|
516
626
|
|
|
517
627
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -548,7 +658,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
548
658
|
mockRegistry,
|
|
549
659
|
async () => {},
|
|
550
660
|
mockConfigService,
|
|
551
|
-
|
|
661
|
+
mockAccessRuleRegistry
|
|
552
662
|
);
|
|
553
663
|
|
|
554
664
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -573,7 +683,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
573
683
|
mockRegistry,
|
|
574
684
|
async () => {},
|
|
575
685
|
mockConfigService,
|
|
576
|
-
|
|
686
|
+
mockAccessRuleRegistry
|
|
577
687
|
);
|
|
578
688
|
|
|
579
689
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -606,7 +716,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
606
716
|
mockRegistry,
|
|
607
717
|
async () => {},
|
|
608
718
|
mockConfigService,
|
|
609
|
-
|
|
719
|
+
mockAccessRuleRegistry
|
|
610
720
|
);
|
|
611
721
|
|
|
612
722
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -653,7 +763,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
653
763
|
mockRegistry,
|
|
654
764
|
async () => {},
|
|
655
765
|
mockConfigService,
|
|
656
|
-
|
|
766
|
+
mockAccessRuleRegistry
|
|
657
767
|
);
|
|
658
768
|
|
|
659
769
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -680,7 +790,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
680
790
|
});
|
|
681
791
|
|
|
682
792
|
describe("setResourceTeamAccess", () => {
|
|
683
|
-
it("creates new grant with default
|
|
793
|
+
it("creates new grant with default access", async () => {
|
|
684
794
|
const mockDb = createMockDb();
|
|
685
795
|
let insertedData: Record<string, unknown> | undefined;
|
|
686
796
|
|
|
@@ -698,7 +808,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
698
808
|
mockRegistry,
|
|
699
809
|
async () => {},
|
|
700
810
|
mockConfigService,
|
|
701
|
-
|
|
811
|
+
mockAccessRuleRegistry
|
|
702
812
|
);
|
|
703
813
|
|
|
704
814
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -720,7 +830,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
720
830
|
expect(insertedData?.canManage).toBe(false);
|
|
721
831
|
});
|
|
722
832
|
|
|
723
|
-
it("creates grant with custom
|
|
833
|
+
it("creates grant with custom access", async () => {
|
|
724
834
|
const mockDb = createMockDb();
|
|
725
835
|
let insertedData: Record<string, unknown> | undefined;
|
|
726
836
|
|
|
@@ -738,7 +848,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
738
848
|
mockRegistry,
|
|
739
849
|
async () => {},
|
|
740
850
|
mockConfigService,
|
|
741
|
-
|
|
851
|
+
mockAccessRuleRegistry
|
|
742
852
|
);
|
|
743
853
|
|
|
744
854
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -768,7 +878,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
768
878
|
mockRegistry,
|
|
769
879
|
async () => {},
|
|
770
880
|
mockConfigService,
|
|
771
|
-
|
|
881
|
+
mockAccessRuleRegistry
|
|
772
882
|
);
|
|
773
883
|
|
|
774
884
|
const context = createMockRpcContext({ user: mockAdminUser });
|
|
@@ -791,7 +901,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
791
901
|
// ==========================================================================
|
|
792
902
|
|
|
793
903
|
describe("checkResourceTeamAccess (S2S)", () => {
|
|
794
|
-
it("allows access when no grants exist and user has global
|
|
904
|
+
it("allows access when no grants exist and user has global access", async () => {
|
|
795
905
|
const mockDb = createMockDb();
|
|
796
906
|
|
|
797
907
|
// No grants exist
|
|
@@ -804,7 +914,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
804
914
|
mockRegistry,
|
|
805
915
|
async () => {},
|
|
806
916
|
mockConfigService,
|
|
807
|
-
|
|
917
|
+
mockAccessRuleRegistry
|
|
808
918
|
);
|
|
809
919
|
|
|
810
920
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -816,7 +926,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
816
926
|
resourceType: "catalog.system",
|
|
817
927
|
resourceId: "sys-1",
|
|
818
928
|
action: "read",
|
|
819
|
-
|
|
929
|
+
hasGlobalAccess: true,
|
|
820
930
|
},
|
|
821
931
|
{ context }
|
|
822
932
|
);
|
|
@@ -824,7 +934,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
824
934
|
expect(result.hasAccess).toBe(true);
|
|
825
935
|
});
|
|
826
936
|
|
|
827
|
-
it("denies access when no grants exist and user lacks global
|
|
937
|
+
it("denies access when no grants exist and user lacks global access", async () => {
|
|
828
938
|
const mockDb = createMockDb();
|
|
829
939
|
|
|
830
940
|
// No grants exist
|
|
@@ -837,7 +947,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
837
947
|
mockRegistry,
|
|
838
948
|
async () => {},
|
|
839
949
|
mockConfigService,
|
|
840
|
-
|
|
950
|
+
mockAccessRuleRegistry
|
|
841
951
|
);
|
|
842
952
|
|
|
843
953
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -849,7 +959,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
849
959
|
resourceType: "catalog.system",
|
|
850
960
|
resourceId: "sys-1",
|
|
851
961
|
action: "read",
|
|
852
|
-
|
|
962
|
+
hasGlobalAccess: false,
|
|
853
963
|
},
|
|
854
964
|
{ context }
|
|
855
965
|
);
|
|
@@ -888,7 +998,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
888
998
|
mockRegistry,
|
|
889
999
|
async () => {},
|
|
890
1000
|
mockConfigService,
|
|
891
|
-
|
|
1001
|
+
mockAccessRuleRegistry
|
|
892
1002
|
);
|
|
893
1003
|
|
|
894
1004
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -900,7 +1010,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
900
1010
|
resourceType: "catalog.system",
|
|
901
1011
|
resourceId: "sys-1",
|
|
902
1012
|
action: "read",
|
|
903
|
-
|
|
1013
|
+
hasGlobalAccess: false,
|
|
904
1014
|
},
|
|
905
1015
|
{ context }
|
|
906
1016
|
);
|
|
@@ -911,7 +1021,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
911
1021
|
it("denies access when user's team has grant but lacks canManage for manage action", async () => {
|
|
912
1022
|
const mockDb = createMockDb();
|
|
913
1023
|
|
|
914
|
-
// Grant exists for team-1 with only read
|
|
1024
|
+
// Grant exists for team-1 with only read access
|
|
915
1025
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
916
1026
|
from: mock(() =>
|
|
917
1027
|
createChain([
|
|
@@ -939,7 +1049,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
939
1049
|
mockRegistry,
|
|
940
1050
|
async () => {},
|
|
941
1051
|
mockConfigService,
|
|
942
|
-
|
|
1052
|
+
mockAccessRuleRegistry
|
|
943
1053
|
);
|
|
944
1054
|
|
|
945
1055
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -951,7 +1061,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
951
1061
|
resourceType: "catalog.system",
|
|
952
1062
|
resourceId: "sys-1",
|
|
953
1063
|
action: "manage",
|
|
954
|
-
|
|
1064
|
+
hasGlobalAccess: false,
|
|
955
1065
|
},
|
|
956
1066
|
{ context }
|
|
957
1067
|
);
|
|
@@ -992,7 +1102,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
992
1102
|
mockRegistry,
|
|
993
1103
|
async () => {},
|
|
994
1104
|
mockConfigService,
|
|
995
|
-
|
|
1105
|
+
mockAccessRuleRegistry
|
|
996
1106
|
);
|
|
997
1107
|
|
|
998
1108
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -1004,7 +1114,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1004
1114
|
resourceType: "catalog.system",
|
|
1005
1115
|
resourceId: "sys-1",
|
|
1006
1116
|
action: "read",
|
|
1007
|
-
|
|
1117
|
+
hasGlobalAccess: true, // Global access doesn't help with teamOnly
|
|
1008
1118
|
},
|
|
1009
1119
|
{ context }
|
|
1010
1120
|
);
|
|
@@ -1045,7 +1155,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1045
1155
|
mockRegistry,
|
|
1046
1156
|
async () => {},
|
|
1047
1157
|
mockConfigService,
|
|
1048
|
-
|
|
1158
|
+
mockAccessRuleRegistry
|
|
1049
1159
|
);
|
|
1050
1160
|
|
|
1051
1161
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
@@ -1057,55 +1167,84 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1057
1167
|
resourceType: "catalog.system",
|
|
1058
1168
|
resourceId: "sys-1",
|
|
1059
1169
|
action: "read",
|
|
1060
|
-
|
|
1170
|
+
hasGlobalAccess: true, // Global access doesn't help with teamOnly
|
|
1061
1171
|
},
|
|
1062
1172
|
{ context }
|
|
1063
1173
|
);
|
|
1064
1174
|
|
|
1065
1175
|
expect(result.hasAccess).toBe(false);
|
|
1066
1176
|
});
|
|
1067
|
-
});
|
|
1068
1177
|
|
|
1069
|
-
|
|
1070
|
-
it("returns empty array for empty input", async () => {
|
|
1178
|
+
it("allows manage access when user's team has canManage grant", async () => {
|
|
1071
1179
|
const mockDb = createMockDb();
|
|
1072
1180
|
|
|
1181
|
+
// Grant exists for team-1 with canManage
|
|
1182
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1183
|
+
from: mock(() =>
|
|
1184
|
+
createChain([
|
|
1185
|
+
{
|
|
1186
|
+
teamId: "team-1",
|
|
1187
|
+
canRead: true,
|
|
1188
|
+
canManage: true,
|
|
1189
|
+
},
|
|
1190
|
+
])
|
|
1191
|
+
),
|
|
1192
|
+
}));
|
|
1193
|
+
|
|
1194
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
1195
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1196
|
+
from: mock(() => createChain([])),
|
|
1197
|
+
}));
|
|
1198
|
+
|
|
1199
|
+
// User is member of team-1
|
|
1200
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1201
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1202
|
+
}));
|
|
1203
|
+
|
|
1073
1204
|
const router = createAuthRouter(
|
|
1074
1205
|
mockDb,
|
|
1075
1206
|
mockRegistry,
|
|
1076
1207
|
async () => {},
|
|
1077
1208
|
mockConfigService,
|
|
1078
|
-
|
|
1209
|
+
mockAccessRuleRegistry
|
|
1079
1210
|
);
|
|
1080
1211
|
|
|
1081
1212
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1082
1213
|
const result = await call(
|
|
1083
|
-
router.
|
|
1214
|
+
router.checkResourceTeamAccess,
|
|
1084
1215
|
{
|
|
1085
1216
|
userId: "user-1",
|
|
1086
1217
|
userType: "user",
|
|
1087
1218
|
resourceType: "catalog.system",
|
|
1088
|
-
|
|
1089
|
-
action: "
|
|
1090
|
-
|
|
1219
|
+
resourceId: "sys-1",
|
|
1220
|
+
action: "manage",
|
|
1221
|
+
hasGlobalAccess: false,
|
|
1091
1222
|
},
|
|
1092
1223
|
{ context }
|
|
1093
1224
|
);
|
|
1094
1225
|
|
|
1095
|
-
expect(result).
|
|
1226
|
+
expect(result.hasAccess).toBe(true);
|
|
1096
1227
|
});
|
|
1097
1228
|
|
|
1098
|
-
it("
|
|
1229
|
+
it("allows access via global access when grants exist but resource is not teamOnly", async () => {
|
|
1099
1230
|
const mockDb = createMockDb();
|
|
1100
1231
|
|
|
1101
|
-
//
|
|
1232
|
+
// Grant exists for team-1 but user is not in team-1
|
|
1102
1233
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1103
|
-
from: mock(() =>
|
|
1234
|
+
from: mock(() =>
|
|
1235
|
+
createChain([
|
|
1236
|
+
{
|
|
1237
|
+
teamId: "team-1",
|
|
1238
|
+
canRead: true,
|
|
1239
|
+
canManage: false,
|
|
1240
|
+
},
|
|
1241
|
+
])
|
|
1242
|
+
),
|
|
1104
1243
|
}));
|
|
1105
1244
|
|
|
1106
|
-
//
|
|
1245
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
1107
1246
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1108
|
-
from: mock(() => createChain([
|
|
1247
|
+
from: mock(() => createChain([])),
|
|
1109
1248
|
}));
|
|
1110
1249
|
|
|
1111
1250
|
const router = createAuthRouter(
|
|
@@ -1113,41 +1252,85 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1113
1252
|
mockRegistry,
|
|
1114
1253
|
async () => {},
|
|
1115
1254
|
mockConfigService,
|
|
1116
|
-
|
|
1255
|
+
mockAccessRuleRegistry
|
|
1117
1256
|
);
|
|
1118
1257
|
|
|
1119
1258
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1120
1259
|
const result = await call(
|
|
1121
|
-
router.
|
|
1260
|
+
router.checkResourceTeamAccess,
|
|
1122
1261
|
{
|
|
1123
1262
|
userId: "user-1",
|
|
1124
1263
|
userType: "user",
|
|
1125
1264
|
resourceType: "catalog.system",
|
|
1126
|
-
|
|
1265
|
+
resourceId: "sys-1",
|
|
1127
1266
|
action: "read",
|
|
1128
|
-
|
|
1267
|
+
hasGlobalAccess: true, // User has global access
|
|
1129
1268
|
},
|
|
1130
1269
|
{ context }
|
|
1131
1270
|
);
|
|
1132
1271
|
|
|
1133
|
-
expect(result).
|
|
1272
|
+
expect(result.hasAccess).toBe(true);
|
|
1134
1273
|
});
|
|
1135
1274
|
|
|
1136
|
-
it("
|
|
1275
|
+
it("denies access when user is not in any team and lacks global access", async () => {
|
|
1137
1276
|
const mockDb = createMockDb();
|
|
1138
1277
|
|
|
1139
|
-
//
|
|
1278
|
+
// Grant exists for team-1
|
|
1140
1279
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1141
1280
|
from: mock(() =>
|
|
1142
1281
|
createChain([
|
|
1143
1282
|
{
|
|
1144
|
-
resourceId: "sys-1",
|
|
1145
1283
|
teamId: "team-1",
|
|
1146
1284
|
canRead: true,
|
|
1147
1285
|
canManage: false,
|
|
1148
1286
|
},
|
|
1287
|
+
])
|
|
1288
|
+
),
|
|
1289
|
+
}));
|
|
1290
|
+
|
|
1291
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
1292
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1293
|
+
from: mock(() => createChain([])),
|
|
1294
|
+
}));
|
|
1295
|
+
|
|
1296
|
+
// User is not in any team
|
|
1297
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1298
|
+
from: mock(() => createChain([])),
|
|
1299
|
+
}));
|
|
1300
|
+
|
|
1301
|
+
const router = createAuthRouter(
|
|
1302
|
+
mockDb,
|
|
1303
|
+
mockRegistry,
|
|
1304
|
+
async () => {},
|
|
1305
|
+
mockConfigService,
|
|
1306
|
+
mockAccessRuleRegistry
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1310
|
+
const result = await call(
|
|
1311
|
+
router.checkResourceTeamAccess,
|
|
1312
|
+
{
|
|
1313
|
+
userId: "user-1",
|
|
1314
|
+
userType: "user",
|
|
1315
|
+
resourceType: "catalog.system",
|
|
1316
|
+
resourceId: "sys-1",
|
|
1317
|
+
action: "read",
|
|
1318
|
+
hasGlobalAccess: false,
|
|
1319
|
+
},
|
|
1320
|
+
{ context }
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
expect(result.hasAccess).toBe(false);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it("allows access when user is in multiple teams and one has the required grant", async () => {
|
|
1327
|
+
const mockDb = createMockDb();
|
|
1328
|
+
|
|
1329
|
+
// Grant exists for team-2 only
|
|
1330
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1331
|
+
from: mock(() =>
|
|
1332
|
+
createChain([
|
|
1149
1333
|
{
|
|
1150
|
-
resourceId: "sys-2",
|
|
1151
1334
|
teamId: "team-2",
|
|
1152
1335
|
canRead: true,
|
|
1153
1336
|
canManage: false,
|
|
@@ -1156,19 +1339,74 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1156
1339
|
),
|
|
1157
1340
|
}));
|
|
1158
1341
|
|
|
1159
|
-
// Settings query -
|
|
1342
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
1343
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1344
|
+
from: mock(() => createChain([])),
|
|
1345
|
+
}));
|
|
1346
|
+
|
|
1347
|
+
// User is member of team-1 AND team-2
|
|
1348
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1349
|
+
from: mock(() =>
|
|
1350
|
+
createChain([{ teamId: "team-1" }, { teamId: "team-2" }])
|
|
1351
|
+
),
|
|
1352
|
+
}));
|
|
1353
|
+
|
|
1354
|
+
const router = createAuthRouter(
|
|
1355
|
+
mockDb,
|
|
1356
|
+
mockRegistry,
|
|
1357
|
+
async () => {},
|
|
1358
|
+
mockConfigService,
|
|
1359
|
+
mockAccessRuleRegistry
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1363
|
+
const result = await call(
|
|
1364
|
+
router.checkResourceTeamAccess,
|
|
1365
|
+
{
|
|
1366
|
+
userId: "user-1",
|
|
1367
|
+
userType: "user",
|
|
1368
|
+
resourceType: "catalog.system",
|
|
1369
|
+
resourceId: "sys-1",
|
|
1370
|
+
action: "read",
|
|
1371
|
+
hasGlobalAccess: false,
|
|
1372
|
+
},
|
|
1373
|
+
{ context }
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
expect(result.hasAccess).toBe(true);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
it("allows access when resource has grants from multiple teams and user is in one of them", async () => {
|
|
1380
|
+
const mockDb = createMockDb();
|
|
1381
|
+
|
|
1382
|
+
// Grants exist for team-1 and team-2
|
|
1160
1383
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1161
1384
|
from: mock(() =>
|
|
1162
1385
|
createChain([
|
|
1163
|
-
{
|
|
1164
|
-
|
|
1386
|
+
{
|
|
1387
|
+
teamId: "team-1",
|
|
1388
|
+
canRead: true,
|
|
1389
|
+
canManage: false,
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
teamId: "team-2",
|
|
1393
|
+
canRead: true,
|
|
1394
|
+
canManage: true,
|
|
1395
|
+
},
|
|
1165
1396
|
])
|
|
1166
1397
|
),
|
|
1167
1398
|
}));
|
|
1168
1399
|
|
|
1169
|
-
//
|
|
1400
|
+
// Settings query - teamOnly = true
|
|
1170
1401
|
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1171
|
-
from: mock(() =>
|
|
1402
|
+
from: mock(() =>
|
|
1403
|
+
createChain([{ teamOnly: true, resourceId: "sys-1" }])
|
|
1404
|
+
),
|
|
1405
|
+
}));
|
|
1406
|
+
|
|
1407
|
+
// User is member of team-2 only
|
|
1408
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1409
|
+
from: mock(() => createChain([{ teamId: "team-2" }])),
|
|
1172
1410
|
}));
|
|
1173
1411
|
|
|
1174
1412
|
const router = createAuthRouter(
|
|
@@ -1176,29 +1414,546 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1176
1414
|
mockRegistry,
|
|
1177
1415
|
async () => {},
|
|
1178
1416
|
mockConfigService,
|
|
1179
|
-
|
|
1417
|
+
mockAccessRuleRegistry
|
|
1180
1418
|
);
|
|
1181
1419
|
|
|
1182
1420
|
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1183
1421
|
const result = await call(
|
|
1184
|
-
router.
|
|
1422
|
+
router.checkResourceTeamAccess,
|
|
1185
1423
|
{
|
|
1186
1424
|
userId: "user-1",
|
|
1187
1425
|
userType: "user",
|
|
1188
1426
|
resourceType: "catalog.system",
|
|
1189
|
-
|
|
1190
|
-
action: "
|
|
1191
|
-
|
|
1427
|
+
resourceId: "sys-1",
|
|
1428
|
+
action: "manage",
|
|
1429
|
+
hasGlobalAccess: false,
|
|
1192
1430
|
},
|
|
1193
1431
|
{ context }
|
|
1194
1432
|
);
|
|
1195
1433
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1434
|
+
expect(result.hasAccess).toBe(true);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it("allows access for application user with proper team grant", async () => {
|
|
1438
|
+
const mockDb = createMockDb();
|
|
1439
|
+
|
|
1440
|
+
// Grant exists for team-1
|
|
1441
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1442
|
+
from: mock(() =>
|
|
1443
|
+
createChain([
|
|
1444
|
+
{
|
|
1445
|
+
teamId: "team-1",
|
|
1446
|
+
canRead: true,
|
|
1447
|
+
canManage: false,
|
|
1448
|
+
},
|
|
1449
|
+
])
|
|
1450
|
+
),
|
|
1451
|
+
}));
|
|
1452
|
+
|
|
1453
|
+
// Settings query - returns empty (teamOnly = false by default)
|
|
1454
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1455
|
+
from: mock(() => createChain([])),
|
|
1456
|
+
}));
|
|
1457
|
+
|
|
1458
|
+
// Application is member of team-1
|
|
1459
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1460
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1461
|
+
}));
|
|
1462
|
+
|
|
1463
|
+
const router = createAuthRouter(
|
|
1464
|
+
mockDb,
|
|
1465
|
+
mockRegistry,
|
|
1466
|
+
async () => {},
|
|
1467
|
+
mockConfigService,
|
|
1468
|
+
mockAccessRuleRegistry
|
|
1469
|
+
);
|
|
1470
|
+
|
|
1471
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1472
|
+
const result = await call(
|
|
1473
|
+
router.checkResourceTeamAccess,
|
|
1474
|
+
{
|
|
1475
|
+
userId: "app-1",
|
|
1476
|
+
userType: "application",
|
|
1477
|
+
resourceType: "catalog.system",
|
|
1478
|
+
resourceId: "sys-1",
|
|
1479
|
+
action: "read",
|
|
1480
|
+
hasGlobalAccess: false,
|
|
1481
|
+
},
|
|
1482
|
+
{ context }
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
expect(result.hasAccess).toBe(true);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
it("denies access for application user when not in granted team", async () => {
|
|
1489
|
+
const mockDb = createMockDb();
|
|
1490
|
+
|
|
1491
|
+
// Grant exists for team-1
|
|
1492
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1493
|
+
from: mock(() =>
|
|
1494
|
+
createChain([
|
|
1495
|
+
{
|
|
1496
|
+
teamId: "team-1",
|
|
1497
|
+
canRead: true,
|
|
1498
|
+
canManage: false,
|
|
1499
|
+
},
|
|
1500
|
+
])
|
|
1501
|
+
),
|
|
1502
|
+
}));
|
|
1503
|
+
|
|
1504
|
+
// Settings query - teamOnly = true
|
|
1505
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1506
|
+
from: mock(() =>
|
|
1507
|
+
createChain([{ teamOnly: true, resourceId: "sys-1" }])
|
|
1508
|
+
),
|
|
1509
|
+
}));
|
|
1510
|
+
|
|
1511
|
+
// Application is member of team-2 (not team-1)
|
|
1512
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1513
|
+
from: mock(() => createChain([{ teamId: "team-2" }])),
|
|
1514
|
+
}));
|
|
1515
|
+
|
|
1516
|
+
const router = createAuthRouter(
|
|
1517
|
+
mockDb,
|
|
1518
|
+
mockRegistry,
|
|
1519
|
+
async () => {},
|
|
1520
|
+
mockConfigService,
|
|
1521
|
+
mockAccessRuleRegistry
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1525
|
+
const result = await call(
|
|
1526
|
+
router.checkResourceTeamAccess,
|
|
1527
|
+
{
|
|
1528
|
+
userId: "app-1",
|
|
1529
|
+
userType: "application",
|
|
1530
|
+
resourceType: "catalog.system",
|
|
1531
|
+
resourceId: "sys-1",
|
|
1532
|
+
action: "read",
|
|
1533
|
+
hasGlobalAccess: true,
|
|
1534
|
+
},
|
|
1535
|
+
{ context }
|
|
1536
|
+
);
|
|
1537
|
+
|
|
1538
|
+
expect(result.hasAccess).toBe(false);
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
it("denies read access when user is in team but grant only has canManage (no canRead)", async () => {
|
|
1542
|
+
const mockDb = createMockDb();
|
|
1543
|
+
|
|
1544
|
+
// Grant exists for team-1 with only canManage (canRead = false)
|
|
1545
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1546
|
+
from: mock(() =>
|
|
1547
|
+
createChain([
|
|
1548
|
+
{
|
|
1549
|
+
teamId: "team-1",
|
|
1550
|
+
canRead: false,
|
|
1551
|
+
canManage: true,
|
|
1552
|
+
},
|
|
1553
|
+
])
|
|
1554
|
+
),
|
|
1555
|
+
}));
|
|
1556
|
+
|
|
1557
|
+
// Settings query - teamOnly = true
|
|
1558
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1559
|
+
from: mock(() =>
|
|
1560
|
+
createChain([{ teamOnly: true, resourceId: "sys-1" }])
|
|
1561
|
+
),
|
|
1562
|
+
}));
|
|
1563
|
+
|
|
1564
|
+
// User is member of team-1
|
|
1565
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1566
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1567
|
+
}));
|
|
1568
|
+
|
|
1569
|
+
const router = createAuthRouter(
|
|
1570
|
+
mockDb,
|
|
1571
|
+
mockRegistry,
|
|
1572
|
+
async () => {},
|
|
1573
|
+
mockConfigService,
|
|
1574
|
+
mockAccessRuleRegistry
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1578
|
+
const result = await call(
|
|
1579
|
+
router.checkResourceTeamAccess,
|
|
1580
|
+
{
|
|
1581
|
+
userId: "user-1",
|
|
1582
|
+
userType: "user",
|
|
1583
|
+
resourceType: "catalog.system",
|
|
1584
|
+
resourceId: "sys-1",
|
|
1585
|
+
action: "read",
|
|
1586
|
+
hasGlobalAccess: false,
|
|
1587
|
+
},
|
|
1588
|
+
{ context }
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1591
|
+
expect(result.hasAccess).toBe(false);
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
describe("getAccessibleResourceIds (S2S)", () => {
|
|
1596
|
+
it("returns empty array for empty input", async () => {
|
|
1597
|
+
const mockDb = createMockDb();
|
|
1598
|
+
|
|
1599
|
+
const router = createAuthRouter(
|
|
1600
|
+
mockDb,
|
|
1601
|
+
mockRegistry,
|
|
1602
|
+
async () => {},
|
|
1603
|
+
mockConfigService,
|
|
1604
|
+
mockAccessRuleRegistry
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1608
|
+
const result = await call(
|
|
1609
|
+
router.getAccessibleResourceIds,
|
|
1610
|
+
{
|
|
1611
|
+
userId: "user-1",
|
|
1612
|
+
userType: "user",
|
|
1613
|
+
resourceType: "catalog.system",
|
|
1614
|
+
resourceIds: [],
|
|
1615
|
+
action: "read",
|
|
1616
|
+
hasGlobalAccess: true,
|
|
1617
|
+
},
|
|
1618
|
+
{ context }
|
|
1619
|
+
);
|
|
1620
|
+
|
|
1621
|
+
expect(result).toEqual([]);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it("returns all resources when no grants exist and user has global access", async () => {
|
|
1625
|
+
const mockDb = createMockDb();
|
|
1626
|
+
|
|
1627
|
+
// No grants exist
|
|
1628
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1629
|
+
from: mock(() => createChain([])),
|
|
1630
|
+
}));
|
|
1631
|
+
|
|
1632
|
+
// User teams (not used when no grants)
|
|
1633
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1634
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1635
|
+
}));
|
|
1636
|
+
|
|
1637
|
+
const router = createAuthRouter(
|
|
1638
|
+
mockDb,
|
|
1639
|
+
mockRegistry,
|
|
1640
|
+
async () => {},
|
|
1641
|
+
mockConfigService,
|
|
1642
|
+
mockAccessRuleRegistry
|
|
1643
|
+
);
|
|
1644
|
+
|
|
1645
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1646
|
+
const result = await call(
|
|
1647
|
+
router.getAccessibleResourceIds,
|
|
1648
|
+
{
|
|
1649
|
+
userId: "user-1",
|
|
1650
|
+
userType: "user",
|
|
1651
|
+
resourceType: "catalog.system",
|
|
1652
|
+
resourceIds: ["sys-1", "sys-2", "sys-3"],
|
|
1653
|
+
action: "read",
|
|
1654
|
+
hasGlobalAccess: true,
|
|
1655
|
+
},
|
|
1656
|
+
{ context }
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
expect(result).toEqual(["sys-1", "sys-2", "sys-3"]);
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
it("filters resources based on team grants", async () => {
|
|
1663
|
+
const mockDb = createMockDb();
|
|
1664
|
+
|
|
1665
|
+
// Grants exist for sys-1 (team-1) and sys-2 (team-2)
|
|
1666
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1667
|
+
from: mock(() =>
|
|
1668
|
+
createChain([
|
|
1669
|
+
{
|
|
1670
|
+
resourceId: "sys-1",
|
|
1671
|
+
teamId: "team-1",
|
|
1672
|
+
canRead: true,
|
|
1673
|
+
canManage: false,
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
resourceId: "sys-2",
|
|
1677
|
+
teamId: "team-2",
|
|
1678
|
+
canRead: true,
|
|
1679
|
+
canManage: false,
|
|
1680
|
+
},
|
|
1681
|
+
])
|
|
1682
|
+
),
|
|
1683
|
+
}));
|
|
1684
|
+
|
|
1685
|
+
// Settings query - both sys-1 and sys-2 are teamOnly
|
|
1686
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1687
|
+
from: mock(() =>
|
|
1688
|
+
createChain([
|
|
1689
|
+
{ resourceId: "sys-1", teamOnly: true },
|
|
1690
|
+
{ resourceId: "sys-2", teamOnly: true },
|
|
1691
|
+
])
|
|
1692
|
+
),
|
|
1693
|
+
}));
|
|
1694
|
+
|
|
1695
|
+
// User is member of team-1 only
|
|
1696
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1697
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1698
|
+
}));
|
|
1699
|
+
|
|
1700
|
+
const router = createAuthRouter(
|
|
1701
|
+
mockDb,
|
|
1702
|
+
mockRegistry,
|
|
1703
|
+
async () => {},
|
|
1704
|
+
mockConfigService,
|
|
1705
|
+
mockAccessRuleRegistry
|
|
1706
|
+
);
|
|
1707
|
+
|
|
1708
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1709
|
+
const result = await call(
|
|
1710
|
+
router.getAccessibleResourceIds,
|
|
1711
|
+
{
|
|
1712
|
+
userId: "user-1",
|
|
1713
|
+
userType: "user",
|
|
1714
|
+
resourceType: "catalog.system",
|
|
1715
|
+
resourceIds: ["sys-1", "sys-2", "sys-3"],
|
|
1716
|
+
action: "read",
|
|
1717
|
+
hasGlobalAccess: true,
|
|
1718
|
+
},
|
|
1719
|
+
{ context }
|
|
1720
|
+
);
|
|
1721
|
+
|
|
1722
|
+
// sys-1: user is in team-1, granted
|
|
1723
|
+
// sys-2: user is not in team-2, denied (teamOnly)
|
|
1724
|
+
// sys-3: no grants, allowed by global access
|
|
1725
|
+
expect(result).toContain("sys-1");
|
|
1726
|
+
expect(result).not.toContain("sys-2");
|
|
1727
|
+
expect(result).toContain("sys-3");
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
it("returns no resources when user lacks global access and has no grants", async () => {
|
|
1731
|
+
const mockDb = createMockDb();
|
|
1732
|
+
|
|
1733
|
+
// No grants exist
|
|
1734
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1735
|
+
from: mock(() => createChain([])),
|
|
1736
|
+
}));
|
|
1737
|
+
|
|
1738
|
+
// Settings query - empty
|
|
1739
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1740
|
+
from: mock(() => createChain([])),
|
|
1741
|
+
}));
|
|
1742
|
+
|
|
1743
|
+
// User teams - empty
|
|
1744
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1745
|
+
from: mock(() => createChain([])),
|
|
1746
|
+
}));
|
|
1747
|
+
|
|
1748
|
+
const router = createAuthRouter(
|
|
1749
|
+
mockDb,
|
|
1750
|
+
mockRegistry,
|
|
1751
|
+
async () => {},
|
|
1752
|
+
mockConfigService,
|
|
1753
|
+
mockAccessRuleRegistry
|
|
1754
|
+
);
|
|
1755
|
+
|
|
1756
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1757
|
+
const result = await call(
|
|
1758
|
+
router.getAccessibleResourceIds,
|
|
1759
|
+
{
|
|
1760
|
+
userId: "user-1",
|
|
1761
|
+
userType: "user",
|
|
1762
|
+
resourceType: "catalog.system",
|
|
1763
|
+
resourceIds: ["sys-1", "sys-2", "sys-3"],
|
|
1764
|
+
action: "read",
|
|
1765
|
+
hasGlobalAccess: false,
|
|
1766
|
+
},
|
|
1767
|
+
{ context }
|
|
1768
|
+
);
|
|
1769
|
+
|
|
1770
|
+
expect(result).toEqual([]);
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
it("filters manage action based on canManage grants", async () => {
|
|
1774
|
+
const mockDb = createMockDb();
|
|
1775
|
+
|
|
1776
|
+
// Grants exist - sys-1 has canManage, sys-2 only has canRead
|
|
1777
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1778
|
+
from: mock(() =>
|
|
1779
|
+
createChain([
|
|
1780
|
+
{
|
|
1781
|
+
resourceId: "sys-1",
|
|
1782
|
+
teamId: "team-1",
|
|
1783
|
+
canRead: true,
|
|
1784
|
+
canManage: true,
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
resourceId: "sys-2",
|
|
1788
|
+
teamId: "team-1",
|
|
1789
|
+
canRead: true,
|
|
1790
|
+
canManage: false,
|
|
1791
|
+
},
|
|
1792
|
+
])
|
|
1793
|
+
),
|
|
1794
|
+
}));
|
|
1795
|
+
|
|
1796
|
+
// Settings query - both are teamOnly
|
|
1797
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1798
|
+
from: mock(() =>
|
|
1799
|
+
createChain([
|
|
1800
|
+
{ resourceId: "sys-1", teamOnly: true },
|
|
1801
|
+
{ resourceId: "sys-2", teamOnly: true },
|
|
1802
|
+
])
|
|
1803
|
+
),
|
|
1804
|
+
}));
|
|
1805
|
+
|
|
1806
|
+
// User is member of team-1
|
|
1807
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1808
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1809
|
+
}));
|
|
1810
|
+
|
|
1811
|
+
const router = createAuthRouter(
|
|
1812
|
+
mockDb,
|
|
1813
|
+
mockRegistry,
|
|
1814
|
+
async () => {},
|
|
1815
|
+
mockConfigService,
|
|
1816
|
+
mockAccessRuleRegistry
|
|
1817
|
+
);
|
|
1818
|
+
|
|
1819
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1820
|
+
const result = await call(
|
|
1821
|
+
router.getAccessibleResourceIds,
|
|
1822
|
+
{
|
|
1823
|
+
userId: "user-1",
|
|
1824
|
+
userType: "user",
|
|
1825
|
+
resourceType: "catalog.system",
|
|
1826
|
+
resourceIds: ["sys-1", "sys-2"],
|
|
1827
|
+
action: "manage",
|
|
1828
|
+
hasGlobalAccess: false,
|
|
1829
|
+
},
|
|
1830
|
+
{ context }
|
|
1831
|
+
);
|
|
1832
|
+
|
|
1833
|
+
// sys-1: has canManage, granted
|
|
1834
|
+
// sys-2: only canRead, denied for manage action
|
|
1835
|
+
expect(result).toContain("sys-1");
|
|
1836
|
+
expect(result).not.toContain("sys-2");
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
it("filters resources for application user based on applicationTeam", async () => {
|
|
1840
|
+
const mockDb = createMockDb();
|
|
1841
|
+
|
|
1842
|
+
// Grants exist for sys-1 (team-1)
|
|
1843
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1844
|
+
from: mock(() =>
|
|
1845
|
+
createChain([
|
|
1846
|
+
{
|
|
1847
|
+
resourceId: "sys-1",
|
|
1848
|
+
teamId: "team-1",
|
|
1849
|
+
canRead: true,
|
|
1850
|
+
canManage: false,
|
|
1851
|
+
},
|
|
1852
|
+
])
|
|
1853
|
+
),
|
|
1854
|
+
}));
|
|
1855
|
+
|
|
1856
|
+
// Settings query - sys-1 is teamOnly
|
|
1857
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1858
|
+
from: mock(() =>
|
|
1859
|
+
createChain([{ resourceId: "sys-1", teamOnly: true }])
|
|
1860
|
+
),
|
|
1861
|
+
}));
|
|
1862
|
+
|
|
1863
|
+
// Application is member of team-1
|
|
1864
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1865
|
+
from: mock(() => createChain([{ teamId: "team-1" }])),
|
|
1866
|
+
}));
|
|
1867
|
+
|
|
1868
|
+
const router = createAuthRouter(
|
|
1869
|
+
mockDb,
|
|
1870
|
+
mockRegistry,
|
|
1871
|
+
async () => {},
|
|
1872
|
+
mockConfigService,
|
|
1873
|
+
mockAccessRuleRegistry
|
|
1874
|
+
);
|
|
1875
|
+
|
|
1876
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1877
|
+
const result = await call(
|
|
1878
|
+
router.getAccessibleResourceIds,
|
|
1879
|
+
{
|
|
1880
|
+
userId: "app-1",
|
|
1881
|
+
userType: "application",
|
|
1882
|
+
resourceType: "catalog.system",
|
|
1883
|
+
resourceIds: ["sys-1", "sys-2"],
|
|
1884
|
+
action: "read",
|
|
1885
|
+
hasGlobalAccess: true,
|
|
1886
|
+
},
|
|
1887
|
+
{ context }
|
|
1888
|
+
);
|
|
1889
|
+
|
|
1890
|
+
// sys-1: application is in team-1, granted
|
|
1891
|
+
// sys-2: no grants, allowed by global access
|
|
1892
|
+
expect(result).toContain("sys-1");
|
|
1893
|
+
expect(result).toContain("sys-2");
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
it("handles mixed teamOnly and non-teamOnly resources correctly", async () => {
|
|
1897
|
+
const mockDb = createMockDb();
|
|
1898
|
+
|
|
1899
|
+
// Grants exist for sys-1 (teamOnly) and sys-2 (not teamOnly)
|
|
1900
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1901
|
+
from: mock(() =>
|
|
1902
|
+
createChain([
|
|
1903
|
+
{
|
|
1904
|
+
resourceId: "sys-1",
|
|
1905
|
+
teamId: "team-1",
|
|
1906
|
+
canRead: true,
|
|
1907
|
+
canManage: false,
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
resourceId: "sys-2",
|
|
1911
|
+
teamId: "team-1",
|
|
1912
|
+
canRead: true,
|
|
1913
|
+
canManage: false,
|
|
1914
|
+
},
|
|
1915
|
+
])
|
|
1916
|
+
),
|
|
1917
|
+
}));
|
|
1918
|
+
|
|
1919
|
+
// Settings query - only sys-1 is teamOnly
|
|
1920
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1921
|
+
from: mock(() =>
|
|
1922
|
+
createChain([{ resourceId: "sys-1", teamOnly: true }])
|
|
1923
|
+
),
|
|
1924
|
+
}));
|
|
1925
|
+
|
|
1926
|
+
// User is member of team-2 (NOT team-1)
|
|
1927
|
+
(mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
|
|
1928
|
+
from: mock(() => createChain([{ teamId: "team-2" }])),
|
|
1929
|
+
}));
|
|
1930
|
+
|
|
1931
|
+
const router = createAuthRouter(
|
|
1932
|
+
mockDb,
|
|
1933
|
+
mockRegistry,
|
|
1934
|
+
async () => {},
|
|
1935
|
+
mockConfigService,
|
|
1936
|
+
mockAccessRuleRegistry
|
|
1937
|
+
);
|
|
1938
|
+
|
|
1939
|
+
const context = createMockRpcContext({ user: mockServiceUser });
|
|
1940
|
+
const result = await call(
|
|
1941
|
+
router.getAccessibleResourceIds,
|
|
1942
|
+
{
|
|
1943
|
+
userId: "user-1",
|
|
1944
|
+
userType: "user",
|
|
1945
|
+
resourceType: "catalog.system",
|
|
1946
|
+
resourceIds: ["sys-1", "sys-2"],
|
|
1947
|
+
action: "read",
|
|
1948
|
+
hasGlobalAccess: true,
|
|
1949
|
+
},
|
|
1950
|
+
{ context }
|
|
1951
|
+
);
|
|
1952
|
+
|
|
1953
|
+
// sys-1: teamOnly=true, user not in team-1, denied
|
|
1954
|
+
// sys-2: teamOnly=false, user has global access, granted
|
|
1955
|
+
expect(result).not.toContain("sys-1");
|
|
1956
|
+
expect(result).toContain("sys-2");
|
|
1202
1957
|
});
|
|
1203
1958
|
});
|
|
1204
1959
|
|
|
@@ -1211,7 +1966,7 @@ describe("Teams and Resource Access Control", () => {
|
|
|
1211
1966
|
mockRegistry,
|
|
1212
1967
|
async () => {},
|
|
1213
1968
|
mockConfigService,
|
|
1214
|
-
|
|
1969
|
+
mockAccessRuleRegistry
|
|
1215
1970
|
);
|
|
1216
1971
|
|
|
1217
1972
|
const context = createMockRpcContext({ user: mockServiceUser });
|