@@ -4,13 +4,16 @@ import { notAuthenticated } from './lib/serviceError';
44import { getAuthContext , getAuthenticatedUser , withAuthV2 , withOptionalAuthV2 } from './withAuthV2' ;
55import { MOCK_API_KEY , MOCK_OAUTH_TOKEN , MOCK_ORG , MOCK_USER_WITH_ACCOUNTS , prisma } from './__mocks__/prisma' ;
66import { OrgRole } from '@sourcebot/db' ;
7+ import { ErrorCode } from './lib/errorCodes' ;
8+ import { StatusCodes } from 'http-status-codes' ;
79
810const mocks = vi . hoisted ( ( ) => {
911 return {
1012 // Defaults to a empty session.
1113 auth : vi . fn ( async ( ) : Promise < Session | null > => null ) ,
1214 headers : vi . fn ( async ( ) : Promise < Headers > => new Headers ( ) ) ,
1315 hasEntitlement : vi . fn ( ( _entitlement : string ) => false ) ,
16+ env : { } as Record < string , string > ,
1417 }
1518} ) ;
1619
@@ -40,7 +43,7 @@ vi.mock('@sourcebot/shared', () => ({
4043 OAUTH_ACCESS_TOKEN_PREFIX : 'sboa_' ,
4144 API_KEY_PREFIX : 'sbk_' ,
4245 LEGACY_API_KEY_PREFIX : 'sourcebot-' ,
43- env : { }
46+ env : mocks . env ,
4447} ) ) ;
4548
4649// Test utility to set the mock session
@@ -70,6 +73,8 @@ beforeEach(() => {
7073 vi . clearAllMocks ( ) ;
7174 mocks . auth . mockResolvedValue ( null ) ;
7275 mocks . headers . mockResolvedValue ( new Headers ( ) ) ;
76+ // Reset env flags between tests
77+ Object . keys ( mocks . env ) . forEach ( key => delete mocks . env [ key ] ) ;
7378} ) ;
7479
7580describe ( 'getAuthenticatedUser' , ( ) => {
@@ -80,9 +85,10 @@ describe('getAuthenticatedUser', () => {
8085 id : userId ,
8186 } ) ;
8287 setMockSession ( createMockSession ( { user : { id : 'test-user-id' } } ) ) ;
83- const user = await getAuthenticatedUser ( ) ;
84- expect ( user ) . not . toBeUndefined ( ) ;
85- expect ( user ?. id ) . toBe ( userId ) ;
88+ const result = await getAuthenticatedUser ( ) ;
89+ expect ( result ) . not . toBeUndefined ( ) ;
90+ expect ( result ?. user . id ) . toBe ( userId ) ;
91+ expect ( result ?. source ) . toBe ( 'session' ) ;
8692 } ) ;
8793
8894 test ( 'should return a user object if a valid api key is present' , async ( ) => {
@@ -98,9 +104,10 @@ describe('getAuthenticatedUser', () => {
98104 } ) ;
99105
100106 setMockHeaders ( new Headers ( { 'X-Sourcebot-Api-Key' : 'sourcebot-apikey' } ) ) ;
101- const user = await getAuthenticatedUser ( ) ;
102- expect ( user ) . not . toBeUndefined ( ) ;
103- expect ( user ?. id ) . toBe ( userId ) ;
107+ const result = await getAuthenticatedUser ( ) ;
108+ expect ( result ) . not . toBeUndefined ( ) ;
109+ expect ( result ?. user . id ) . toBe ( userId ) ;
110+ expect ( result ?. source ) . toBe ( 'api_key' ) ;
104111 expect ( prisma . apiKey . update ) . toHaveBeenCalledWith ( {
105112 where : {
106113 hash : 'apikey' ,
@@ -124,9 +131,10 @@ describe('getAuthenticatedUser', () => {
124131 } ) ;
125132
126133 setMockHeaders ( new Headers ( { 'X-Sourcebot-Api-Key' : 'sbk_apikey' } ) ) ;
127- const user = await getAuthenticatedUser ( ) ;
128- expect ( user ) . not . toBeUndefined ( ) ;
129- expect ( user ?. id ) . toBe ( userId ) ;
134+ const result = await getAuthenticatedUser ( ) ;
135+ expect ( result ) . not . toBeUndefined ( ) ;
136+ expect ( result ?. user . id ) . toBe ( userId ) ;
137+ expect ( result ?. source ) . toBe ( 'api_key' ) ;
130138 expect ( prisma . apiKey . update ) . toHaveBeenCalledWith ( {
131139 where : { hash : 'apikey' } ,
132140 data : { lastUsedAt : expect . any ( Date ) } ,
@@ -146,9 +154,10 @@ describe('getAuthenticatedUser', () => {
146154 } ) ;
147155
148156 setMockHeaders ( new Headers ( { 'Authorization' : 'Bearer sourcebot-apikey' } ) ) ;
149- const user = await getAuthenticatedUser ( ) ;
150- expect ( user ) . not . toBeUndefined ( ) ;
151- expect ( user ?. id ) . toBe ( userId ) ;
157+ const result = await getAuthenticatedUser ( ) ;
158+ expect ( result ) . not . toBeUndefined ( ) ;
159+ expect ( result ?. user . id ) . toBe ( userId ) ;
160+ expect ( result ?. source ) . toBe ( 'api_key' ) ;
152161 expect ( prisma . apiKey . update ) . toHaveBeenCalledWith ( {
153162 where : {
154163 hash : 'apikey' ,
@@ -170,9 +179,10 @@ describe('getAuthenticatedUser', () => {
170179 mocks . hasEntitlement . mockReturnValue ( true ) ;
171180 prisma . oAuthToken . findUnique . mockResolvedValue ( MOCK_OAUTH_TOKEN ) ;
172181 setMockHeaders ( new Headers ( { 'Authorization' : 'Bearer sboa_oauthtoken' } ) ) ;
173- const user = await getAuthenticatedUser ( ) ;
174- expect ( user ) . not . toBeUndefined ( ) ;
175- expect ( user ?. id ) . toBe ( MOCK_USER_WITH_ACCOUNTS . id ) ;
182+ const result = await getAuthenticatedUser ( ) ;
183+ expect ( result ) . not . toBeUndefined ( ) ;
184+ expect ( result ?. user . id ) . toBe ( MOCK_USER_WITH_ACCOUNTS . id ) ;
185+ expect ( result ?. source ) . toBe ( 'oauth' ) ;
176186 } ) ;
177187
178188 test ( 'should update lastUsedAt when an OAuth Bearer token is used' , async ( ) => {
@@ -380,6 +390,75 @@ describe('getAuthContext', () => {
380390 prisma : undefined ,
381391 } ) ;
382392 } ) ;
393+
394+ describe ( 'DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS' , ( ) => {
395+ test ( 'should return a 403 service error when flag is enabled and a non-owner authenticates via api key' , async ( ) => {
396+ mocks . env . DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS = 'true' ;
397+ const userId = 'test-user-id' ;
398+ prisma . user . findUnique . mockResolvedValue ( { ...MOCK_USER_WITH_ACCOUNTS , id : userId } ) ;
399+ prisma . org . findUnique . mockResolvedValue ( { ...MOCK_ORG } ) ;
400+ prisma . userToOrg . findUnique . mockResolvedValue ( {
401+ joinedAt : new Date ( ) ,
402+ userId,
403+ orgId : MOCK_ORG . id ,
404+ role : OrgRole . MEMBER ,
405+ } ) ;
406+ prisma . apiKey . findUnique . mockResolvedValue ( { ...MOCK_API_KEY , hash : 'apikey' , createdById : userId } ) ;
407+ setMockHeaders ( new Headers ( { 'X-Sourcebot-Api-Key' : 'sourcebot-apikey' } ) ) ;
408+
409+ const authContext = await getAuthContext ( ) ;
410+ expect ( authContext ) . toStrictEqual ( {
411+ statusCode : StatusCodes . FORBIDDEN ,
412+ errorCode : ErrorCode . API_KEY_USAGE_DISABLED ,
413+ message : 'API key usage is disabled for non-admin users.' ,
414+ } ) ;
415+ } ) ;
416+
417+ test ( 'should allow an owner to authenticate via api key when flag is enabled' , async ( ) => {
418+ mocks . env . DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS = 'true' ;
419+ const userId = 'test-user-id' ;
420+ prisma . user . findUnique . mockResolvedValue ( { ...MOCK_USER_WITH_ACCOUNTS , id : userId } ) ;
421+ prisma . org . findUnique . mockResolvedValue ( { ...MOCK_ORG } ) ;
422+ prisma . userToOrg . findUnique . mockResolvedValue ( {
423+ joinedAt : new Date ( ) ,
424+ userId,
425+ orgId : MOCK_ORG . id ,
426+ role : OrgRole . OWNER ,
427+ } ) ;
428+ prisma . apiKey . findUnique . mockResolvedValue ( { ...MOCK_API_KEY , hash : 'apikey' , createdById : userId } ) ;
429+ setMockHeaders ( new Headers ( { 'X-Sourcebot-Api-Key' : 'sourcebot-apikey' } ) ) ;
430+
431+ const authContext = await getAuthContext ( ) ;
432+ expect ( authContext ) . toStrictEqual ( {
433+ user : { ...MOCK_USER_WITH_ACCOUNTS , id : userId } ,
434+ org : MOCK_ORG ,
435+ role : OrgRole . OWNER ,
436+ prisma : undefined ,
437+ } ) ;
438+ } ) ;
439+
440+ test ( 'should allow a non-owner to authenticate via session when flag is enabled' , async ( ) => {
441+ mocks . env . DISABLE_API_KEY_USAGE_FOR_NON_OWNER_USERS = 'true' ;
442+ const userId = 'test-user-id' ;
443+ prisma . user . findUnique . mockResolvedValue ( { ...MOCK_USER_WITH_ACCOUNTS , id : userId } ) ;
444+ prisma . org . findUnique . mockResolvedValue ( { ...MOCK_ORG } ) ;
445+ prisma . userToOrg . findUnique . mockResolvedValue ( {
446+ joinedAt : new Date ( ) ,
447+ userId,
448+ orgId : MOCK_ORG . id ,
449+ role : OrgRole . MEMBER ,
450+ } ) ;
451+ setMockSession ( createMockSession ( { user : { id : userId } } ) ) ;
452+
453+ const authContext = await getAuthContext ( ) ;
454+ expect ( authContext ) . toStrictEqual ( {
455+ user : { ...MOCK_USER_WITH_ACCOUNTS , id : userId } ,
456+ org : MOCK_ORG ,
457+ role : OrgRole . MEMBER ,
458+ prisma : undefined ,
459+ } ) ;
460+ } ) ;
461+ } ) ;
383462} ) ;
384463
385464describe ( 'withAuthV2' , ( ) => {
0 commit comments