33 BadRequestException ,
44 Body ,
55 Controller ,
6+ HttpCode ,
67 HttpException ,
78 Logger ,
89 Post ,
@@ -57,8 +58,9 @@ export class PermissionCheckController {
5758 description : 'List of user ids and one target document to evaluate.' ,
5859 type : PermissionCheckRequestDto ,
5960 } )
61+ @HttpCode ( 200 )
6062 @ApiResponse ( {
61- status : 201 ,
63+ status : 200 ,
6264 description : 'Permission result for each user id.' ,
6365 schema : {
6466 type : 'object' ,
@@ -84,12 +86,28 @@ export class PermissionCheckController {
8486 } )
8587 @ApiUnauthorizedResponse ( { description : 'Authentication required.' } )
8688 async checkPermissions ( @Body ( ) body : PermissionCheckRequestDto ) {
89+ this . logPermissionCheckRequest ( body ) ;
90+ this . validatePermissionCheckRequest ( body ) ;
91+
92+ const action : Action = body . action ?? 'read' ;
93+ const entityDoc = await this . loadCanonicalEntityDoc ( body . entityId ) ;
94+
95+ const results = await Promise . all (
96+ body . userIds . map ( ( userId ) => this . evaluatePermissionForUser ( userId , action , entityDoc ) ) ,
97+ ) ;
98+
99+ return Object . fromEntries ( results ) ;
100+ }
101+
102+ private logPermissionCheckRequest ( body : PermissionCheckRequestDto ) {
87103 this . logger . debug (
88104 `Incoming permission check: userCount=${ Array . isArray ( body ?. userIds ) ? body . userIds . length : 0 } , ` +
89105 `entityId=${ body ?. entityId } , action=${ body ?. action ?? 'read' } , ` +
90106 `body keys=${ body ? Object . keys ( body ) : 'null' } ` ,
91107 ) ;
108+ }
92109
110+ private validatePermissionCheckRequest ( body : PermissionCheckRequestDto ) {
93111 if (
94112 ! Array . isArray ( body ?. userIds ) ||
95113 body . userIds . length === 0 ||
@@ -112,55 +130,59 @@ export class PermissionCheckController {
112130 ) {
113131 throw new BadRequestException ( 'action is invalid' ) ;
114132 }
133+ }
115134
116- const action : Action = body . action ?? 'read' ;
117- const entityDoc = await this . loadCanonicalEntityDoc ( body . entityId ) ;
135+ private async evaluatePermissionForUser (
136+ userId : string ,
137+ action : Action ,
138+ entityDoc : unknown ,
139+ ) {
140+ try {
141+ const user = await this . userIdentityService . resolveUser ( userId ) ;
142+ const permitted = await this . permissionService . isAllowedTo (
143+ action ,
144+ entityDoc ,
145+ user ,
146+ 'app' ,
147+ ) ;
118148
119- const results = await Promise . all (
120- body . userIds . map ( async ( userId ) => {
121- try {
122- const user = await this . userIdentityService . resolveUser ( userId ) ;
123- const permitted = await this . permissionService . isAllowedTo (
124- action ,
125- entityDoc ,
126- user ,
127- 'app' ,
128- ) ;
129-
130- return [ userId , { permitted } ] as const ;
131- } catch ( error ) {
132- // Infrastructure failure: Keycloak unreachable or returned a server error → fail the whole batch
133- if (
134- error instanceof AxiosError &&
135- ( ! error . response || error . response . status >= 500 )
136- ) {
137- throw new BadGatewayException (
138- 'Upstream identity provider is unavailable' ,
139- ) ;
140- }
141-
142- // User not found in Keycloak (404) or bad user ID format (400)
143- if (
144- ( error instanceof AxiosError &&
145- error . response ?. status >= 400 &&
146- error . response ?. status < 500 ) ||
147- ( error instanceof HttpException &&
148- error . getStatus ( ) >= 400 &&
149- error . getStatus ( ) < 500 )
150- ) {
151- return [ userId , { permitted : false , error : 'NOT_FOUND' } ] as const ;
152- }
153-
154- this . logger . error (
155- `Failed to evaluate permissions for user ${ userId } ` ,
156- error ?. stack || error ,
157- ) ;
158- return [ userId , { permitted : false , error : 'ERROR' } ] as const ;
159- }
160- } ) ,
149+ return [ userId , { permitted } ] as const ;
150+ } catch ( error ) {
151+ return this . handlePermissionEvaluationError ( userId , error ) ;
152+ }
153+ }
154+
155+ private handlePermissionEvaluationError ( userId : string , error : unknown ) {
156+ // Infrastructure failure: Keycloak unreachable or returned a server error -> fail the whole batch
157+ if (
158+ error instanceof AxiosError &&
159+ ( ! error . response || error . response . status >= 500 )
160+ ) {
161+ throw new BadGatewayException ( 'Upstream identity provider is unavailable' ) ;
162+ }
163+
164+ // User not found in Keycloak (404) or bad user ID format (400)
165+ if ( this . isClientError ( error ) ) {
166+ return [ userId , { permitted : false , error : 'NOT_FOUND' } ] as const ;
167+ }
168+
169+ this . logger . error (
170+ `Failed to evaluate permissions for user ${ userId } ` ,
171+ error instanceof Error ? error . stack || error . message : String ( error ) ,
161172 ) ;
173+ return [ userId , { permitted : false , error : 'ERROR' } ] as const ;
174+ }
162175
163- return Object . fromEntries ( results ) ;
176+ private isClientError ( error : unknown ) {
177+ return (
178+ ( error instanceof AxiosError &&
179+ error . response ?. status !== undefined &&
180+ error . response . status >= 400 &&
181+ error . response . status < 500 ) ||
182+ ( error instanceof HttpException &&
183+ error . getStatus ( ) >= 400 &&
184+ error . getStatus ( ) < 500 )
185+ ) ;
164186 }
165187
166188 private async loadCanonicalEntityDoc ( entityId : string ) {
0 commit comments