@checkstack/auth-backend 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/drizzle/0002_lowly_squirrel_girl.sql +43 -0
- package/drizzle/0003_tranquil_sally_floyd.sql +8 -0
- package/drizzle/meta/0002_snapshot.json +1017 -0
- package/drizzle/meta/0003_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/index.ts +10 -0
- package/src/router.ts +435 -0
- package/src/schema.ts +107 -0
- package/src/teams.test.ts +1230 -0
- package/src/utils/user.test.ts +60 -41
- package/src/utils/user.ts +9 -1
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
"when": 1767621359670,
|
|
16
16
|
"tag": "0001_certain_madame_hydra",
|
|
17
17
|
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1768325932660,
|
|
23
|
+
"tag": "0002_lowly_squirrel_girl",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1768332454744,
|
|
30
|
+
"tag": "0003_tranquil_sally_floyd",
|
|
31
|
+
"breakpoints": true
|
|
18
32
|
}
|
|
19
33
|
]
|
|
20
34
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -402,6 +402,15 @@ export default createBackendPlugin({
|
|
|
402
402
|
];
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
// Get team memberships for this application
|
|
406
|
+
const appTeams = await db
|
|
407
|
+
.select({ teamId: schema.applicationTeam.teamId })
|
|
408
|
+
.from(schema.applicationTeam)
|
|
409
|
+
.where(
|
|
410
|
+
eq(schema.applicationTeam.applicationId, applicationId)
|
|
411
|
+
);
|
|
412
|
+
const teamIds = appTeams.map((t) => t.teamId);
|
|
413
|
+
|
|
405
414
|
// Return ApplicationUser
|
|
406
415
|
return {
|
|
407
416
|
type: "application" as const,
|
|
@@ -409,6 +418,7 @@ export default createBackendPlugin({
|
|
|
409
418
|
name: app.name,
|
|
410
419
|
roles: roleIds,
|
|
411
420
|
permissions,
|
|
421
|
+
teamIds,
|
|
412
422
|
};
|
|
413
423
|
}
|
|
414
424
|
}
|
package/src/router.ts
CHANGED
|
@@ -1016,6 +1016,423 @@ 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 permission
|
|
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
|
+
hasGlobalPermission,
|
|
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 permission applies
|
|
1305
|
+
if (grants.length === 0) return { hasAccess: hasGlobalPermission };
|
|
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 && hasGlobalPermission) 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
|
+
hasGlobalPermission,
|
|
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 hasGlobalPermission;
|
|
1414
|
+
const isTeamOnly = teamOnlyByResource.get(id) ?? false;
|
|
1415
|
+
if (!isTeamOnly && hasGlobalPermission) 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
|
permissions,
|
|
@@ -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
|
|
package/src/schema.ts
CHANGED
|
@@ -171,3 +171,110 @@ export const applicationRole = pgTable(
|
|
|
171
171
|
pk: primaryKey({ columns: [t.applicationId, t.roleId] }),
|
|
172
172
|
})
|
|
173
173
|
);
|
|
174
|
+
|
|
175
|
+
// --- Teams Schema ---
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Teams for resource-level access control.
|
|
179
|
+
* Users can be members of multiple teams, and resources can be scoped to teams.
|
|
180
|
+
*/
|
|
181
|
+
export const team = pgTable("team", {
|
|
182
|
+
id: text("id").primaryKey(),
|
|
183
|
+
name: text("name").notNull(),
|
|
184
|
+
description: text("description"),
|
|
185
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
186
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* User-to-Team membership (M:N).
|
|
191
|
+
* Users can belong to multiple teams.
|
|
192
|
+
*/
|
|
193
|
+
export const userTeam = pgTable(
|
|
194
|
+
"user_team",
|
|
195
|
+
{
|
|
196
|
+
userId: text("user_id")
|
|
197
|
+
.notNull()
|
|
198
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
199
|
+
teamId: text("team_id")
|
|
200
|
+
.notNull()
|
|
201
|
+
.references(() => team.id, { onDelete: "cascade" }),
|
|
202
|
+
},
|
|
203
|
+
(t) => ({
|
|
204
|
+
pk: primaryKey({ columns: [t.userId, t.teamId] }),
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Application-to-Team membership (M:N).
|
|
210
|
+
* API keys can belong to teams for resource access.
|
|
211
|
+
*/
|
|
212
|
+
export const applicationTeam = pgTable(
|
|
213
|
+
"application_team",
|
|
214
|
+
{
|
|
215
|
+
applicationId: text("application_id")
|
|
216
|
+
.notNull()
|
|
217
|
+
.references(() => application.id, { onDelete: "cascade" }),
|
|
218
|
+
teamId: text("team_id")
|
|
219
|
+
.notNull()
|
|
220
|
+
.references(() => team.id, { onDelete: "cascade" }),
|
|
221
|
+
},
|
|
222
|
+
(t) => ({
|
|
223
|
+
pk: primaryKey({ columns: [t.applicationId, t.teamId] }),
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Team managers - users who can manage a specific team's membership and resource access.
|
|
229
|
+
* Team managers cannot delete the team or manage other teams.
|
|
230
|
+
*/
|
|
231
|
+
export const teamManager = pgTable(
|
|
232
|
+
"team_manager",
|
|
233
|
+
{
|
|
234
|
+
teamId: text("team_id")
|
|
235
|
+
.notNull()
|
|
236
|
+
.references(() => team.id, { onDelete: "cascade" }),
|
|
237
|
+
userId: text("user_id")
|
|
238
|
+
.notNull()
|
|
239
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
240
|
+
},
|
|
241
|
+
(t) => ({
|
|
242
|
+
pk: primaryKey({ columns: [t.teamId, t.userId] }),
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resource-level access settings.
|
|
248
|
+
* Controls whether a resource requires team membership (teamOnly) vs allowing global permissions.
|
|
249
|
+
*/
|
|
250
|
+
export const resourceAccessSettings = pgTable(
|
|
251
|
+
"resource_access_settings",
|
|
252
|
+
{
|
|
253
|
+
resourceType: text("resource_type").notNull(), // e.g., "catalog.system"
|
|
254
|
+
resourceId: text("resource_id").notNull(),
|
|
255
|
+
teamOnly: boolean("team_only").notNull().default(false), // If true, global permissions don't apply
|
|
256
|
+
},
|
|
257
|
+
(t) => ({
|
|
258
|
+
pk: primaryKey({ columns: [t.resourceType, t.resourceId] }),
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Centralized resource-level access control.
|
|
264
|
+
* Stores team grants for all resource types across the platform.
|
|
265
|
+
*/
|
|
266
|
+
export const resourceTeamAccess = pgTable(
|
|
267
|
+
"resource_team_access",
|
|
268
|
+
{
|
|
269
|
+
resourceType: text("resource_type").notNull(), // e.g., "catalog.system"
|
|
270
|
+
resourceId: text("resource_id").notNull(),
|
|
271
|
+
teamId: text("team_id")
|
|
272
|
+
.notNull()
|
|
273
|
+
.references(() => team.id, { onDelete: "cascade" }),
|
|
274
|
+
canRead: boolean("can_read").notNull().default(true),
|
|
275
|
+
canManage: boolean("can_manage").notNull().default(false),
|
|
276
|
+
},
|
|
277
|
+
(t) => ({
|
|
278
|
+
pk: primaryKey({ columns: [t.resourceType, t.resourceId, t.teamId] }),
|
|
279
|
+
})
|
|
280
|
+
);
|