1
- import {
2
- type DefaultSession ,
3
- type Session ,
4
- SvelteKitAuth ,
5
- type SvelteKitAuthConfig
6
- } from '@auth/sveltekit' ;
1
+ import { type DefaultSession , SvelteKitAuth , type SvelteKitAuthConfig } from '@auth/sveltekit' ;
7
2
import Auth0Provider from '@auth/sveltekit/providers/auth0' ;
8
3
import { trace } from '@opentelemetry/api' ;
9
- import { type Handle , error , isHttpError , redirect } from '@sveltejs/kit' ;
4
+ import { type Handle , error , redirect } from '@sveltejs/kit' ;
10
5
import { env } from '$env/dynamic/private' ;
11
6
import { checkInviteErrors } from '$lib/organizationInvites' ;
12
7
import { localizeHref } from '$lib/paraglide/runtime' ;
13
8
import { RoleId } from '$lib/prisma' ;
14
- import { verifyCanEdit , verifyCanView } from '$lib/projects/server' ;
15
9
import { DatabaseReads , DatabaseWrites } from '$lib/server/database' ;
16
- import { adminOrgs } from '$lib/users/server' ;
17
- import { ServerStatus } from '$lib/utils' ;
18
- import { isAdmin , isAdminForOrg , isSuperAdmin } from '$lib/utils/roles' ;
19
10
20
11
declare module '@auth/sveltekit' {
21
12
interface Session {
22
- user : {
23
- userId : number ;
24
- /** [organizationId, RoleId][]*/
25
- roles : [ number , RoleId ] [ ] ;
26
- } & DefaultSession [ 'user' ] ;
13
+ user : SecurityLike & DefaultSession [ 'user' ] ;
27
14
}
28
15
}
29
16
// Stupidly hacky way to get access to the request data from the auth callbacks
@@ -130,10 +117,15 @@ const config: SvelteKitAuthConfig = {
130
117
UserId : token . userId as number
131
118
}
132
119
} ) ;
133
- session . user . roles = userRoles . map ( ( role ) => [ role . OrganizationId , role . RoleId ] ) ;
120
+ const rolesMap = new Map < number , RoleId [ ] > ( ) ;
121
+ for ( const { OrganizationId, RoleId } of userRoles ) {
122
+ if ( ! rolesMap . has ( OrganizationId ) ) rolesMap . set ( OrganizationId , [ ] ) ;
123
+ rolesMap . get ( OrganizationId ) ! . push ( RoleId ) ;
124
+ }
125
+ session . user . roles = rolesMap ;
134
126
trace . getActiveSpan ( ) ?. addEvent ( 'session callback completed' , {
135
127
'auth.session.userId' : session . user . userId ,
136
- 'auth.session.roles' : JSON . stringify ( session . user . roles )
128
+ 'auth.session.roles' : JSON . stringify ( [ ... session . user . roles . entries ( ) ] )
137
129
} ) ;
138
130
return session ;
139
131
}
@@ -168,7 +160,7 @@ export class Security {
168
160
return this ;
169
161
}
170
162
171
- requireAdminForOrgIn ( organizationIds : number [ ] ) {
163
+ requireAdminOfOrgIn ( organizationIds : number [ ] ) {
172
164
this . requireAuthenticated ( ) ;
173
165
if (
174
166
! this . isSuperAdmin &&
@@ -179,9 +171,9 @@ export class Security {
179
171
return this ;
180
172
}
181
173
182
- requireAdminForOrg ( organizationId : number ) {
174
+ requireAdminOfOrg ( organizationId : number ) {
183
175
this . requireAuthenticated ( ) ;
184
- this . requireAdminForOrgIn ( [ organizationId ] ) ;
176
+ this . requireAdminOfOrgIn ( [ organizationId ] ) ;
185
177
return this ;
186
178
}
187
179
@@ -192,9 +184,13 @@ export class Security {
192
184
return this ;
193
185
}
194
186
195
- requireHasRole ( organizationId : number , roleId : RoleId ) {
187
+ requireHasRole ( organizationId : number , roleId : RoleId , orOrgAdmin = true ) {
196
188
this . requireAuthenticated ( ) ;
197
- if ( ! this . isSuperAdmin && ! this . roles . get ( organizationId ) ?. includes ( roleId ) )
189
+ if (
190
+ ! this . isSuperAdmin &&
191
+ ! this . roles . get ( organizationId ) ?. includes ( roleId ) &&
192
+ ! ( orOrgAdmin && this . roles . get ( organizationId ) ?. includes ( RoleId . OrgAdmin ) )
193
+ )
198
194
error ( 403 , 'User does not have the required role ' + roleId ) ;
199
195
return this ;
200
196
}
@@ -212,6 +208,17 @@ export class Security {
212
208
return this ;
213
209
}
214
210
211
+ requireProjectWriteAccess ( project ?: { OwnerId : number ; OrganizationId : number } ) {
212
+ this . requireAuthenticated ( ) ;
213
+ if ( ! project ) {
214
+ error ( 400 , 'Project is required for write access check' ) ;
215
+ }
216
+ if ( ! this . isSuperAdmin && project . OwnerId !== this . userId ) {
217
+ this . requireAdminOfOrg ( project . OrganizationId ) ;
218
+ }
219
+ return this ;
220
+ }
221
+
215
222
requireMemberOfAnyOrg ( ) {
216
223
this . requireAuthenticated ( ) ;
217
224
if ( this . organizationMemberships . length === 0 )
@@ -226,36 +233,29 @@ export class Security {
226
233
}
227
234
228
235
export const populateSecurityInfo : Handle = async ( { event, resolve } ) => {
229
- const session = ( await event . locals . auth ( ) ) ?. user ;
236
+ const security = ( await event . locals . auth ( ) ) ?. user ;
230
237
try {
231
- if ( session ) {
238
+ if ( security ) {
232
239
// If the user does not exist in the database, invalidate the login and redirect to prevent unauthorized access
233
240
// This can happen when the user is deleted from the database but still has a valid session.
234
241
// This should only happen when a superadmin manually deletes a user but is particularly annoying in development
235
242
// The user should also be redirected if they are not a member of any organizations
236
243
// Finally, the user should be redirected if they are locked
237
244
const user = await DatabaseReads . users . findUnique ( {
238
245
where : {
239
- Id : session . userId
246
+ Id : security . userId
240
247
} ,
241
248
include : {
242
249
OrganizationMemberships : true
243
250
}
244
251
} ) ;
245
252
246
- // Create a map of organizationId -> RoleId[]
247
- const rolesMap = new Map < number , RoleId [ ] > ( ) ;
248
- for ( const [ orgId , roleId ] of session . roles ) {
249
- if ( ! rolesMap . has ( orgId ) ) rolesMap . set ( orgId , [ ] ) ;
250
- rolesMap . get ( orgId ) ! . push ( roleId ) ;
251
- }
252
-
253
253
trace . getActiveSpan ( ) ?. addEvent ( 'checkUserExistsHandle completed' , {
254
- 'auth.userId' : session ?. userId ,
254
+ 'auth.userId' : security ?. userId ,
255
255
'auth.user' : user ? user . Email + ' - ' + user . Id : 'null' ,
256
256
'auth.user.OrganizationMemberships' :
257
257
user ?. OrganizationMemberships . map ( ( o ) => o . OrganizationId ) . join ( ', ' ) ?? 'null' ,
258
- 'auth.user.roles' : user ? JSON . stringify ( rolesMap . entries ( ) ) : 'null' ,
258
+ 'auth.user.roles' : user ? JSON . stringify ( [ ... security . roles . entries ( ) ] ) : 'null' ,
259
259
'auth.user.IsLocked' : user ? user . IsLocked + '' : 'null'
260
260
} ) ;
261
261
@@ -272,9 +272,9 @@ export const populateSecurityInfo: Handle = async ({ event, resolve }) => {
272
272
}
273
273
event . locals . security = new Security (
274
274
event ,
275
- session . userId ,
275
+ security . userId ,
276
276
user . OrganizationMemberships . map ( ( o ) => o . OrganizationId ) ,
277
- rolesMap
277
+ security . roles
278
278
) ;
279
279
}
280
280
} finally {
@@ -314,125 +314,3 @@ export const organizationInviteHandle: Handle = async ({ event, resolve }) => {
314
314
tokenStatus = null ;
315
315
return result ;
316
316
} ;
317
-
318
- // This is entirely unused now but will be referenced for setting up individual guards.
319
- // Locks down the authenticated routes by redirecting to /login
320
- // This guarantees a logged in user under (authenticated) but does not guarantee
321
- // authorization to the route. Each page must manually check in +page.server.ts or here
322
- export const localRouteHandle : Handle = async ( { event, resolve } ) => {
323
- if (
324
- ! event . route . id ?. startsWith ( '/(unauthenticated)' ) &&
325
- event . route . id !== '/' &&
326
- event . route . id !== null
327
- ) {
328
- const session = await event . locals . auth ( ) ;
329
- if ( ! session ) return redirect ( 302 , localizeHref ( '/login' ) ) ;
330
- const status = await validateRouteForAuthenticatedUser (
331
- session ,
332
- event . route . id ,
333
- event . params ,
334
- event . request . method
335
- ) ;
336
- trace . getActiveSpan ( ) ?. addEvent ( 'localRouteHandle completed' , {
337
- 'auth.localRouteHandle.routeId' : event . route . id ?? 'null' ,
338
- 'auth.localRouteHandle.params' : JSON . stringify ( event . params ) ,
339
- 'auth.localRouteHandle.session.userId' : session . user . userId ,
340
- 'auth.localRouteHandle.session.roles' : JSON . stringify ( session . user . roles ) ,
341
- 'auth.localRouteHandle.status' : status
342
- } ) ;
343
- if ( status !== ServerStatus . Ok ) {
344
- // error.html is extremely ugly, so we use a manual error throw
345
- // event.locals.error = status;
346
- }
347
- }
348
- return resolve ( event ) ;
349
- } ;
350
-
351
- async function validateRouteForAuthenticatedUser (
352
- session : Session ,
353
- route : string ,
354
- params : Partial < Record < string , string > > ,
355
- method : string
356
- ) : Promise < ServerStatus > {
357
- const path = route . split ( '/' ) . filter ( ( r ) => ! ! r ) ;
358
- // Only guarding authenticated routes
359
- if ( path [ 0 ] === '(authenticated)' ) {
360
- if ( path [ 1 ] === 'admin' || path [ 1 ] === 'workflow-instances' )
361
- return isSuperAdmin ( session ?. user ?. roles ) ? ServerStatus . Ok : ServerStatus . Forbidden ;
362
- else if ( path [ 1 ] === 'directory' || path [ 1 ] === 'open-source' )
363
- // Always allowed. Open pages
364
- return ServerStatus . Ok ;
365
- else if ( path [ 1 ] === 'organizations' ) {
366
- // Must be org admin for some organization (or a super admin)
367
- if ( ! isAdmin ( session ?. user ?. roles ) ) return ServerStatus . Forbidden ;
368
- if ( params . id ) {
369
- // Must be org admin for specified organization (or a super admin)
370
- const Id = parseInt ( params . id ) ;
371
- if ( isNaN ( Id ) || ! ( await DatabaseReads . organizations . findFirst ( { where : { Id } } ) ) ) {
372
- return ServerStatus . NotFound ;
373
- }
374
- return isAdminForOrg ( Id , session ?. user ?. roles ) ? ServerStatus . Ok : ServerStatus . Forbidden ;
375
- }
376
- return ServerStatus . Ok ;
377
- } else if ( path [ 1 ] === 'products' ) {
378
- try {
379
- const product = await DatabaseReads . products . findFirst ( {
380
- where : {
381
- Id : params . id
382
- }
383
- } ) ;
384
- if ( ! product ) return ServerStatus . NotFound ;
385
- // Must be allowed to view associated project
386
- // (this route was originally part of the project page but was moved elsewhere to improve load time)
387
- return await verifyCanView ( session , product . ProjectId ) ;
388
- } catch {
389
- return ServerStatus . NotFound ;
390
- }
391
- } else if ( path [ 1 ] === 'projects' ) {
392
- if ( path [ 2 ] === '[filter=projectSelector]' ) return ServerStatus . Ok ;
393
- else if ( path [ 2 ] === '[id=idNumber]' ) {
394
- // prevent edits and actions without breaking SSE
395
- if ( path [ 3 ] === 'edit' || ( method !== 'GET' && path [ 3 ] !== 'sse' ) ) {
396
- return await verifyCanEdit ( session , parseInt ( params . id ! ) ) ;
397
- }
398
- // A project can be viewed if the user is an admin or in the project's group
399
- return await verifyCanView ( session , parseInt ( params . id ! ) ) ;
400
- }
401
- return ServerStatus . Ok ;
402
- } else if ( path [ 1 ] === 'tasks' ) {
403
- // Own task list always allowed, and specific products checked manually
404
- return ServerStatus . Ok ;
405
- } else if ( path [ 1 ] === 'users' ) {
406
- // /(invite): admin
407
- if ( path . length === 2 || path [ 2 ] === 'invite' ) {
408
- return isAdmin ( session ?. user ?. roles ) ? ServerStatus . Ok : ServerStatus . Forbidden ;
409
- } else if ( path [ 2 ] === '[id=idNumber]' ) {
410
- // /id: not implemented yet (ISSUE #1142)
411
- const subjectId = parseInt ( params . id ! ) ;
412
- if ( ! ( await DatabaseReads . users . findFirst ( { where : { Id : subjectId } } ) ) )
413
- return ServerStatus . NotFound ;
414
- const admin = ! ! ( await DatabaseReads . organizations . findFirst ( {
415
- where : adminOrgs ( subjectId , session . user . userId , isSuperAdmin ( session . user . roles ) ) ,
416
- select : {
417
- Id : true
418
- }
419
- } ) ) ;
420
- // /id/settings/(profile): self and admin
421
- if ( ! path . at ( 4 ) || path [ 4 ] === 'profile' ) {
422
- return subjectId === session . user . userId || admin
423
- ? ServerStatus . Ok
424
- : ServerStatus . Forbidden ;
425
- } else {
426
- // /id/settings/*: admin
427
- return admin ? ServerStatus . Ok : ServerStatus . Forbidden ;
428
- }
429
- }
430
- return ServerStatus . Ok ;
431
- } else {
432
- // Unknown route. We'll assume it's a legal route
433
- return ServerStatus . Ok ;
434
- }
435
- } else {
436
- return ServerStatus . Ok ;
437
- }
438
- }
0 commit comments