@@ -35,6 +35,7 @@ async function loadRuntime() {
3535 { runCleanupOAuthTokens, OAUTH_TOKEN_RETENTION_DAYS } ,
3636 { isCapabilityWithheldForUser } ,
3737 { acquirePermissionGroupOrgLock } ,
38+ { acquireOrganizationMutationLock } ,
3839 ] = await Promise . all ( [
3940 import ( '@sim/db' ) ,
4041 import ( '@sim/db/schema' ) ,
@@ -50,6 +51,7 @@ async function loadRuntime() {
5051 import ( '@/background/cleanup-oauth-tokens' ) ,
5152 import ( '@/lib/permission-groups/user-scope.server' ) ,
5253 import ( '@/lib/permission-groups/locks' ) ,
54+ import ( '@/lib/billing/organizations/membership' ) ,
5355 ] )
5456 const adapter = createSimAuthAdapter ( {
5557 plugins : [
@@ -81,6 +83,7 @@ async function loadRuntime() {
8183 OAUTH_TOKEN_RETENTION_DAYS ,
8284 isCapabilityWithheldForUser,
8385 acquirePermissionGroupOrgLock,
86+ acquireOrganizationMutationLock,
8487 }
8588}
8689
@@ -298,111 +301,157 @@ describe.skipIf(!databaseUrl)('OAuth lifecycle on the provisioned PostgreSQL sch
298301 }
299302 )
300303
301- it ( 'serializes refresh against a pending policy restriction without consuming the token' , async ( ) => {
302- const { db, schema, eq, sql, statement, acquirePermissionGroupOrgLock } = runtime
303- const groupId = await createDefaultGroup ( )
304- await grantConsent ( )
305- const family = await issueFamily ( )
306- const credentials = { clientId, method : 'none' as const }
307- const writerReady = Promise . withResolvers < number > ( )
308- const releaseWriter = Promise . withResolvers < void > ( )
309- const writer = db . transaction ( async ( tx ) => {
310- await acquirePermissionGroupOrgLock ( tx , organizationId ! )
311- await tx
312- . update ( schema . permissionGroup )
313- . set ( { config : { disableOAuthAppAccess : true } } )
314- . where ( eq ( schema . permissionGroup . id , groupId ) )
315- const [ connection ] = await tx . execute < { pid : number } > (
316- statement `select pg_backend_pid() as pid`
317- )
318- writerReady . resolve ( connection . pid )
319- await releaseWriter . promise
320- } )
321- let refresh : ReturnType < typeof runtime . rotateOAuthRefreshToken > | undefined
322-
323- try {
324- const writerPid = await Promise . race ( [
325- writerReady . promise ,
326- writer . then ( ( ) => {
327- throw new Error ( 'Policy writer finished before the concurrency check' )
328- } ) ,
329- ] )
330- let refreshSettled = false
331- refresh = runtime . rotateOAuthRefreshToken ( { credentials, refreshToken : family . refreshToken } )
332- void refresh . then (
333- ( ) => {
334- refreshSettled = true
335- } ,
336- ( ) => {
337- refreshSettled = true
304+ it . each ( [ 'policy restriction' , 'enterprise activation' ] as const ) (
305+ 'serializes refresh against a pending %s without consuming the token' ,
306+ async ( change ) => {
307+ const {
308+ db,
309+ schema,
310+ eq,
311+ sql,
312+ statement,
313+ acquirePermissionGroupOrgLock,
314+ acquireOrganizationMutationLock,
315+ } = runtime
316+ const groupId = await createDefaultGroup ( )
317+ await grantConsent ( )
318+ const family = await issueFamily ( )
319+ if ( change === 'enterprise activation' ) {
320+ await db
321+ . update ( schema . subscription )
322+ . set ( {
323+ plan : 'team' ,
324+ metadata : { plan : 'team' , referenceId : organizationId ! , seats : 5 , monthlyPrice : 100 } ,
325+ } )
326+ . where ( eq ( schema . subscription . referenceId , organizationId ! ) )
327+ await db
328+ . update ( schema . permissionGroup )
329+ . set ( { config : { disableOAuthAppAccess : true } } )
330+ . where ( eq ( schema . permissionGroup . id , groupId ) )
331+ }
332+ expect ( await runtime . isCapabilityWithheldForUser ( userId , 'oauth_apps.use' ) ) . toBe ( false )
333+ const credentials = { clientId, method : 'none' as const }
334+ const writerReady = Promise . withResolvers < number > ( )
335+ const releaseWriter = Promise . withResolvers < void > ( )
336+ const writer = db . transaction ( async ( tx ) => {
337+ if ( change === 'enterprise activation' ) {
338+ await acquireOrganizationMutationLock ( tx , organizationId ! )
339+ await tx
340+ . update ( schema . subscription )
341+ . set ( {
342+ plan : 'enterprise' ,
343+ metadata : {
344+ plan : 'enterprise' ,
345+ referenceId : organizationId ! ,
346+ seats : 5 ,
347+ monthlyPrice : 100 ,
348+ } ,
349+ } )
350+ . where ( eq ( schema . subscription . referenceId , organizationId ! ) )
351+ } else {
352+ await acquirePermissionGroupOrgLock ( tx , organizationId ! )
353+ await tx
354+ . update ( schema . permissionGroup )
355+ . set ( { config : { disableOAuthAppAccess : true } } )
356+ . where ( eq ( schema . permissionGroup . id , groupId ) )
338357 }
339- )
340-
341- let waitingForPolicy = false
342- const deadline = Date . now ( ) + 2_000
343- while ( ! refreshSettled && Date . now ( ) < deadline ) {
344- const [ waiter ] = await sql < { waiting : boolean } [ ] > `
358+ const [ connection ] = await tx . execute < { pid : number } > (
359+ statement `select pg_backend_pid() as pid`
360+ )
361+ writerReady . resolve ( connection . pid )
362+ await releaseWriter . promise
363+ } )
364+ let refresh : ReturnType < typeof runtime . rotateOAuthRefreshToken > | undefined
365+
366+ try {
367+ const writerPid = await Promise . race ( [
368+ writerReady . promise ,
369+ writer . then ( ( ) => {
370+ throw new Error ( 'Policy writer finished before the concurrency check' )
371+ } ) ,
372+ ] )
373+ let refreshSettled = false
374+ refresh = runtime . rotateOAuthRefreshToken ( {
375+ credentials,
376+ refreshToken : family . refreshToken ,
377+ } )
378+ void refresh . then (
379+ ( ) => {
380+ refreshSettled = true
381+ } ,
382+ ( ) => {
383+ refreshSettled = true
384+ }
385+ )
386+
387+ let waitingForPolicy = false
388+ const deadline = Date . now ( ) + 2_000
389+ while ( ! refreshSettled && Date . now ( ) < deadline ) {
390+ const [ waiter ] = await sql < { waiting : boolean } [ ] > `
345391 SELECT EXISTS (
346392 SELECT 1 FROM pg_stat_activity
347393 WHERE ${ writerPid } = ANY(pg_blocking_pids(pid))
348394 AND wait_event_type = 'Lock'
349395 AND wait_event = 'advisory'
350396 ) AS waiting
351397 `
352- if ( waiter . waiting ) {
353- waitingForPolicy = true
354- break
398+ if ( waiter . waiting ) {
399+ waitingForPolicy = true
400+ break
401+ }
402+ await sleep ( 1 )
355403 }
356- await sleep ( 1 )
404+ expect ( waitingForPolicy , 'Refresh must wait for the organization policy writer' ) . toBe ( true )
405+ expect ( refreshSettled ) . toBe ( false )
406+ releaseWriter . resolve ( )
407+ await writer
408+ expect ( await runtime . isCapabilityWithheldForUser ( userId , 'oauth_apps.use' ) ) . toBe ( true )
409+ await expect ( refresh ) . resolves . toMatchObject ( { success : false , error : 'invalid_grant' } )
410+ expect (
411+ await db
412+ . select ( { generation : schema . oauthTokenFamily . currentGeneration } )
413+ . from ( schema . oauthTokenFamily )
414+ . where ( eq ( schema . oauthTokenFamily . id , family . id ) )
415+ ) . toEqual ( [ { generation : 0 } ] )
416+ expect (
417+ await db
418+ . select ( {
419+ generation : schema . oauthRefreshToken . generation ,
420+ revoked : schema . oauthRefreshToken . revoked ,
421+ } )
422+ . from ( schema . oauthRefreshToken )
423+ . where ( eq ( schema . oauthRefreshToken . familyId , family . id ) )
424+ ) . toEqual ( [ { generation : 0 , revoked : null } ] )
425+ expect (
426+ await db
427+ . select ( { id : schema . oauthAccessToken . id } )
428+ . from ( schema . oauthAccessToken )
429+ . where ( eq ( schema . oauthAccessToken . clientId , clientId ) )
430+ ) . toHaveLength ( 1 )
431+
432+ await db . transaction ( async ( tx ) => {
433+ await acquirePermissionGroupOrgLock ( tx , organizationId ! )
434+ await tx
435+ . update ( schema . permissionGroup )
436+ . set ( { config : { } } )
437+ . where ( eq ( schema . permissionGroup . id , groupId ) )
438+ } )
439+ await expect (
440+ runtime . rotateOAuthRefreshToken ( { credentials, refreshToken : family . refreshToken } )
441+ ) . resolves . toMatchObject ( { success : true } )
442+ expect (
443+ await db
444+ . select ( { generation : schema . oauthTokenFamily . currentGeneration } )
445+ . from ( schema . oauthTokenFamily )
446+ . where ( eq ( schema . oauthTokenFamily . id , family . id ) )
447+ ) . toEqual ( [ { generation : 1 } ] )
448+ } finally {
449+ releaseWriter . resolve ( )
450+ await Promise . allSettled ( [ writer , ...( refresh ? [ refresh ] : [ ] ) ] )
357451 }
358- expect ( waitingForPolicy , 'Refresh must wait for the organization policy writer' ) . toBe ( true )
359- expect ( refreshSettled ) . toBe ( false )
360- releaseWriter . resolve ( )
361- await writer
362- await expect ( refresh ) . resolves . toMatchObject ( { success : false , error : 'invalid_grant' } )
363- expect (
364- await db
365- . select ( { generation : schema . oauthTokenFamily . currentGeneration } )
366- . from ( schema . oauthTokenFamily )
367- . where ( eq ( schema . oauthTokenFamily . id , family . id ) )
368- ) . toEqual ( [ { generation : 0 } ] )
369- expect (
370- await db
371- . select ( {
372- generation : schema . oauthRefreshToken . generation ,
373- revoked : schema . oauthRefreshToken . revoked ,
374- } )
375- . from ( schema . oauthRefreshToken )
376- . where ( eq ( schema . oauthRefreshToken . familyId , family . id ) )
377- ) . toEqual ( [ { generation : 0 , revoked : null } ] )
378- expect (
379- await db
380- . select ( { id : schema . oauthAccessToken . id } )
381- . from ( schema . oauthAccessToken )
382- . where ( eq ( schema . oauthAccessToken . clientId , clientId ) )
383- ) . toHaveLength ( 1 )
384-
385- await db . transaction ( async ( tx ) => {
386- await acquirePermissionGroupOrgLock ( tx , organizationId ! )
387- await tx
388- . update ( schema . permissionGroup )
389- . set ( { config : { } } )
390- . where ( eq ( schema . permissionGroup . id , groupId ) )
391- } )
392- await expect (
393- runtime . rotateOAuthRefreshToken ( { credentials, refreshToken : family . refreshToken } )
394- ) . resolves . toMatchObject ( { success : true } )
395- expect (
396- await db
397- . select ( { generation : schema . oauthTokenFamily . currentGeneration } )
398- . from ( schema . oauthTokenFamily )
399- . where ( eq ( schema . oauthTokenFamily . id , family . id ) )
400- ) . toEqual ( [ { generation : 1 } ] )
401- } finally {
402- releaseWriter . resolve ( )
403- await Promise . allSettled ( [ writer , ...( refresh ? [ refresh ] : [ ] ) ] )
404- }
405- } , 10_000 )
452+ } ,
453+ 10_000
454+ )
406455
407456 it ( 'atomically converges concurrent consent submissions on one grant' , async ( ) => {
408457 const grants = await Promise . all ( [ grantConsent ( ) , grantConsent ( ) ] )
0 commit comments