@@ -18,33 +18,34 @@ async function seedUser(prisma: PrismaClient, email: string, admin = false) {
1818 } ) ;
1919}
2020
21+ async function uat ( userId : string ) {
22+ return signUserActorToken ( SECRET , { userId, client : "test" } ) ;
23+ }
24+
2125postgresTest (
22- "authenticateUserActor: member is allowed, non-member is denied " ,
26+ "authenticateUserActor: scoped membership floor " ,
2327 async ( { prisma } ) => {
24- const org = await prisma . organization . create ( {
28+ const p = prisma as PrismaClient ;
29+ const org = await p . organization . create ( {
2530 data : { slug : `org-${ Date . now ( ) } ` , title : "Org" } ,
2631 } ) ;
27- const member = await seedUser ( prisma as PrismaClient , "member@example.test" ) ;
28- const stranger = await seedUser ( prisma as PrismaClient , "stranger@example.test" ) ;
29- await prisma . orgMember . create ( {
30- data : { organizationId : org . id , userId : member . id } ,
32+ const project = await p . project . create ( {
33+ data : {
34+ slug : `proj-${ Date . now ( ) } ` ,
35+ name : "Project" ,
36+ externalRef : `ref-${ Date . now ( ) } ` ,
37+ organizationId : org . id ,
38+ } ,
3139 } ) ;
40+ const member = await seedUser ( p , "member@example.test" ) ;
41+ const stranger = await seedUser ( p , "stranger@example.test" ) ;
42+ const admin = await seedUser ( p , "admin@example.test" , true ) ;
43+ await p . orgMember . create ( { data : { organizationId : org . id , userId : member . id } } ) ;
3244
33- const controller = new RoleBaseAccessFallback ( prisma as PrismaClient , {
34- userActorSecret : SECRET ,
35- } ) . create ( ) ;
36-
37- const memberToken = await signUserActorToken ( SECRET , {
38- userId : member . id ,
39- client : "test" ,
40- } ) ;
41- const strangerToken = await signUserActorToken ( SECRET , {
42- userId : stranger . id ,
43- client : "test" ,
44- } ) ;
45+ const controller = new RoleBaseAccessFallback ( p , { userActorSecret : SECRET } ) . create ( ) ;
4546
4647 // Member with a capless token keeps the read:all default.
47- const memberResult = await controller . authenticateUserActor ( uatRequest ( memberToken ) , {
48+ const memberResult = await controller . authenticateUserActor ( uatRequest ( await uat ( member . id ) ) , {
4849 organizationId : org . id ,
4950 } ) ;
5051 expect ( memberResult . ok ) . toBe ( true ) ;
@@ -53,12 +54,48 @@ postgresTest(
5354 }
5455
5556 // Non-member is denied at the ability layer, not handed a usable ability.
56- const strangerResult = await controller . authenticateUserActor ( uatRequest ( strangerToken ) , {
57+ const strangerResult = await controller . authenticateUserActor (
58+ uatRequest ( await uat ( stranger . id ) ) ,
59+ { organizationId : org . id }
60+ ) ;
61+ expect ( strangerResult . ok ) . toBe ( false ) ;
62+ if ( ! strangerResult . ok ) expect ( strangerResult . status ) . toBe ( 403 ) ;
63+
64+ // A token for a user that no longer exists fails closed.
65+ const ghostResult = await controller . authenticateUserActor ( uatRequest ( await uat ( "usr_ghost" ) ) , {
5766 organizationId : org . id ,
5867 } ) ;
59- expect ( strangerResult . ok ) . toBe ( false ) ;
60- if ( ! strangerResult . ok ) {
61- expect ( strangerResult . status ) . toBe ( 403 ) ;
68+ expect ( ghostResult . ok ) . toBe ( false ) ;
69+ if ( ! ghostResult . ok ) expect ( ghostResult . status ) . toBe ( 401 ) ;
70+
71+ // A platform admin is exempt from the membership floor.
72+ const adminResult = await controller . authenticateUserActor ( uatRequest ( await uat ( admin . id ) ) , {
73+ organizationId : org . id ,
74+ } ) ;
75+ expect ( adminResult . ok ) . toBe ( true ) ;
76+
77+ // A project-only scope resolves through the project's org: non-member denied.
78+ const projectResult = await controller . authenticateUserActor (
79+ uatRequest ( await uat ( stranger . id ) ) ,
80+ { projectId : project . id }
81+ ) ;
82+ expect ( projectResult . ok ) . toBe ( false ) ;
83+ if ( ! projectResult . ok ) expect ( projectResult . status ) . toBe ( 403 ) ;
84+ } ,
85+ 120_000
86+ ) ;
87+
88+ postgresTest (
89+ "authenticateUserActor: unscoped context skips the floor and never queries the user" ,
90+ async ( { prisma } ) => {
91+ const p = prisma as PrismaClient ;
92+ const stranger = await seedUser ( p , "unscoped@example.test" ) ;
93+ const controller = new RoleBaseAccessFallback ( p , { userActorSecret : SECRET } ) . create ( ) ;
94+
95+ const result = await controller . authenticateUserActor ( uatRequest ( await uat ( stranger . id ) ) , { } ) ;
96+ expect ( result . ok ) . toBe ( true ) ;
97+ if ( result . ok ) {
98+ expect ( result . ability . can ( "read" , { type : "runs" , id : "run_x" } ) ) . toBe ( true ) ;
6299 }
63100 } ,
64101 120_000
0 commit comments