@checkstack/auth-backend 0.0.3 → 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 +174 -0
- package/drizzle/0002_lowly_squirrel_girl.sql +43 -0
- package/drizzle/0003_tranquil_sally_floyd.sql +8 -0
- package/drizzle/0004_lucky_power_man.sql +21 -0
- package/drizzle/meta/0002_snapshot.json +1017 -0
- package/drizzle/meta/0003_snapshot.json +1050 -0
- package/drizzle/meta/0004_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +1 -1
- package/src/index.ts +176 -162
- package/src/router.test.ts +11 -11
- package/src/router.ts +525 -90
- package/src/schema.ts +125 -18
- package/src/teams.test.ts +1985 -0
- package/src/utils/user.test.ts +65 -46
- package/src/utils/user.ts +21 -13
package/src/router.ts
CHANGED
|
@@ -34,8 +34,8 @@ import {
|
|
|
34
34
|
/**
|
|
35
35
|
* Creates the auth router using contract-based implementation.
|
|
36
36
|
*
|
|
37
|
-
* Auth and
|
|
38
|
-
* based on the contract's meta.userType and meta.
|
|
37
|
+
* Auth and access rules are automatically enforced via autoAuthMiddleware
|
|
38
|
+
* based on the contract's meta.userType and meta.access.
|
|
39
39
|
*/
|
|
40
40
|
const os = implement(authContract)
|
|
41
41
|
.$context<RpcContext>()
|
|
@@ -117,12 +117,12 @@ export const createAuthRouter = (
|
|
|
117
117
|
strategyRegistry: { getStrategies: () => AuthStrategy<unknown>[] },
|
|
118
118
|
reloadAuthFn: () => Promise<void>,
|
|
119
119
|
configService: ConfigService,
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
accessRuleRegistry: {
|
|
121
|
+
getAccessRules: () => {
|
|
122
122
|
id: string;
|
|
123
123
|
description?: string;
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
isDefault?: boolean;
|
|
125
|
+
isPublic?: boolean;
|
|
126
126
|
}[];
|
|
127
127
|
}
|
|
128
128
|
) => {
|
|
@@ -155,12 +155,12 @@ export const createAuthRouter = (
|
|
|
155
155
|
return enabledStrategies.filter((s) => s.enabled);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
const
|
|
158
|
+
const accessRulesHandler = os.accessRules.handler(async ({ context }) => {
|
|
159
159
|
const user = context.user;
|
|
160
160
|
if (!isRealUser(user)) {
|
|
161
|
-
return {
|
|
161
|
+
return { accessRules: [] };
|
|
162
162
|
}
|
|
163
|
-
return {
|
|
163
|
+
return { accessRules: user.accessRules || [] };
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
const getUsers = os.getUsers.handler(async () => {
|
|
@@ -214,42 +214,42 @@ export const createAuthRouter = (
|
|
|
214
214
|
|
|
215
215
|
const getRoles = os.getRoles.handler(async () => {
|
|
216
216
|
const roles = await internalDb.select().from(schema.role);
|
|
217
|
-
const
|
|
217
|
+
const roleAccessRules = await internalDb
|
|
218
218
|
.select()
|
|
219
|
-
.from(schema.
|
|
219
|
+
.from(schema.roleAccessRule);
|
|
220
220
|
|
|
221
221
|
return roles.map((role) => ({
|
|
222
222
|
id: role.id,
|
|
223
223
|
name: role.name,
|
|
224
224
|
description: role.description,
|
|
225
|
-
|
|
225
|
+
accessRules: roleAccessRules
|
|
226
226
|
.filter((rp) => rp.roleId === role.id)
|
|
227
|
-
.map((rp) => rp.
|
|
227
|
+
.map((rp) => rp.accessRuleId),
|
|
228
228
|
isSystem: role.isSystem || false,
|
|
229
229
|
// Anonymous role cannot be assigned to users - it's for unauthenticated access
|
|
230
230
|
isAssignable: role.id !== "anonymous",
|
|
231
231
|
}));
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
const
|
|
235
|
-
// Return only currently active
|
|
236
|
-
return
|
|
234
|
+
const getAccessRules = os.getAccessRules.handler(async () => {
|
|
235
|
+
// Return only currently active access rules (registered by loaded plugins)
|
|
236
|
+
return accessRuleRegistry.getAccessRules();
|
|
237
237
|
});
|
|
238
238
|
|
|
239
239
|
const createRole = os.createRole.handler(async ({ input }) => {
|
|
240
|
-
const { name, description,
|
|
240
|
+
const { name, description, accessRules: inputAccessRules } = input;
|
|
241
241
|
|
|
242
242
|
// Generate UUID for new role
|
|
243
243
|
const id = crypto.randomUUID();
|
|
244
244
|
|
|
245
|
-
// Get active
|
|
246
|
-
const
|
|
247
|
-
|
|
245
|
+
// Get active access rules to filter input
|
|
246
|
+
const activeAccessRules = new Set(
|
|
247
|
+
accessRuleRegistry.getAccessRules().map((p) => p.id)
|
|
248
248
|
);
|
|
249
249
|
|
|
250
|
-
// Filter to only include active
|
|
251
|
-
const
|
|
252
|
-
|
|
250
|
+
// Filter to only include active access rules
|
|
251
|
+
const validAccessRules = inputAccessRules.filter((p) =>
|
|
252
|
+
activeAccessRules.has(p)
|
|
253
253
|
);
|
|
254
254
|
|
|
255
255
|
await internalDb.transaction(async (tx) => {
|
|
@@ -261,12 +261,12 @@ export const createAuthRouter = (
|
|
|
261
261
|
isSystem: false,
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
// Create role-
|
|
265
|
-
if (
|
|
266
|
-
await tx.insert(schema.
|
|
267
|
-
|
|
264
|
+
// Create role-access rule mappings
|
|
265
|
+
if (validAccessRules.length > 0) {
|
|
266
|
+
await tx.insert(schema.roleAccessRule).values(
|
|
267
|
+
validAccessRules.map((accessRuleId) => ({
|
|
268
268
|
roleId: id,
|
|
269
|
-
|
|
269
|
+
accessRuleId,
|
|
270
270
|
}))
|
|
271
271
|
);
|
|
272
272
|
}
|
|
@@ -274,9 +274,9 @@ export const createAuthRouter = (
|
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
const updateRole = os.updateRole.handler(async ({ input, context }) => {
|
|
277
|
-
const { id, name, description,
|
|
277
|
+
const { id, name, description, accessRules: inputAccessRules } = input;
|
|
278
278
|
|
|
279
|
-
// Track if user has this role (for
|
|
279
|
+
// Track if user has this role (for access elevation prevention)
|
|
280
280
|
const userRoles = isRealUser(context.user) ? context.user.roles || [] : [];
|
|
281
281
|
const isUserOwnRole = userRoles.includes(id);
|
|
282
282
|
|
|
@@ -296,87 +296,87 @@ export const createAuthRouter = (
|
|
|
296
296
|
const isAdminRole = id === "admin";
|
|
297
297
|
|
|
298
298
|
// System roles can have name/description edited, but not deleted
|
|
299
|
-
// Admin role:
|
|
300
|
-
// Users role:
|
|
301
|
-
// User's own role:
|
|
299
|
+
// Admin role: access rules cannot be changed (wildcard access)
|
|
300
|
+
// Users role: access rules can be changed with default tracking
|
|
301
|
+
// User's own role: access rules cannot be changed (prevent access elevation)
|
|
302
302
|
|
|
303
|
-
// Get active
|
|
304
|
-
const
|
|
305
|
-
|
|
303
|
+
// Get active access rules to filter input
|
|
304
|
+
const activeAccessRules = new Set(
|
|
305
|
+
accessRuleRegistry.getAccessRules().map((p) => p.id)
|
|
306
306
|
);
|
|
307
307
|
|
|
308
|
-
// Filter to only include active
|
|
309
|
-
const
|
|
310
|
-
|
|
308
|
+
// Filter to only include active access rules
|
|
309
|
+
const validAccessRules = inputAccessRules.filter((p) =>
|
|
310
|
+
activeAccessRules.has(p)
|
|
311
311
|
);
|
|
312
312
|
|
|
313
|
-
// Track disabled authenticated default
|
|
313
|
+
// Track disabled authenticated default access rules for "users" role
|
|
314
314
|
if (isUsersRole && !isUserOwnRole) {
|
|
315
|
-
const allPerms =
|
|
315
|
+
const allPerms = accessRuleRegistry.getAccessRules();
|
|
316
316
|
const defaultPermIds = allPerms
|
|
317
|
-
.filter((p) => p.
|
|
317
|
+
.filter((p) => p.isDefault)
|
|
318
318
|
.map((p) => p.id);
|
|
319
319
|
|
|
320
|
-
// Find authenticated default
|
|
320
|
+
// Find authenticated default access rules that are being removed
|
|
321
321
|
const removedDefaults = defaultPermIds.filter(
|
|
322
|
-
(defId) => !
|
|
322
|
+
(defId) => !validAccessRules.includes(defId)
|
|
323
323
|
);
|
|
324
324
|
|
|
325
|
-
// Insert into
|
|
325
|
+
// Insert into disabled_default_access_rule table
|
|
326
326
|
for (const permId of removedDefaults) {
|
|
327
327
|
await internalDb
|
|
328
|
-
.insert(schema.
|
|
328
|
+
.insert(schema.disabledDefaultAccessRule)
|
|
329
329
|
.values({
|
|
330
|
-
|
|
330
|
+
accessRuleId: permId,
|
|
331
331
|
disabledAt: new Date(),
|
|
332
332
|
})
|
|
333
333
|
.onConflictDoNothing();
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// Remove from disabled table if being re-added
|
|
337
|
-
const readdedDefaults =
|
|
337
|
+
const readdedDefaults = validAccessRules.filter((p) =>
|
|
338
338
|
defaultPermIds.includes(p)
|
|
339
339
|
);
|
|
340
340
|
for (const permId of readdedDefaults) {
|
|
341
341
|
await internalDb
|
|
342
|
-
.delete(schema.
|
|
343
|
-
.where(eq(schema.
|
|
342
|
+
.delete(schema.disabledDefaultAccessRule)
|
|
343
|
+
.where(eq(schema.disabledDefaultAccessRule.accessRuleId, permId));
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// Track disabled public default
|
|
347
|
+
// Track disabled public default access rules for "anonymous" role
|
|
348
348
|
const isAnonymousRole = id === "anonymous";
|
|
349
349
|
if (isAnonymousRole) {
|
|
350
|
-
const allPerms =
|
|
350
|
+
const allPerms = accessRuleRegistry.getAccessRules();
|
|
351
351
|
const publicDefaultPermIds = allPerms
|
|
352
|
-
.filter((p) => p.
|
|
352
|
+
.filter((p) => p.isPublic)
|
|
353
353
|
.map((p) => p.id);
|
|
354
354
|
|
|
355
|
-
// Find public default
|
|
355
|
+
// Find public default access rules that are being removed
|
|
356
356
|
const removedPublicDefaults = publicDefaultPermIds.filter(
|
|
357
|
-
(defId) => !
|
|
357
|
+
(defId) => !validAccessRules.includes(defId)
|
|
358
358
|
);
|
|
359
359
|
|
|
360
|
-
// Insert into
|
|
360
|
+
// Insert into disabled_public_default_access_rule table
|
|
361
361
|
for (const permId of removedPublicDefaults) {
|
|
362
362
|
await internalDb
|
|
363
|
-
.insert(schema.
|
|
363
|
+
.insert(schema.disabledPublicDefaultAccessRule)
|
|
364
364
|
.values({
|
|
365
|
-
|
|
365
|
+
accessRuleId: permId,
|
|
366
366
|
disabledAt: new Date(),
|
|
367
367
|
})
|
|
368
368
|
.onConflictDoNothing();
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
// Remove from disabled table if being re-added
|
|
372
|
-
const readdedPublicDefaults =
|
|
372
|
+
const readdedPublicDefaults = validAccessRules.filter((p) =>
|
|
373
373
|
publicDefaultPermIds.includes(p)
|
|
374
374
|
);
|
|
375
375
|
for (const permId of readdedPublicDefaults) {
|
|
376
376
|
await internalDb
|
|
377
|
-
.delete(schema.
|
|
377
|
+
.delete(schema.disabledPublicDefaultAccessRule)
|
|
378
378
|
.where(
|
|
379
|
-
eq(schema.
|
|
379
|
+
eq(schema.disabledPublicDefaultAccessRule.accessRuleId, permId)
|
|
380
380
|
);
|
|
381
381
|
}
|
|
382
382
|
}
|
|
@@ -391,21 +391,21 @@ export const createAuthRouter = (
|
|
|
391
391
|
await tx.update(schema.role).set(updates).where(eq(schema.role.id, id));
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
// Skip
|
|
394
|
+
// Skip access rule changes for admin role (wildcard) or user's own role (prevent access elevation)
|
|
395
395
|
if (isAdminRole || isUserOwnRole) {
|
|
396
|
-
return; // Don't modify
|
|
396
|
+
return; // Don't modify access rules
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
// Replace
|
|
399
|
+
// Replace access rule mappings for non-admin roles
|
|
400
400
|
await tx
|
|
401
|
-
.delete(schema.
|
|
402
|
-
.where(eq(schema.
|
|
401
|
+
.delete(schema.roleAccessRule)
|
|
402
|
+
.where(eq(schema.roleAccessRule.roleId, id));
|
|
403
403
|
|
|
404
|
-
if (
|
|
405
|
-
await tx.insert(schema.
|
|
406
|
-
|
|
404
|
+
if (validAccessRules.length > 0) {
|
|
405
|
+
await tx.insert(schema.roleAccessRule).values(
|
|
406
|
+
validAccessRules.map((accessRuleId) => ({
|
|
407
407
|
roleId: id,
|
|
408
|
-
|
|
408
|
+
accessRuleId,
|
|
409
409
|
}))
|
|
410
410
|
);
|
|
411
411
|
}
|
|
@@ -441,10 +441,10 @@ export const createAuthRouter = (
|
|
|
441
441
|
|
|
442
442
|
// Delete role and related records in transaction
|
|
443
443
|
await internalDb.transaction(async (tx) => {
|
|
444
|
-
// Delete role-
|
|
444
|
+
// Delete role-access-rule mappings
|
|
445
445
|
await tx
|
|
446
|
-
.delete(schema.
|
|
447
|
-
.where(eq(schema.
|
|
446
|
+
.delete(schema.roleAccessRule)
|
|
447
|
+
.where(eq(schema.roleAccessRule.roleId, id));
|
|
448
448
|
|
|
449
449
|
// Delete user-role mappings
|
|
450
450
|
await tx.delete(schema.userRole).where(eq(schema.userRole.roleId, id));
|
|
@@ -582,40 +582,40 @@ export const createAuthRouter = (
|
|
|
582
582
|
}
|
|
583
583
|
);
|
|
584
584
|
|
|
585
|
-
const
|
|
585
|
+
const getAnonymousAccessRules = os.getAnonymousAccessRules.handler(
|
|
586
586
|
async () => {
|
|
587
587
|
const rolePerms = await internalDb
|
|
588
588
|
.select()
|
|
589
|
-
.from(schema.
|
|
590
|
-
.where(eq(schema.
|
|
591
|
-
return rolePerms.map((rp) => rp.
|
|
589
|
+
.from(schema.roleAccessRule)
|
|
590
|
+
.where(eq(schema.roleAccessRule.roleId, "anonymous"));
|
|
591
|
+
return rolePerms.map((rp) => rp.accessRuleId);
|
|
592
592
|
}
|
|
593
593
|
);
|
|
594
594
|
|
|
595
|
-
const
|
|
595
|
+
const filterUsersByAccessRule = os.filterUsersByAccessRule.handler(
|
|
596
596
|
async ({ input }) => {
|
|
597
|
-
const { userIds,
|
|
597
|
+
const { userIds, accessRule } = input;
|
|
598
598
|
|
|
599
599
|
if (userIds.length === 0) return [];
|
|
600
600
|
|
|
601
|
-
// Single efficient query: join user_role with
|
|
602
|
-
// and filter by both userIds AND the specific
|
|
603
|
-
const
|
|
601
|
+
// Single efficient query: join user_role with role_access_rule
|
|
602
|
+
// and filter by both userIds AND the specific access rule
|
|
603
|
+
const usersWithAccess = await internalDb
|
|
604
604
|
.select({ userId: schema.userRole.userId })
|
|
605
605
|
.from(schema.userRole)
|
|
606
606
|
.innerJoin(
|
|
607
|
-
schema.
|
|
608
|
-
eq(schema.userRole.roleId, schema.
|
|
607
|
+
schema.roleAccessRule,
|
|
608
|
+
eq(schema.userRole.roleId, schema.roleAccessRule.roleId)
|
|
609
609
|
)
|
|
610
610
|
.where(
|
|
611
611
|
and(
|
|
612
612
|
inArray(schema.userRole.userId, userIds),
|
|
613
|
-
eq(schema.
|
|
613
|
+
eq(schema.roleAccessRule.accessRuleId, accessRule)
|
|
614
614
|
)
|
|
615
615
|
)
|
|
616
616
|
.groupBy(schema.userRole.userId);
|
|
617
617
|
|
|
618
|
-
return
|
|
618
|
+
return usersWithAccess.map((row) => row.userId);
|
|
619
619
|
}
|
|
620
620
|
);
|
|
621
621
|
|
|
@@ -1016,13 +1016,430 @@ export const createAuthRouter = (
|
|
|
1016
1016
|
}
|
|
1017
1017
|
);
|
|
1018
1018
|
|
|
1019
|
+
// ==========================================================================
|
|
1020
|
+
// TEAM MANAGEMENT HANDLERS
|
|
1021
|
+
// ==========================================================================
|
|
1022
|
+
|
|
1023
|
+
const getTeams = os.getTeams.handler(async ({ context }) => {
|
|
1024
|
+
const teams = await internalDb.select().from(schema.team);
|
|
1025
|
+
const memberCounts = await internalDb
|
|
1026
|
+
.select({ teamId: schema.userTeam.teamId })
|
|
1027
|
+
.from(schema.userTeam);
|
|
1028
|
+
|
|
1029
|
+
const userId = isRealUser(context.user) ? context.user.id : undefined;
|
|
1030
|
+
const managerRows = userId
|
|
1031
|
+
? await internalDb
|
|
1032
|
+
.select()
|
|
1033
|
+
.from(schema.teamManager)
|
|
1034
|
+
.where(eq(schema.teamManager.userId, userId))
|
|
1035
|
+
: [];
|
|
1036
|
+
const managedTeamIds = new Set(managerRows.map((m) => m.teamId));
|
|
1037
|
+
|
|
1038
|
+
return teams.map((t) => ({
|
|
1039
|
+
id: t.id,
|
|
1040
|
+
name: t.name,
|
|
1041
|
+
description: t.description,
|
|
1042
|
+
memberCount: memberCounts.filter((m) => m.teamId === t.id).length,
|
|
1043
|
+
isManager: managedTeamIds.has(t.id),
|
|
1044
|
+
}));
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const getTeam = os.getTeam.handler(async ({ input }) => {
|
|
1048
|
+
const teams = await internalDb
|
|
1049
|
+
.select()
|
|
1050
|
+
.from(schema.team)
|
|
1051
|
+
.where(eq(schema.team.id, input.teamId))
|
|
1052
|
+
.limit(1);
|
|
1053
|
+
if (teams.length === 0) return;
|
|
1054
|
+
|
|
1055
|
+
const team = teams[0];
|
|
1056
|
+
const memberRows = await internalDb
|
|
1057
|
+
.select({ userId: schema.userTeam.userId })
|
|
1058
|
+
.from(schema.userTeam)
|
|
1059
|
+
.where(eq(schema.userTeam.teamId, team.id));
|
|
1060
|
+
const managerRows = await internalDb
|
|
1061
|
+
.select({ userId: schema.teamManager.userId })
|
|
1062
|
+
.from(schema.teamManager)
|
|
1063
|
+
.where(eq(schema.teamManager.teamId, team.id));
|
|
1064
|
+
|
|
1065
|
+
const userIds = [
|
|
1066
|
+
...new Set([
|
|
1067
|
+
...memberRows.map((m) => m.userId),
|
|
1068
|
+
...managerRows.map((m) => m.userId),
|
|
1069
|
+
]),
|
|
1070
|
+
];
|
|
1071
|
+
const users =
|
|
1072
|
+
userIds.length > 0
|
|
1073
|
+
? await internalDb
|
|
1074
|
+
.select({
|
|
1075
|
+
id: schema.user.id,
|
|
1076
|
+
name: schema.user.name,
|
|
1077
|
+
email: schema.user.email,
|
|
1078
|
+
})
|
|
1079
|
+
.from(schema.user)
|
|
1080
|
+
.where(inArray(schema.user.id, userIds))
|
|
1081
|
+
: [];
|
|
1082
|
+
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
1083
|
+
|
|
1084
|
+
return {
|
|
1085
|
+
id: team.id,
|
|
1086
|
+
name: team.name,
|
|
1087
|
+
description: team.description,
|
|
1088
|
+
members: memberRows
|
|
1089
|
+
.map((m) => userMap.get(m.userId))
|
|
1090
|
+
.filter((u): u is NonNullable<typeof u> => u !== undefined),
|
|
1091
|
+
managers: managerRows
|
|
1092
|
+
.map((m) => userMap.get(m.userId))
|
|
1093
|
+
.filter((u): u is NonNullable<typeof u> => u !== undefined),
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const createTeam = os.createTeam.handler(async ({ input, context }) => {
|
|
1098
|
+
const id = crypto.randomUUID();
|
|
1099
|
+
const now = new Date();
|
|
1100
|
+
await internalDb.insert(schema.team).values({
|
|
1101
|
+
id,
|
|
1102
|
+
name: input.name,
|
|
1103
|
+
description: input.description,
|
|
1104
|
+
createdAt: now,
|
|
1105
|
+
updatedAt: now,
|
|
1106
|
+
});
|
|
1107
|
+
context.logger.info(`[auth-backend] Created team: ${input.name}`);
|
|
1108
|
+
return { id };
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const updateTeam = os.updateTeam.handler(async ({ input, context }) => {
|
|
1112
|
+
const { id, name, description } = input;
|
|
1113
|
+
// TODO: Check if user is manager or has teamsManage access
|
|
1114
|
+
const updates: {
|
|
1115
|
+
name?: string;
|
|
1116
|
+
description?: string | null;
|
|
1117
|
+
updatedAt: Date;
|
|
1118
|
+
} = {
|
|
1119
|
+
updatedAt: new Date(),
|
|
1120
|
+
};
|
|
1121
|
+
if (name !== undefined) updates.name = name;
|
|
1122
|
+
if (description !== undefined) updates.description = description;
|
|
1123
|
+
await internalDb
|
|
1124
|
+
.update(schema.team)
|
|
1125
|
+
.set(updates)
|
|
1126
|
+
.where(eq(schema.team.id, id));
|
|
1127
|
+
context.logger.info(`[auth-backend] Updated team: ${id}`);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const deleteTeam = os.deleteTeam.handler(async ({ input: id, context }) => {
|
|
1131
|
+
await internalDb.transaction(async (tx) => {
|
|
1132
|
+
await tx.delete(schema.userTeam).where(eq(schema.userTeam.teamId, id));
|
|
1133
|
+
await tx
|
|
1134
|
+
.delete(schema.teamManager)
|
|
1135
|
+
.where(eq(schema.teamManager.teamId, id));
|
|
1136
|
+
await tx
|
|
1137
|
+
.delete(schema.applicationTeam)
|
|
1138
|
+
.where(eq(schema.applicationTeam.teamId, id));
|
|
1139
|
+
await tx
|
|
1140
|
+
.delete(schema.resourceTeamAccess)
|
|
1141
|
+
.where(eq(schema.resourceTeamAccess.teamId, id));
|
|
1142
|
+
await tx.delete(schema.team).where(eq(schema.team.id, id));
|
|
1143
|
+
});
|
|
1144
|
+
context.logger.info(`[auth-backend] Deleted team: ${id}`);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const addUserToTeam = os.addUserToTeam.handler(async ({ input }) => {
|
|
1148
|
+
await internalDb
|
|
1149
|
+
.insert(schema.userTeam)
|
|
1150
|
+
.values({ userId: input.userId, teamId: input.teamId })
|
|
1151
|
+
.onConflictDoNothing();
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
const removeUserFromTeam = os.removeUserFromTeam.handler(
|
|
1155
|
+
async ({ input }) => {
|
|
1156
|
+
await internalDb
|
|
1157
|
+
.delete(schema.userTeam)
|
|
1158
|
+
.where(
|
|
1159
|
+
and(
|
|
1160
|
+
eq(schema.userTeam.userId, input.userId),
|
|
1161
|
+
eq(schema.userTeam.teamId, input.teamId)
|
|
1162
|
+
)
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
const addTeamManager = os.addTeamManager.handler(async ({ input }) => {
|
|
1168
|
+
await internalDb
|
|
1169
|
+
.insert(schema.teamManager)
|
|
1170
|
+
.values({ userId: input.userId, teamId: input.teamId })
|
|
1171
|
+
.onConflictDoNothing();
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
const removeTeamManager = os.removeTeamManager.handler(async ({ input }) => {
|
|
1175
|
+
await internalDb
|
|
1176
|
+
.delete(schema.teamManager)
|
|
1177
|
+
.where(
|
|
1178
|
+
and(
|
|
1179
|
+
eq(schema.teamManager.userId, input.userId),
|
|
1180
|
+
eq(schema.teamManager.teamId, input.teamId)
|
|
1181
|
+
)
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
const getResourceTeamAccess = os.getResourceTeamAccess.handler(
|
|
1186
|
+
async ({ input }) => {
|
|
1187
|
+
const rows = await internalDb
|
|
1188
|
+
.select()
|
|
1189
|
+
.from(schema.resourceTeamAccess)
|
|
1190
|
+
.innerJoin(
|
|
1191
|
+
schema.team,
|
|
1192
|
+
eq(schema.resourceTeamAccess.teamId, schema.team.id)
|
|
1193
|
+
)
|
|
1194
|
+
.where(
|
|
1195
|
+
and(
|
|
1196
|
+
eq(schema.resourceTeamAccess.resourceType, input.resourceType),
|
|
1197
|
+
eq(schema.resourceTeamAccess.resourceId, input.resourceId)
|
|
1198
|
+
)
|
|
1199
|
+
);
|
|
1200
|
+
return rows.map((r) => ({
|
|
1201
|
+
teamId: r.resource_team_access.teamId,
|
|
1202
|
+
teamName: r.team.name,
|
|
1203
|
+
canRead: r.resource_team_access.canRead,
|
|
1204
|
+
canManage: r.resource_team_access.canManage,
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
const setResourceTeamAccess = os.setResourceTeamAccess.handler(
|
|
1210
|
+
async ({ input }) => {
|
|
1211
|
+
const { resourceType, resourceId, teamId, canRead, canManage } = input;
|
|
1212
|
+
await internalDb
|
|
1213
|
+
.insert(schema.resourceTeamAccess)
|
|
1214
|
+
.values({
|
|
1215
|
+
resourceType,
|
|
1216
|
+
resourceId,
|
|
1217
|
+
teamId,
|
|
1218
|
+
canRead: canRead ?? true,
|
|
1219
|
+
canManage: canManage ?? false,
|
|
1220
|
+
})
|
|
1221
|
+
.onConflictDoUpdate({
|
|
1222
|
+
target: [
|
|
1223
|
+
schema.resourceTeamAccess.resourceType,
|
|
1224
|
+
schema.resourceTeamAccess.resourceId,
|
|
1225
|
+
schema.resourceTeamAccess.teamId,
|
|
1226
|
+
],
|
|
1227
|
+
set: {
|
|
1228
|
+
canRead: canRead ?? true,
|
|
1229
|
+
canManage: canManage ?? false,
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
const removeResourceTeamAccess = os.removeResourceTeamAccess.handler(
|
|
1236
|
+
async ({ input }) => {
|
|
1237
|
+
await internalDb
|
|
1238
|
+
.delete(schema.resourceTeamAccess)
|
|
1239
|
+
.where(
|
|
1240
|
+
and(
|
|
1241
|
+
eq(schema.resourceTeamAccess.resourceType, input.resourceType),
|
|
1242
|
+
eq(schema.resourceTeamAccess.resourceId, input.resourceId),
|
|
1243
|
+
eq(schema.resourceTeamAccess.teamId, input.teamId)
|
|
1244
|
+
)
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
// Resource-level access settings
|
|
1250
|
+
const getResourceAccessSettings = os.getResourceAccessSettings.handler(
|
|
1251
|
+
async ({ input }) => {
|
|
1252
|
+
const rows = await internalDb
|
|
1253
|
+
.select()
|
|
1254
|
+
.from(schema.resourceAccessSettings)
|
|
1255
|
+
.where(
|
|
1256
|
+
and(
|
|
1257
|
+
eq(schema.resourceAccessSettings.resourceType, input.resourceType),
|
|
1258
|
+
eq(schema.resourceAccessSettings.resourceId, input.resourceId)
|
|
1259
|
+
)
|
|
1260
|
+
)
|
|
1261
|
+
.limit(1);
|
|
1262
|
+
return { teamOnly: rows[0]?.teamOnly ?? false };
|
|
1263
|
+
}
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
const setResourceAccessSettings = os.setResourceAccessSettings.handler(
|
|
1267
|
+
async ({ input }) => {
|
|
1268
|
+
const { resourceType, resourceId, teamOnly } = input;
|
|
1269
|
+
await internalDb
|
|
1270
|
+
.insert(schema.resourceAccessSettings)
|
|
1271
|
+
.values({ resourceType, resourceId, teamOnly })
|
|
1272
|
+
.onConflictDoUpdate({
|
|
1273
|
+
target: [
|
|
1274
|
+
schema.resourceAccessSettings.resourceType,
|
|
1275
|
+
schema.resourceAccessSettings.resourceId,
|
|
1276
|
+
],
|
|
1277
|
+
set: { teamOnly },
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// S2S Endpoints for middleware
|
|
1283
|
+
const checkResourceTeamAccess = os.checkResourceTeamAccess.handler(
|
|
1284
|
+
async ({ input }) => {
|
|
1285
|
+
const {
|
|
1286
|
+
userId,
|
|
1287
|
+
userType,
|
|
1288
|
+
resourceType,
|
|
1289
|
+
resourceId,
|
|
1290
|
+
action,
|
|
1291
|
+
hasGlobalAccess,
|
|
1292
|
+
} = input;
|
|
1293
|
+
|
|
1294
|
+
const grants = await internalDb
|
|
1295
|
+
.select()
|
|
1296
|
+
.from(schema.resourceTeamAccess)
|
|
1297
|
+
.where(
|
|
1298
|
+
and(
|
|
1299
|
+
eq(schema.resourceTeamAccess.resourceType, resourceType),
|
|
1300
|
+
eq(schema.resourceTeamAccess.resourceId, resourceId)
|
|
1301
|
+
)
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
// No grants = global access applies
|
|
1305
|
+
if (grants.length === 0) return { hasAccess: hasGlobalAccess };
|
|
1306
|
+
|
|
1307
|
+
// Check resource-level settings for teamOnly
|
|
1308
|
+
const settingsRows = await internalDb
|
|
1309
|
+
.select()
|
|
1310
|
+
.from(schema.resourceAccessSettings)
|
|
1311
|
+
.where(
|
|
1312
|
+
and(
|
|
1313
|
+
eq(schema.resourceAccessSettings.resourceType, resourceType),
|
|
1314
|
+
eq(schema.resourceAccessSettings.resourceId, resourceId)
|
|
1315
|
+
)
|
|
1316
|
+
)
|
|
1317
|
+
.limit(1);
|
|
1318
|
+
const isTeamOnly = settingsRows[0]?.teamOnly ?? false;
|
|
1319
|
+
|
|
1320
|
+
if (!isTeamOnly && hasGlobalAccess) return { hasAccess: true };
|
|
1321
|
+
|
|
1322
|
+
// Get user's teams
|
|
1323
|
+
const teamTable =
|
|
1324
|
+
userType === "user" ? schema.userTeam : schema.applicationTeam;
|
|
1325
|
+
const userIdCol =
|
|
1326
|
+
userType === "user"
|
|
1327
|
+
? schema.userTeam.userId
|
|
1328
|
+
: schema.applicationTeam.applicationId;
|
|
1329
|
+
const userTeams = await internalDb
|
|
1330
|
+
.select({
|
|
1331
|
+
teamId:
|
|
1332
|
+
userType === "user"
|
|
1333
|
+
? schema.userTeam.teamId
|
|
1334
|
+
: schema.applicationTeam.teamId,
|
|
1335
|
+
})
|
|
1336
|
+
.from(teamTable)
|
|
1337
|
+
.where(eq(userIdCol, userId));
|
|
1338
|
+
const userTeamIds = new Set(userTeams.map((t) => t.teamId));
|
|
1339
|
+
|
|
1340
|
+
const field = action === "manage" ? "canManage" : "canRead";
|
|
1341
|
+
const hasAccess = grants.some(
|
|
1342
|
+
(g) => userTeamIds.has(g.teamId) && g[field]
|
|
1343
|
+
);
|
|
1344
|
+
return { hasAccess };
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1348
|
+
const getAccessibleResourceIds = os.getAccessibleResourceIds.handler(
|
|
1349
|
+
async ({ input }) => {
|
|
1350
|
+
const {
|
|
1351
|
+
userId,
|
|
1352
|
+
userType,
|
|
1353
|
+
resourceType,
|
|
1354
|
+
resourceIds,
|
|
1355
|
+
action,
|
|
1356
|
+
hasGlobalAccess,
|
|
1357
|
+
} = input;
|
|
1358
|
+
if (resourceIds.length === 0) return [];
|
|
1359
|
+
|
|
1360
|
+
// Get all grants for these resources
|
|
1361
|
+
const grants = await internalDb
|
|
1362
|
+
.select()
|
|
1363
|
+
.from(schema.resourceTeamAccess)
|
|
1364
|
+
.where(
|
|
1365
|
+
and(
|
|
1366
|
+
eq(schema.resourceTeamAccess.resourceType, resourceType),
|
|
1367
|
+
inArray(schema.resourceTeamAccess.resourceId, resourceIds)
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
|
|
1371
|
+
// Get resource-level settings for teamOnly
|
|
1372
|
+
const settingsRows = await internalDb
|
|
1373
|
+
.select()
|
|
1374
|
+
.from(schema.resourceAccessSettings)
|
|
1375
|
+
.where(
|
|
1376
|
+
and(
|
|
1377
|
+
eq(schema.resourceAccessSettings.resourceType, resourceType),
|
|
1378
|
+
inArray(schema.resourceAccessSettings.resourceId, resourceIds)
|
|
1379
|
+
)
|
|
1380
|
+
);
|
|
1381
|
+
const teamOnlyByResource = new Map(
|
|
1382
|
+
settingsRows.map((s) => [s.resourceId, s.teamOnly])
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
// Get user's teams
|
|
1386
|
+
const teamTable =
|
|
1387
|
+
userType === "user" ? schema.userTeam : schema.applicationTeam;
|
|
1388
|
+
const userIdCol =
|
|
1389
|
+
userType === "user"
|
|
1390
|
+
? schema.userTeam.userId
|
|
1391
|
+
: schema.applicationTeam.applicationId;
|
|
1392
|
+
const userTeams = await internalDb
|
|
1393
|
+
.select({
|
|
1394
|
+
teamId:
|
|
1395
|
+
userType === "user"
|
|
1396
|
+
? schema.userTeam.teamId
|
|
1397
|
+
: schema.applicationTeam.teamId,
|
|
1398
|
+
})
|
|
1399
|
+
.from(teamTable)
|
|
1400
|
+
.where(eq(userIdCol, userId));
|
|
1401
|
+
const userTeamIds = new Set(userTeams.map((t) => t.teamId));
|
|
1402
|
+
|
|
1403
|
+
const field = action === "manage" ? "canManage" : "canRead";
|
|
1404
|
+
const grantsByResource = new Map<string, typeof grants>();
|
|
1405
|
+
for (const g of grants) {
|
|
1406
|
+
const existing = grantsByResource.get(g.resourceId) || [];
|
|
1407
|
+
existing.push(g);
|
|
1408
|
+
grantsByResource.set(g.resourceId, existing);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return resourceIds.filter((id) => {
|
|
1412
|
+
const resourceGrants = grantsByResource.get(id) || [];
|
|
1413
|
+
if (resourceGrants.length === 0) return hasGlobalAccess;
|
|
1414
|
+
const isTeamOnly = teamOnlyByResource.get(id) ?? false;
|
|
1415
|
+
if (!isTeamOnly && hasGlobalAccess) return true;
|
|
1416
|
+
return resourceGrants.some(
|
|
1417
|
+
(g) => userTeamIds.has(g.teamId) && g[field]
|
|
1418
|
+
);
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
const deleteResourceGrants = os.deleteResourceGrants.handler(
|
|
1424
|
+
async ({ input }) => {
|
|
1425
|
+
await internalDb
|
|
1426
|
+
.delete(schema.resourceTeamAccess)
|
|
1427
|
+
.where(
|
|
1428
|
+
and(
|
|
1429
|
+
eq(schema.resourceTeamAccess.resourceType, input.resourceType),
|
|
1430
|
+
eq(schema.resourceTeamAccess.resourceId, input.resourceId)
|
|
1431
|
+
)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1019
1436
|
return os.router({
|
|
1020
1437
|
getEnabledStrategies,
|
|
1021
|
-
|
|
1438
|
+
accessRules: accessRulesHandler,
|
|
1022
1439
|
getUsers,
|
|
1023
1440
|
deleteUser,
|
|
1024
1441
|
getRoles,
|
|
1025
|
-
|
|
1442
|
+
getAccessRules,
|
|
1026
1443
|
createRole,
|
|
1027
1444
|
updateRole,
|
|
1028
1445
|
deleteRole,
|
|
@@ -1033,9 +1450,9 @@ export const createAuthRouter = (
|
|
|
1033
1450
|
getRegistrationSchema,
|
|
1034
1451
|
getRegistrationStatus,
|
|
1035
1452
|
setRegistrationStatus,
|
|
1036
|
-
|
|
1453
|
+
getAnonymousAccessRules,
|
|
1037
1454
|
getUserById,
|
|
1038
|
-
|
|
1455
|
+
filterUsersByAccessRule,
|
|
1039
1456
|
findUserByEmail,
|
|
1040
1457
|
upsertExternalUser,
|
|
1041
1458
|
createSession,
|
|
@@ -1045,6 +1462,24 @@ export const createAuthRouter = (
|
|
|
1045
1462
|
updateApplication,
|
|
1046
1463
|
deleteApplication,
|
|
1047
1464
|
regenerateApplicationSecret,
|
|
1465
|
+
// Teams
|
|
1466
|
+
getTeams,
|
|
1467
|
+
getTeam,
|
|
1468
|
+
createTeam,
|
|
1469
|
+
updateTeam,
|
|
1470
|
+
deleteTeam,
|
|
1471
|
+
addUserToTeam,
|
|
1472
|
+
removeUserFromTeam,
|
|
1473
|
+
addTeamManager,
|
|
1474
|
+
removeTeamManager,
|
|
1475
|
+
getResourceTeamAccess,
|
|
1476
|
+
setResourceTeamAccess,
|
|
1477
|
+
removeResourceTeamAccess,
|
|
1478
|
+
getResourceAccessSettings,
|
|
1479
|
+
setResourceAccessSettings,
|
|
1480
|
+
checkResourceTeamAccess,
|
|
1481
|
+
getAccessibleResourceIds,
|
|
1482
|
+
deleteResourceGrants,
|
|
1048
1483
|
});
|
|
1049
1484
|
};
|
|
1050
1485
|
|