11import { describe , expect , test } from 'vitest' ;
22
3- import { splitByScope } from '../authorization' ;
3+ import { createCheckAuthorization , splitByScope } from '../authorization' ;
44import { __experimental_JWTPayloadToAuthObjectProperties as JWTPayloadToAuthObjectProperties } from '../jwtPayloadParser' ;
55
66const baseClaims = {
@@ -14,6 +14,33 @@ const baseClaims = {
1414 __raw : '' ,
1515} ;
1616
17+ const permissionNames = Array . from ( { length : 54 } , ( _ , index ) => `permission_${ index } ` ) ;
18+
19+ const authFromFeaturePermissionMask = ( fpm : string ) => {
20+ const authObject = JWTPayloadToAuthObjectProperties ( {
21+ ...baseClaims ,
22+ v : 2 ,
23+ fea : 'o:feature' ,
24+ o : {
25+ id : 'org_id' ,
26+ rol : 'admin' ,
27+ per : permissionNames . join ( ',' ) ,
28+ fpm,
29+ } ,
30+ } ) ;
31+ const has = createCheckAuthorization ( {
32+ userId : authObject . userId ,
33+ orgId : authObject . orgId ,
34+ orgRole : authObject . orgRole ,
35+ orgPermissions : authObject . orgPermissions ,
36+ factorVerificationAge : authObject . factorVerificationAge ,
37+ features : null ,
38+ plans : null ,
39+ } ) ;
40+
41+ return { authObject, has } ;
42+ } ;
43+
1744describe ( 'JWTPayloadToAuthObjectProperties' , ( ) => {
1845 test ( 'auth object with JWT v2 does not produces anything org related if there is no org active' , ( ) => {
1946 const { sessionClaims : v2Claims , ...signedInAuthObjectV2 } = JWTPayloadToAuthObjectProperties ( {
@@ -75,7 +102,7 @@ describe('JWTPayloadToAuthObjectProperties', () => {
75102 ) ;
76103 } ) ;
77104
78- test ( 'if a feature is not mapped to any permissions it is added as is to the orgPermissions array ' , ( ) => {
105+ test ( 'features without permissions use zero masks to preserve alignment ' , ( ) => {
79106 const { sessionClaims : v2Claims , ...signedInAuthObject } = JWTPayloadToAuthObjectProperties ( {
80107 ...baseClaims ,
81108 v : 2 ,
@@ -85,7 +112,7 @@ describe('JWTPayloadToAuthObjectProperties', () => {
85112 rol : 'admin' ,
86113 slg : 'org_slug' ,
87114 per : 'read,manage' ,
88- fpm : '1,3' ,
115+ fpm : '1,3,0 ' ,
89116 } ,
90117 } ) ;
91118
@@ -153,7 +180,7 @@ describe('JWTPayloadToAuthObjectProperties', () => {
153180 rol : 'admin' ,
154181 slg : 'org_slug' ,
155182 per : 'read,manage' ,
156- fpm : '3' ,
183+ fpm : '3,0 ' ,
157184 } ,
158185 } ) ;
159186
@@ -227,7 +254,7 @@ describe('JWTPayloadToAuthObjectProperties', () => {
227254 rol : 'admin' ,
228255 slg : 'org_slug' ,
229256 per : 'read,manage' ,
230- fpm : '1,2,3' ,
257+ fpm : '1,2,3,0 ' ,
231258 } ,
232259 } ) ;
233260
@@ -246,7 +273,7 @@ describe('JWTPayloadToAuthObjectProperties', () => {
246273 rol : 'admin' ,
247274 slg : 'org_slug' ,
248275 per : 'read,create,update,delete,revoke' ,
249- fpm : '7,21' ,
276+ fpm : '7,21,0 ' ,
250277 } ,
251278 } ) ;
252279
@@ -261,6 +288,89 @@ describe('JWTPayloadToAuthObjectProperties', () => {
261288 ] . sort ( ) ,
262289 ) ;
263290 } ) ;
291+
292+ test ( 'preserves permissions above the safe integer boundary' , ( ) => {
293+ const { authObject, has } = authFromFeaturePermissionMask ( '9007199254740993' ) ;
294+
295+ expect ( authObject . orgPermissions ) . toEqual ( [ 'org:feature:permission_0' , 'org:feature:permission_53' ] ) ;
296+ expect ( has ( { permission : 'org:feature:permission_0' } ) ) . toBe ( true ) ;
297+ expect ( has ( { permission : 'org:feature:permission_53' } ) ) . toBe ( true ) ;
298+ } ) ;
299+
300+ test ( 'does not introduce permissions when decoding a mask above the safe integer boundary' , ( ) => {
301+ const { authObject, has } = authFromFeaturePermissionMask ( '9007199254740995' ) ;
302+
303+ expect ( authObject . orgPermissions ) . toEqual ( [
304+ 'org:feature:permission_0' ,
305+ 'org:feature:permission_1' ,
306+ 'org:feature:permission_53' ,
307+ ] ) ;
308+ expect ( has ( { permission : 'org:feature:permission_0' } ) ) . toBe ( true ) ;
309+ expect ( has ( { permission : 'org:feature:permission_1' } ) ) . toBe ( true ) ;
310+ expect ( has ( { permission : 'org:feature:permission_2' } ) ) . toBe ( false ) ;
311+ expect ( has ( { permission : 'org:feature:permission_53' } ) ) . toBe ( true ) ;
312+ } ) ;
313+
314+ test ( 'discards mask bits outside the declared permission list' , ( ) => {
315+ const { authObject, has } = authFromFeaturePermissionMask ( '18014398509481984' ) ;
316+
317+ expect ( authObject . orgPermissions ) . toEqual ( [ ] ) ;
318+ expect ( has ( { permission : 'org:feature:undefined' } ) ) . toBe ( false ) ;
319+ } ) ;
320+
321+ test ( 'keeps permission masks aligned when a targeted feature follows a feature without permissions' , ( ) => {
322+ const authObject = JWTPayloadToAuthObjectProperties ( {
323+ ...baseClaims ,
324+ v : 2 ,
325+ fea : 'o:repositories,o:impersonation,o:billing' ,
326+ o : {
327+ id : 'org_id' ,
328+ rol : 'admin' ,
329+ per : 'manage,read,update' ,
330+ fpm : '6,0,3' ,
331+ } ,
332+ } ) ;
333+ const has = createCheckAuthorization ( {
334+ userId : authObject . userId ,
335+ orgId : authObject . orgId ,
336+ orgRole : authObject . orgRole ,
337+ orgPermissions : authObject . orgPermissions ,
338+ factorVerificationAge : authObject . factorVerificationAge ,
339+ features : null ,
340+ plans : null ,
341+ } ) ;
342+
343+ expect ( authObject . orgPermissions ) . toEqual ( [
344+ 'org:repositories:read' ,
345+ 'org:repositories:update' ,
346+ 'org:billing:manage' ,
347+ 'org:billing:read' ,
348+ ] ) ;
349+ expect ( has ( { permission : 'org:impersonation:manage' } ) ) . toBe ( false ) ;
350+ expect ( has ( { permission : 'org:billing:manage' } ) ) . toBe ( true ) ;
351+ } ) ;
352+
353+ test . each ( [
354+ [ '1' , [ 0 ] ] ,
355+ [ '3' , [ 0 , 1 ] ] ,
356+ [ '7' , [ 0 , 1 , 2 ] ] ,
357+ [ '21' , [ 0 , 2 , 4 ] ] ,
358+ ] ) ( 'preserves permissions for the safe mask %s' , ( fpm , expectedPermissionIndexes ) => {
359+ const { authObject, has } = authFromFeaturePermissionMask ( fpm ) ;
360+ const expectedPermissions = expectedPermissionIndexes . map ( index => `org:feature:permission_${ index } ` ) ;
361+
362+ expect ( authObject . orgPermissions ) . toEqual ( expectedPermissions ) ;
363+ for ( const permission of expectedPermissions ) {
364+ expect ( has ( { permission } ) ) . toBe ( true ) ;
365+ }
366+ } ) ;
367+
368+ test . each ( [ '1invalid' , '-1' , '1.5' ] ) ( 'fails closed for the malformed mask %s' , fpm => {
369+ const { authObject, has } = authFromFeaturePermissionMask ( fpm ) ;
370+
371+ expect ( authObject . orgPermissions ) . toEqual ( [ ] ) ;
372+ expect ( has ( { permission : 'org:feature:permission_0' } ) ) . toBe ( false ) ;
373+ } ) ;
264374} ) ;
265375
266376describe ( 'splitByScope ' , ( ) => {
0 commit comments