@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/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 permissions
23
+ // Mock user with admin accesss
24
24
  const mockAdminUser = {
25
25
  type: "user" as const,
26
26
  id: "admin-user",
27
- permissions: ["*"],
27
+ accessRules: ["*"],
28
28
  roles: ["admin"],
29
29
  teamIds: ["team-alpha"],
30
30
  };
31
31
 
32
- // Mock regular user with limited permissions
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
- permissions: ["auth.teams.read"],
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 mockPermissionRegistry = {
121
- getPermissions: () => [
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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 permissions", async () => {
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
- mockPermissionRegistry
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 permissions", async () => {
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
- mockPermissionRegistry
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
- mockPermissionRegistry
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 permission", async () => {
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
- mockPermissionRegistry
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
- hasGlobalPermission: true,
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 permission", async () => {
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
- mockPermissionRegistry
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
- hasGlobalPermission: false,
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
- mockPermissionRegistry
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
- hasGlobalPermission: false,
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 permission
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
- mockPermissionRegistry
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
- hasGlobalPermission: false,
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
- mockPermissionRegistry
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
- hasGlobalPermission: true, // Global permission doesn't help with teamOnly
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
- mockPermissionRegistry
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
- hasGlobalPermission: true, // Global permission doesn't help with teamOnly
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
- describe("getAccessibleResourceIds (S2S)", () => {
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
- mockPermissionRegistry
1209
+ mockAccessRuleRegistry
1079
1210
  );
1080
1211
 
1081
1212
  const context = createMockRpcContext({ user: mockServiceUser });
1082
1213
  const result = await call(
1083
- router.getAccessibleResourceIds,
1214
+ router.checkResourceTeamAccess,
1084
1215
  {
1085
1216
  userId: "user-1",
1086
1217
  userType: "user",
1087
1218
  resourceType: "catalog.system",
1088
- resourceIds: [],
1089
- action: "read",
1090
- hasGlobalPermission: true,
1219
+ resourceId: "sys-1",
1220
+ action: "manage",
1221
+ hasGlobalAccess: false,
1091
1222
  },
1092
1223
  { context }
1093
1224
  );
1094
1225
 
1095
- expect(result).toEqual([]);
1226
+ expect(result.hasAccess).toBe(true);
1096
1227
  });
1097
1228
 
1098
- it("returns all resources when no grants exist and user has global permission", async () => {
1229
+ it("allows access via global access when grants exist but resource is not teamOnly", async () => {
1099
1230
  const mockDb = createMockDb();
1100
1231
 
1101
- // No grants exist
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(() => createChain([])),
1234
+ from: mock(() =>
1235
+ createChain([
1236
+ {
1237
+ teamId: "team-1",
1238
+ canRead: true,
1239
+ canManage: false,
1240
+ },
1241
+ ])
1242
+ ),
1104
1243
  }));
1105
1244
 
1106
- // User teams (not used when no grants)
1245
+ // Settings query - returns empty (teamOnly = false by default)
1107
1246
  (mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
1108
- from: mock(() => createChain([{ teamId: "team-1" }])),
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
- mockPermissionRegistry
1255
+ mockAccessRuleRegistry
1117
1256
  );
1118
1257
 
1119
1258
  const context = createMockRpcContext({ user: mockServiceUser });
1120
1259
  const result = await call(
1121
- router.getAccessibleResourceIds,
1260
+ router.checkResourceTeamAccess,
1122
1261
  {
1123
1262
  userId: "user-1",
1124
1263
  userType: "user",
1125
1264
  resourceType: "catalog.system",
1126
- resourceIds: ["sys-1", "sys-2", "sys-3"],
1265
+ resourceId: "sys-1",
1127
1266
  action: "read",
1128
- hasGlobalPermission: true,
1267
+ hasGlobalAccess: true, // User has global access
1129
1268
  },
1130
1269
  { context }
1131
1270
  );
1132
1271
 
1133
- expect(result).toEqual(["sys-1", "sys-2", "sys-3"]);
1272
+ expect(result.hasAccess).toBe(true);
1134
1273
  });
1135
1274
 
1136
- it("filters resources based on team grants", async () => {
1275
+ it("denies access when user is not in any team and lacks global access", async () => {
1137
1276
  const mockDb = createMockDb();
1138
1277
 
1139
- // Grants exist for sys-1 (team-1) and sys-2 (team-2)
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 - both sys-1 and sys-2 are teamOnly
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
- { resourceId: "sys-1", teamOnly: true },
1164
- { resourceId: "sys-2", teamOnly: true },
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
- // User is member of team-1 only
1400
+ // Settings query - teamOnly = true
1170
1401
  (mockDb.select as ReturnType<typeof mock>).mockImplementationOnce(() => ({
1171
- from: mock(() => createChain([{ teamId: "team-1" }])),
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
- mockPermissionRegistry
1417
+ mockAccessRuleRegistry
1180
1418
  );
1181
1419
 
1182
1420
  const context = createMockRpcContext({ user: mockServiceUser });
1183
1421
  const result = await call(
1184
- router.getAccessibleResourceIds,
1422
+ router.checkResourceTeamAccess,
1185
1423
  {
1186
1424
  userId: "user-1",
1187
1425
  userType: "user",
1188
1426
  resourceType: "catalog.system",
1189
- resourceIds: ["sys-1", "sys-2", "sys-3"],
1190
- action: "read",
1191
- hasGlobalPermission: true,
1427
+ resourceId: "sys-1",
1428
+ action: "manage",
1429
+ hasGlobalAccess: false,
1192
1430
  },
1193
1431
  { context }
1194
1432
  );
1195
1433
 
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");
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
- mockPermissionRegistry
1969
+ mockAccessRuleRegistry
1215
1970
  );
1216
1971
 
1217
1972
  const context = createMockRpcContext({ user: mockServiceUser });