@checkstack/auth-backend 0.1.0 → 0.2.1
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 +103 -0
- package/drizzle/0004_lucky_power_man.sql +21 -0
- package/drizzle/meta/0004_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/index.ts +166 -162
- package/src/router.test.ts +11 -11
- package/src/router.ts +98 -98
- package/src/schema.ts +20 -20
- package/src/teams.test.ts +836 -81
- package/src/utils/user.test.ts +10 -10
- package/src/utils/user.ts +13 -13
package/src/router.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ describe("Auth Router", () => {
|
|
|
17
17
|
const mockUser = {
|
|
18
18
|
type: "user" as const,
|
|
19
19
|
id: "test-user",
|
|
20
|
-
|
|
20
|
+
accessRules: ["*"],
|
|
21
21
|
roles: ["admin"],
|
|
22
22
|
} as any;
|
|
23
23
|
|
|
@@ -69,8 +69,8 @@ describe("Auth Router", () => {
|
|
|
69
69
|
list: mock(() => Promise.resolve([])),
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
|
|
72
|
+
const mockAccessRuleRegistry = {
|
|
73
|
+
getAccessRules: () => [
|
|
74
74
|
{ id: "auth-backend.users.read", description: "List all users" },
|
|
75
75
|
{ id: "auth-backend.users.manage", description: "Delete users" },
|
|
76
76
|
{ id: "auth-backend.roles.read", description: "Read and list roles" },
|
|
@@ -82,13 +82,13 @@ describe("Auth Router", () => {
|
|
|
82
82
|
mockRegistry,
|
|
83
83
|
async () => {},
|
|
84
84
|
mockConfigService,
|
|
85
|
-
|
|
85
|
+
mockAccessRuleRegistry
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
-
it("
|
|
88
|
+
it("getAccessRules returns current user access rules", async () => {
|
|
89
89
|
const context = createMockRpcContext({ user: mockUser });
|
|
90
|
-
const result = await call(router.
|
|
91
|
-
expect(result.
|
|
90
|
+
const result = await call(router.accessRules, undefined, { context });
|
|
91
|
+
expect(result.accessRules).toContain("*");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it("getUsers lists users with roles", async () => {
|
|
@@ -145,21 +145,21 @@ describe("Auth Router", () => {
|
|
|
145
145
|
expect(deletedTables.includes(schema.user)).toBe(true);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
it("getRoles returns all roles with
|
|
148
|
+
it("getRoles returns all roles with accesss", async () => {
|
|
149
149
|
const context = createMockRpcContext({ user: mockUser });
|
|
150
150
|
mockDb.select.mockImplementationOnce(() => ({
|
|
151
151
|
from: mock(() => createChain([{ id: "admin", name: "Admin" }])),
|
|
152
152
|
}));
|
|
153
153
|
mockDb.select.mockImplementationOnce(() => ({
|
|
154
154
|
from: mock(() =>
|
|
155
|
-
createChain([{ roleId: "admin",
|
|
155
|
+
createChain([{ roleId: "admin", accessRuleId: "users.manage" }])
|
|
156
156
|
),
|
|
157
157
|
}));
|
|
158
158
|
|
|
159
159
|
const result = await call(router.getRoles, undefined, { context });
|
|
160
160
|
expect(result).toHaveLength(1);
|
|
161
161
|
expect(result[0].id).toBe("admin");
|
|
162
|
-
expect(result[0].
|
|
162
|
+
expect(result[0].accessRules).toContain("users.manage");
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it("updateUserRoles updates user roles", async () => {
|
|
@@ -214,7 +214,7 @@ describe("Auth Router", () => {
|
|
|
214
214
|
expect(result.allowRegistration).toBe(true);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
it("setRegistrationStatus updates flag and requires
|
|
217
|
+
it("setRegistrationStatus updates flag and requires access", async () => {
|
|
218
218
|
const context = createMockRpcContext({ user: mockUser });
|
|
219
219
|
const result = await call(
|
|
220
220
|
router.setRegistrationStatus,
|
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
|
|
|
@@ -1110,7 +1110,7 @@ export const createAuthRouter = (
|
|
|
1110
1110
|
|
|
1111
1111
|
const updateTeam = os.updateTeam.handler(async ({ input, context }) => {
|
|
1112
1112
|
const { id, name, description } = input;
|
|
1113
|
-
// TODO: Check if user is manager or has teamsManage
|
|
1113
|
+
// TODO: Check if user is manager or has teamsManage access
|
|
1114
1114
|
const updates: {
|
|
1115
1115
|
name?: string;
|
|
1116
1116
|
description?: string | null;
|
|
@@ -1288,7 +1288,7 @@ export const createAuthRouter = (
|
|
|
1288
1288
|
resourceType,
|
|
1289
1289
|
resourceId,
|
|
1290
1290
|
action,
|
|
1291
|
-
|
|
1291
|
+
hasGlobalAccess,
|
|
1292
1292
|
} = input;
|
|
1293
1293
|
|
|
1294
1294
|
const grants = await internalDb
|
|
@@ -1301,8 +1301,8 @@ export const createAuthRouter = (
|
|
|
1301
1301
|
)
|
|
1302
1302
|
);
|
|
1303
1303
|
|
|
1304
|
-
// No grants = global
|
|
1305
|
-
if (grants.length === 0) return { hasAccess:
|
|
1304
|
+
// No grants = global access applies
|
|
1305
|
+
if (grants.length === 0) return { hasAccess: hasGlobalAccess };
|
|
1306
1306
|
|
|
1307
1307
|
// Check resource-level settings for teamOnly
|
|
1308
1308
|
const settingsRows = await internalDb
|
|
@@ -1317,7 +1317,7 @@ export const createAuthRouter = (
|
|
|
1317
1317
|
.limit(1);
|
|
1318
1318
|
const isTeamOnly = settingsRows[0]?.teamOnly ?? false;
|
|
1319
1319
|
|
|
1320
|
-
if (!isTeamOnly &&
|
|
1320
|
+
if (!isTeamOnly && hasGlobalAccess) return { hasAccess: true };
|
|
1321
1321
|
|
|
1322
1322
|
// Get user's teams
|
|
1323
1323
|
const teamTable =
|
|
@@ -1353,7 +1353,7 @@ export const createAuthRouter = (
|
|
|
1353
1353
|
resourceType,
|
|
1354
1354
|
resourceIds,
|
|
1355
1355
|
action,
|
|
1356
|
-
|
|
1356
|
+
hasGlobalAccess,
|
|
1357
1357
|
} = input;
|
|
1358
1358
|
if (resourceIds.length === 0) return [];
|
|
1359
1359
|
|
|
@@ -1410,9 +1410,9 @@ export const createAuthRouter = (
|
|
|
1410
1410
|
|
|
1411
1411
|
return resourceIds.filter((id) => {
|
|
1412
1412
|
const resourceGrants = grantsByResource.get(id) || [];
|
|
1413
|
-
if (resourceGrants.length === 0) return
|
|
1413
|
+
if (resourceGrants.length === 0) return hasGlobalAccess;
|
|
1414
1414
|
const isTeamOnly = teamOnlyByResource.get(id) ?? false;
|
|
1415
|
-
if (!isTeamOnly &&
|
|
1415
|
+
if (!isTeamOnly && hasGlobalAccess) return true;
|
|
1416
1416
|
return resourceGrants.some(
|
|
1417
1417
|
(g) => userTeamIds.has(g.teamId) && g[field]
|
|
1418
1418
|
);
|
|
@@ -1435,11 +1435,11 @@ export const createAuthRouter = (
|
|
|
1435
1435
|
|
|
1436
1436
|
return os.router({
|
|
1437
1437
|
getEnabledStrategies,
|
|
1438
|
-
|
|
1438
|
+
accessRules: accessRulesHandler,
|
|
1439
1439
|
getUsers,
|
|
1440
1440
|
deleteUser,
|
|
1441
1441
|
getRoles,
|
|
1442
|
-
|
|
1442
|
+
getAccessRules,
|
|
1443
1443
|
createRole,
|
|
1444
1444
|
updateRole,
|
|
1445
1445
|
deleteRole,
|
|
@@ -1450,9 +1450,9 @@ export const createAuthRouter = (
|
|
|
1450
1450
|
getRegistrationSchema,
|
|
1451
1451
|
getRegistrationStatus,
|
|
1452
1452
|
setRegistrationStatus,
|
|
1453
|
-
|
|
1453
|
+
getAnonymousAccessRules,
|
|
1454
1454
|
getUserById,
|
|
1455
|
-
|
|
1455
|
+
filterUsersByAccessRule,
|
|
1456
1456
|
findUserByEmail,
|
|
1457
1457
|
upsertExternalUser,
|
|
1458
1458
|
createSession,
|
package/src/schema.ts
CHANGED
|
@@ -66,23 +66,23 @@ export const role = pgTable("role", {
|
|
|
66
66
|
isSystem: boolean("is_system").default(false), // Prevent deletion of core roles
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
export const
|
|
69
|
+
export const accessRule = pgTable("access_rule", {
|
|
70
70
|
id: text("id").primaryKey(), // 'core.manage-users', etc.
|
|
71
71
|
description: text("description"),
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
export const
|
|
75
|
-
"
|
|
74
|
+
export const roleAccessRule = pgTable(
|
|
75
|
+
"role_access_rule",
|
|
76
76
|
{
|
|
77
77
|
roleId: text("role_id")
|
|
78
78
|
.notNull()
|
|
79
79
|
.references(() => role.id),
|
|
80
|
-
|
|
80
|
+
accessRuleId: text("access_rule_id")
|
|
81
81
|
.notNull()
|
|
82
|
-
.references(() =>
|
|
82
|
+
.references(() => accessRule.id),
|
|
83
83
|
},
|
|
84
84
|
(t) => ({
|
|
85
|
-
pk: primaryKey({ columns: [t.roleId, t.
|
|
85
|
+
pk: primaryKey({ columns: [t.roleId, t.accessRuleId] }),
|
|
86
86
|
})
|
|
87
87
|
);
|
|
88
88
|
|
|
@@ -102,31 +102,31 @@ export const userRole = pgTable(
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Tracks authenticated default
|
|
106
|
-
* When a plugin registers
|
|
105
|
+
* Tracks authenticated default access rules that have been disabled by admins.
|
|
106
|
+
* When a plugin registers an access rule with isAuthenticatedDefault=true, it gets assigned
|
|
107
107
|
* to the "users" role unless it's in this table.
|
|
108
108
|
*/
|
|
109
|
-
export const
|
|
110
|
-
"
|
|
109
|
+
export const disabledDefaultAccessRule = pgTable(
|
|
110
|
+
"disabled_default_access_rule",
|
|
111
111
|
{
|
|
112
|
-
|
|
112
|
+
accessRuleId: text("access_rule_id")
|
|
113
113
|
.primaryKey()
|
|
114
|
-
.references(() =>
|
|
114
|
+
.references(() => accessRule.id),
|
|
115
115
|
disabledAt: timestamp("disabled_at").notNull(),
|
|
116
116
|
}
|
|
117
117
|
);
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Tracks public default
|
|
121
|
-
* When a plugin registers
|
|
120
|
+
* Tracks public default access rules that have been disabled by admins.
|
|
121
|
+
* When a plugin registers an access rule with isPublicDefault=true, it gets assigned
|
|
122
122
|
* to the "anonymous" role unless it's in this table.
|
|
123
123
|
*/
|
|
124
|
-
export const
|
|
125
|
-
"
|
|
124
|
+
export const disabledPublicDefaultAccessRule = pgTable(
|
|
125
|
+
"disabled_public_default_access_rule",
|
|
126
126
|
{
|
|
127
|
-
|
|
127
|
+
accessRuleId: text("access_rule_id")
|
|
128
128
|
.primaryKey()
|
|
129
|
-
.references(() =>
|
|
129
|
+
.references(() => accessRule.id),
|
|
130
130
|
disabledAt: timestamp("disabled_at").notNull(),
|
|
131
131
|
}
|
|
132
132
|
);
|
|
@@ -245,14 +245,14 @@ export const teamManager = pgTable(
|
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* Resource-level access settings.
|
|
248
|
-
* Controls whether a resource requires team membership (teamOnly) vs allowing global
|
|
248
|
+
* Controls whether a resource requires team membership (teamOnly) vs allowing global access.
|
|
249
249
|
*/
|
|
250
250
|
export const resourceAccessSettings = pgTable(
|
|
251
251
|
"resource_access_settings",
|
|
252
252
|
{
|
|
253
253
|
resourceType: text("resource_type").notNull(), // e.g., "catalog.system"
|
|
254
254
|
resourceId: text("resource_id").notNull(),
|
|
255
|
-
teamOnly: boolean("team_only").notNull().default(false), // If true, global
|
|
255
|
+
teamOnly: boolean("team_only").notNull().default(false), // If true, global access doesn't apply
|
|
256
256
|
},
|
|
257
257
|
(t) => ({
|
|
258
258
|
pk: primaryKey({ columns: [t.resourceType, t.resourceId] }),
|