proteum 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import type { Application } from '@server/app/index';
12
12
  import Service from '@server/app/service';
13
13
  import { type TAnyRouter, Request as ServerRequest } from '@server/services/router';
14
14
  import * as AuthErrors from '@common/errors';
15
+ import type { TTraceCaptureMode, TTraceEventType } from '@common/dev/requestTrace';
15
16
 
16
17
  /*----------------------------------
17
18
  - TYPES
@@ -112,8 +113,6 @@ export type TAuthRulesFactory<TUser extends TBasicUser, TRequest extends ServerR
112
113
  - CONFIG
113
114
  ----------------------------------*/
114
115
 
115
- const LogPrefix = '[auth]';
116
-
117
116
  export const UserRoles = ['USER', 'ADMIN', 'TEST', 'DEV'] as const;
118
117
 
119
118
  /*----------------------------------
@@ -161,54 +160,209 @@ export default abstract class AuthService<
161
160
  public login?(request: TRequest, email: string): Promise<unknown>;
162
161
  public abstract decodeSession(jwt: TJwtSession, req: THttpRequest): Promise<TUser | null>;
163
162
 
163
+ private traceRequestById(
164
+ requestId: string | undefined,
165
+ type: TTraceEventType,
166
+ details: Record<string, any>,
167
+ minimumCapture: TTraceCaptureMode = 'summary',
168
+ ) {
169
+ if (!requestId) return;
170
+ this.app.container.Trace.record(requestId, type, details, minimumCapture);
171
+ }
172
+
173
+ private traceRequest(
174
+ request: Pick<TRequest, 'id'> | undefined,
175
+ type: TTraceEventType,
176
+ details: Record<string, any>,
177
+ minimumCapture: TTraceCaptureMode = 'summary',
178
+ ) {
179
+ this.traceRequestById(request?.id, type, details, minimumCapture);
180
+ }
181
+
182
+ private inspectRequestAuth(req: THttpRequest): {
183
+ source: 'header' | 'cookie' | 'none';
184
+ scheme: string | null;
185
+ } {
186
+ const authorizationHeader = typeof req.headers['authorization'] === 'string' ? req.headers['authorization'].trim() : '';
187
+ if (authorizationHeader) {
188
+ const [scheme] = authorizationHeader.split(/\s+/, 1);
189
+ return { source: 'header', scheme: scheme || null };
190
+ }
191
+
192
+ const authorizationCookie =
193
+ 'cookies' in req && typeof req.cookies['authorization'] === 'string' ? req.cookies['authorization'].trim() : '';
194
+ if (authorizationCookie) return { source: 'cookie', scheme: 'Bearer' };
195
+
196
+ return { source: 'none', scheme: null };
197
+ }
198
+
199
+ private describeSessionPayload(session: TJwtSession) {
200
+ if ('apiKey' in session) {
201
+ return {
202
+ payloadKind: 'api-key',
203
+ payloadAccountType: session.accountType ?? null,
204
+ };
205
+ }
206
+
207
+ return {
208
+ payloadKind: 'email',
209
+ payloadAccountType: null,
210
+ };
211
+ }
212
+
213
+ private describeTraceError(error: Error) {
214
+ const details = error as Error & {
215
+ action?: string;
216
+ feature?: string;
217
+ http?: number;
218
+ title?: string;
219
+ };
220
+
221
+ return {
222
+ name: error.name,
223
+ message: error.message,
224
+ http: typeof details.http === 'number' ? details.http : null,
225
+ title: typeof details.title === 'string' ? details.title : null,
226
+ feature: typeof details.feature === 'string' ? details.feature : null,
227
+ action: typeof details.action === 'string' ? details.action : null,
228
+ };
229
+ }
230
+
164
231
  // https://beeceptor.com/docs/concepts/authorization-header/#examples
165
- public async decode(req: THttpRequest, withData: true): Promise<TUser | null>;
166
- public async decode(req: THttpRequest, withData?: false): Promise<TJwtSession | null>;
167
- public async decode(req: THttpRequest, withData: boolean = false): Promise<TJwtSession | TUser | null> {
168
- const requestCookies = 'cookies' in req ? req.cookies : undefined;
169
- this.config.debug && console.log(LogPrefix, 'Decode:', { cookie: requestCookies?.['authorization'] });
232
+ public async decode(req: THttpRequest, withData: true, traceRequestId?: string): Promise<TUser | null>;
233
+ public async decode(req: THttpRequest, withData?: false, traceRequestId?: string): Promise<TJwtSession | null>;
234
+ public async decode(
235
+ req: THttpRequest,
236
+ withData: boolean = false,
237
+ traceRequestId?: string,
238
+ ): Promise<TJwtSession | TUser | null> {
239
+ const authInput = this.inspectRequestAuth(req);
240
+
241
+ this.traceRequestById(
242
+ traceRequestId,
243
+ 'auth.decode',
244
+ {
245
+ phase: 'start',
246
+ withData,
247
+ source: authInput.source,
248
+ scheme: authInput.scheme,
249
+ },
250
+ 'resolve',
251
+ );
170
252
 
171
253
  // Get auth token
172
254
  const authMethod = this.getAuthMethod(req);
173
- if (authMethod === null) return null;
174
- const { tokenType, token } = authMethod;
255
+ if (authMethod === null) {
256
+ this.traceRequestById(
257
+ traceRequestId,
258
+ 'auth.decode',
259
+ {
260
+ phase: 'result',
261
+ withData,
262
+ source: authInput.source,
263
+ scheme: authInput.scheme,
264
+ outcome: authInput.source === 'none' ? 'anonymous' : 'malformed-credentials',
265
+ },
266
+ 'resolve',
267
+ );
268
+ return null;
269
+ }
270
+ const { tokenType, token, source } = authMethod;
175
271
 
176
272
  // Get auth session
177
- const session = this.getAuthSession(tokenType, token);
178
- if (session === null) return null;
273
+ const session = this.getAuthSession(tokenType, token, traceRequestId);
274
+ if (session === null) {
275
+ this.traceRequestById(
276
+ traceRequestId,
277
+ 'auth.decode',
278
+ {
279
+ phase: 'result',
280
+ withData,
281
+ source,
282
+ scheme: tokenType ?? authInput.scheme,
283
+ outcome: 'rejected',
284
+ },
285
+ 'resolve',
286
+ );
287
+ return null;
288
+ }
289
+
290
+ const payload = this.describeSessionPayload(session);
179
291
 
180
292
  // Return email only
181
293
  if (!withData) {
182
- this.config.debug && console.log(LogPrefix, `Auth user successfull. Return email only`);
294
+ this.traceRequestById(
295
+ traceRequestId,
296
+ 'auth.decode',
297
+ {
298
+ phase: 'result',
299
+ withData,
300
+ source,
301
+ scheme: tokenType ?? authInput.scheme,
302
+ outcome: 'session',
303
+ ...payload,
304
+ },
305
+ 'resolve',
306
+ );
183
307
  return session;
184
308
  }
185
309
 
186
310
  // Deserialize full user data
187
- this.config.debug && console.log(LogPrefix, `Deserialize user`, session);
188
311
  const user = await this.decodeSession(session, req);
189
- if (user === null) return null;
312
+ if (user === null) {
313
+ this.traceRequestById(
314
+ traceRequestId,
315
+ 'auth.decode',
316
+ {
317
+ phase: 'result',
318
+ withData,
319
+ source,
320
+ scheme: tokenType ?? authInput.scheme,
321
+ outcome: 'user-missing',
322
+ ...payload,
323
+ },
324
+ 'resolve',
325
+ );
326
+ return null;
327
+ }
190
328
 
191
- this.config.debug && console.log(LogPrefix, `Deserialized user:`, user.name);
329
+ this.traceRequestById(
330
+ traceRequestId,
331
+ 'auth.decode',
332
+ {
333
+ phase: 'result',
334
+ withData,
335
+ source,
336
+ scheme: tokenType ?? authInput.scheme,
337
+ outcome: 'user',
338
+ ...payload,
339
+ userType: user.type,
340
+ userRoles: user.roles,
341
+ },
342
+ 'resolve',
343
+ );
192
344
 
193
345
  return { ...user, _token: token };
194
346
  }
195
347
 
196
- private getAuthMethod(req: THttpRequest): null | { token: string; tokenType?: string } {
348
+ private getAuthMethod(req: THttpRequest): null | { source: 'header' | 'cookie'; token: string; tokenType?: string } {
197
349
  let token: string | undefined;
198
350
  let tokenType: string | undefined;
199
351
  if (typeof req.headers['authorization'] === 'string') {
200
352
  [tokenType, token] = req.headers['authorization'].split(' ');
353
+ if (token === undefined) return null;
354
+ return { source: 'header', tokenType, token };
201
355
  } else if ('cookies' in req && typeof req.cookies['authorization'] === 'string') {
202
356
  token = req.cookies['authorization'];
203
357
  tokenType = 'Bearer';
358
+ if (token === undefined) return null;
359
+ return { source: 'cookie', tokenType, token };
204
360
  } else return null;
205
361
 
206
- if (token === undefined) return null;
207
-
208
- return { tokenType, token };
362
+ return null;
209
363
  }
210
364
 
211
- private getAuthSession(tokenType: string | undefined, token: string): TJwtSession | null {
365
+ private getAuthSession(tokenType: string | undefined, token: string, traceRequestId?: string): TJwtSession | null {
212
366
  let session: TJwtSession;
213
367
 
214
368
  // API Key
@@ -216,45 +370,103 @@ export default abstract class AuthService<
216
370
  const [accountType] = token.split('-');
217
371
  const apiKeySession = { accountType, apiKey: token } satisfies TApiKeySession;
218
372
 
219
- this.config.debug && console.log(LogPrefix, `Auth via API Key`, token);
220
373
  session = apiKeySession as TJwtSession & TApiKeySession;
374
+ this.traceRequestById(
375
+ traceRequestId,
376
+ 'auth.decode',
377
+ {
378
+ phase: 'session',
379
+ scheme: tokenType,
380
+ outcome: 'accepted',
381
+ payloadKind: 'api-key',
382
+ payloadAccountType: accountType || null,
383
+ },
384
+ 'resolve',
385
+ );
221
386
 
222
387
  // JWT
223
388
  } else if (tokenType === 'Bearer') {
224
- this.config.debug && console.log(LogPrefix, `Auth via JWT token`, token);
225
389
  try {
226
390
  session = jwt.verify(token, this.config.jwt.key, {
227
391
  maxAge: this.config.jwt.expiration,
228
392
  }) as TJwtSession;
393
+ this.traceRequestById(
394
+ traceRequestId,
395
+ 'auth.decode',
396
+ {
397
+ phase: 'session',
398
+ scheme: tokenType,
399
+ outcome: 'accepted',
400
+ ...this.describeSessionPayload(session),
401
+ },
402
+ 'resolve',
403
+ );
229
404
  } catch (error) {
230
- console.warn(LogPrefix, 'Failed to decode jwt token:', token);
405
+ this.traceRequestById(
406
+ traceRequestId,
407
+ 'auth.decode',
408
+ {
409
+ phase: 'session',
410
+ scheme: tokenType,
411
+ outcome: 'invalid-bearer',
412
+ error:
413
+ error instanceof Error
414
+ ? this.describeTraceError(error)
415
+ : this.describeTraceError(
416
+ new Error(typeof error === 'string' ? error : 'Invalid bearer token'),
417
+ ),
418
+ },
419
+ 'resolve',
420
+ );
231
421
  return null;
232
422
  //throw new Forbidden(`The JWT token provided in the Authorization header is invalid`);
233
423
  }
234
- } else return null;
424
+ } else {
425
+ this.traceRequestById(
426
+ traceRequestId,
427
+ 'auth.decode',
428
+ {
429
+ phase: 'session',
430
+ scheme: tokenType ?? null,
431
+ outcome: 'unsupported-scheme',
432
+ },
433
+ 'resolve',
434
+ );
435
+ return null;
436
+ }
235
437
  //throw new InputError(`The authorization scheme provided in the Authorization header is unsupported.`);
236
438
 
237
439
  return session;
238
440
  }
239
441
 
240
442
  public createSession(session: TJwtSession, request2: TRequest): string {
241
- this.config.debug && console.info(LogPrefix, `Creating new session:`, session);
242
-
243
443
  const token = jwt.sign(session, this.config.jwt.key);
244
444
 
245
- this.config.debug && console.info(LogPrefix, `Generated JWT token for session:` + token);
246
-
247
445
  request2.res.cookie('authorization', token, { maxAge: this.config.jwt.expiration });
248
446
 
447
+ this.traceRequest(
448
+ request2,
449
+ 'auth.session',
450
+ {
451
+ action: 'create',
452
+ ...this.describeSessionPayload(session),
453
+ maxAgeMs: this.config.jwt.expiration,
454
+ },
455
+ 'resolve',
456
+ );
457
+
249
458
  return token;
250
459
  }
251
460
 
252
461
  public logout(request: TRequest) {
253
462
  const user = request.user;
254
- if (!user) return;
463
+ if (!user) {
464
+ this.traceRequest(request, 'auth.session', { action: 'clear-noop', userPresent: false }, 'summary');
465
+ return;
466
+ }
255
467
 
256
- this.config.debug && console.info(LogPrefix, `Logout ${user.name}`);
257
468
  request.res.clearCookie('authorization');
469
+ this.traceRequest(request, 'auth.session', { action: 'clear', userPresent: true }, 'summary');
258
470
  }
259
471
 
260
472
  protected getDecodedUser(request: TRequest): TUser | null {
@@ -322,17 +534,65 @@ export default abstract class AuthService<
322
534
  }
323
535
 
324
536
  private invokeConfiguredRule<TRuleName extends Extract<keyof TAuthConfiguredRules, string>>(
537
+ request: TRequest,
325
538
  ruleName: TRuleName,
326
539
  rule: TAuthConfiguredRules[TRuleName],
327
540
  input: TAuthCheckConditions[TRuleName],
328
541
  ): TAuthRuleOutcome {
329
- if (typeof rule !== 'function') throw new AuthErrors.InputError(`Unknown auth rule "${ruleName}".`);
542
+ if (typeof rule !== 'function') {
543
+ const error = new AuthErrors.InputError(`Unknown auth rule "${ruleName}".`);
544
+ this.traceRequest(
545
+ request,
546
+ 'auth.check.rule',
547
+ {
548
+ rule: ruleName,
549
+ input,
550
+ result: 'configuration-error',
551
+ error: this.describeTraceError(error),
552
+ },
553
+ 'resolve',
554
+ );
555
+ throw error;
556
+ }
330
557
 
331
- const callable = rule as Function;
558
+ try {
559
+ const callable = rule as Function;
560
+ const outcome =
561
+ callable.length === 0
562
+ ? (callable() as TAuthRuleOutcome)
563
+ : Array.isArray(input)
564
+ ? (Reflect.apply(callable, undefined, input) as TAuthRuleOutcome)
565
+ : (Reflect.apply(callable, undefined, [input]) as TAuthRuleOutcome);
332
566
 
333
- if (callable.length === 0) return callable() as TAuthRuleOutcome;
334
- if (Array.isArray(input)) return Reflect.apply(callable, undefined, input) as TAuthRuleOutcome;
335
- return Reflect.apply(callable, undefined, [input]) as TAuthRuleOutcome;
567
+ this.traceRequest(
568
+ request,
569
+ 'auth.check.rule',
570
+ {
571
+ rule: ruleName,
572
+ input,
573
+ result: outcome === true ? 'allow' : outcome === false ? 'deny' : 'error',
574
+ error: outcome instanceof Error ? this.describeTraceError(outcome) : null,
575
+ },
576
+ 'resolve',
577
+ );
578
+
579
+ return outcome;
580
+ } catch (error) {
581
+ const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown auth rule error');
582
+
583
+ this.traceRequest(
584
+ request,
585
+ 'auth.check.rule',
586
+ {
587
+ rule: ruleName,
588
+ input,
589
+ result: 'threw',
590
+ error: this.describeTraceError(typedError),
591
+ },
592
+ 'resolve',
593
+ );
594
+ throw error;
595
+ }
336
596
  }
337
597
 
338
598
  private checkWithConditions(
@@ -341,34 +601,163 @@ export default abstract class AuthService<
341
601
  tracking: TAuthTrackingContext,
342
602
  ): TUser | null {
343
603
  const user = this.getDecodedUser(request);
604
+ const conditionRuleNames =
605
+ conditions && conditions !== false
606
+ ? (Object.keys(conditions) as Array<Extract<keyof TAuthConfiguredRules, string>>)
607
+ : [];
608
+
609
+ this.traceRequest(
610
+ request,
611
+ 'auth.check.start',
612
+ {
613
+ phase: 'evaluate',
614
+ strategy: 'conditions',
615
+ evaluationMode: conditions === false ? 'guest-only' : conditions === null ? 'authenticated' : 'rules',
616
+ userPresent: user !== null,
617
+ userRoles: user?.roles || [],
618
+ ruleNames: conditionRuleNames,
619
+ tracking,
620
+ },
621
+ 'resolve',
622
+ );
344
623
 
345
- this.config.debug && console.warn(LogPrefix, `Check auth with rules. Current user =`, user?.name, conditions);
346
-
347
- if (conditions === false) return user;
624
+ if (conditions === false) {
625
+ this.traceRequest(
626
+ request,
627
+ 'auth.check.result',
628
+ {
629
+ strategy: 'conditions',
630
+ evaluationMode: 'guest-only',
631
+ outcome: 'guest-pass',
632
+ userPresent: user !== null,
633
+ },
634
+ 'resolve',
635
+ );
636
+ return user;
637
+ }
348
638
 
349
639
  if (user === null) {
350
- console.warn(LogPrefix, 'Refusé pour anonyme (' + request.ip + ')');
351
- throw this.buildUnauthenticatedError(request, tracking);
640
+ const error = this.buildUnauthenticatedError(request, tracking);
641
+ this.traceRequest(
642
+ request,
643
+ 'auth.check.result',
644
+ {
645
+ strategy: 'conditions',
646
+ evaluationMode: conditions === null ? 'authenticated' : 'rules',
647
+ outcome: 'unauthenticated',
648
+ ruleNames: conditionRuleNames,
649
+ tracking,
650
+ error: this.describeTraceError(error),
651
+ },
652
+ 'resolve',
653
+ );
654
+ throw error;
352
655
  }
353
656
 
354
- if (!conditions) return user;
657
+ if (!conditions) {
658
+ this.traceRequest(
659
+ request,
660
+ 'auth.check.result',
661
+ {
662
+ strategy: 'conditions',
663
+ evaluationMode: 'authenticated',
664
+ outcome: 'allowed',
665
+ userRoles: user.roles,
666
+ },
667
+ 'resolve',
668
+ );
669
+ return user;
670
+ }
355
671
 
356
- if (!this.config.rules) throw new AuthErrors.InputError(`Auth rules are not configured for this application.`);
672
+ if (!this.config.rules) {
673
+ const error = new AuthErrors.InputError(`Auth rules are not configured for this application.`);
674
+ this.traceRequest(
675
+ request,
676
+ 'auth.check.result',
677
+ {
678
+ strategy: 'conditions',
679
+ evaluationMode: 'rules',
680
+ outcome: 'configuration-error',
681
+ ruleNames: conditionRuleNames,
682
+ error: this.describeTraceError(error),
683
+ },
684
+ 'resolve',
685
+ );
686
+ throw error;
687
+ }
357
688
 
358
689
  const rules = this.config.rules(user, tracking, request);
359
- const conditionRuleNames = Object.keys(conditions) as Array<Extract<keyof TAuthConfiguredRules, string>>;
690
+ const configuredRuleNames = Object.keys(rules) as Array<Extract<keyof TAuthConfiguredRules, string>>;
691
+
692
+ this.traceRequest(
693
+ request,
694
+ 'auth.check.start',
695
+ {
696
+ phase: 'rules-ready',
697
+ strategy: 'conditions',
698
+ evaluationMode: 'rules',
699
+ userRoles: user.roles,
700
+ ruleNames: conditionRuleNames,
701
+ configuredRuleNames,
702
+ tracking,
703
+ },
704
+ 'resolve',
705
+ );
360
706
 
361
707
  for (const ruleName of conditionRuleNames) {
362
708
  const input = conditions[ruleName];
363
709
  if (input === undefined) continue;
364
710
 
365
- const outcome = this.invokeConfiguredRule(ruleName, rules[ruleName], input);
711
+ const outcome = this.invokeConfiguredRule(request, ruleName, rules[ruleName], input);
366
712
  if (outcome === true) continue;
367
- if (outcome === false)
368
- throw new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
713
+ if (outcome === false) {
714
+ const error = new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
715
+ this.traceRequest(
716
+ request,
717
+ 'auth.check.result',
718
+ {
719
+ strategy: 'conditions',
720
+ evaluationMode: 'rules',
721
+ outcome: 'forbidden',
722
+ failedRule: ruleName,
723
+ ruleNames: conditionRuleNames,
724
+ userRoles: user.roles,
725
+ error: this.describeTraceError(error),
726
+ },
727
+ 'resolve',
728
+ );
729
+ throw error;
730
+ }
731
+
732
+ this.traceRequest(
733
+ request,
734
+ 'auth.check.result',
735
+ {
736
+ strategy: 'conditions',
737
+ evaluationMode: 'rules',
738
+ outcome: 'error',
739
+ failedRule: ruleName,
740
+ ruleNames: conditionRuleNames,
741
+ userRoles: user.roles,
742
+ error: this.describeTraceError(outcome),
743
+ },
744
+ 'resolve',
745
+ );
369
746
  throw outcome;
370
747
  }
371
748
 
749
+ this.traceRequest(
750
+ request,
751
+ 'auth.check.result',
752
+ {
753
+ strategy: 'conditions',
754
+ evaluationMode: 'rules',
755
+ outcome: 'allowed',
756
+ ruleNames: conditionRuleNames,
757
+ userRoles: user.roles,
758
+ },
759
+ 'resolve',
760
+ );
372
761
  return user;
373
762
  }
374
763
 
@@ -381,37 +770,86 @@ export default abstract class AuthService<
381
770
  const normalizedRole = role === true ? 'USER' : role;
382
771
  const user = this.getDecodedUser(request);
383
772
 
384
- this.config.debug &&
385
- console.warn(LogPrefix, `Check auth, role = ${normalizedRole}. Current user =`, user?.name, feature);
773
+ this.traceRequest(
774
+ request,
775
+ 'auth.check.start',
776
+ {
777
+ phase: 'evaluate',
778
+ strategy: 'legacy-role',
779
+ normalizedRole,
780
+ feature: feature ?? null,
781
+ action: action ?? null,
782
+ userPresent: user !== null,
783
+ userRoles: user?.roles || [],
784
+ },
785
+ 'resolve',
786
+ );
386
787
 
387
788
  if (normalizedRole === false) {
789
+ this.traceRequest(
790
+ request,
791
+ 'auth.check.result',
792
+ {
793
+ strategy: 'legacy-role',
794
+ outcome: 'guest-pass',
795
+ normalizedRole,
796
+ userPresent: user !== null,
797
+ },
798
+ 'resolve',
799
+ );
388
800
  return user as TUser;
389
801
 
390
802
  // Not connected
391
803
  } else if (user === null) {
392
- console.warn(LogPrefix, 'Refusé pour anonyme (' + request.ip + ')');
393
- throw new AuthErrors.AuthRequired(
804
+ const error = new AuthErrors.AuthRequired(
394
805
  'Please login to continue',
395
806
  feature && feature !== null ? feature : ('auth' as FeatureKeys),
396
807
  action || 'view',
397
808
  );
809
+ this.traceRequest(
810
+ request,
811
+ 'auth.check.result',
812
+ {
813
+ strategy: 'legacy-role',
814
+ outcome: 'unauthenticated',
815
+ normalizedRole,
816
+ feature: feature ?? null,
817
+ action: action || 'view',
818
+ error: this.describeTraceError(error),
819
+ },
820
+ 'resolve',
821
+ );
822
+ throw error;
398
823
 
399
824
  // Insufficient permissions
400
825
  } else if (!user.roles.includes(normalizedRole)) {
401
- console.warn(
402
- LogPrefix,
403
- 'Refusé: ' + normalizedRole + ' pour ' + user.name + ' (' + (user.roles || 'role inconnu') + ')',
826
+ const error = new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
827
+ this.traceRequest(
828
+ request,
829
+ 'auth.check.result',
830
+ {
831
+ strategy: 'legacy-role',
832
+ outcome: 'forbidden',
833
+ normalizedRole,
834
+ userRoles: user.roles,
835
+ error: this.describeTraceError(error),
836
+ },
837
+ 'resolve',
404
838
  );
405
-
406
- throw new AuthErrors.Forbidden('You do not have sufficient permissions to access this resource.');
407
- } else {
408
- this.config.debug &&
409
- console.warn(
410
- LogPrefix,
411
- 'Autorisé ' + normalizedRole + ' pour ' + user.name + ' (' + user.roles + ')',
412
- );
839
+ throw error;
413
840
  }
414
841
 
842
+ this.traceRequest(
843
+ request,
844
+ 'auth.check.result',
845
+ {
846
+ strategy: 'legacy-role',
847
+ outcome: 'allowed',
848
+ normalizedRole,
849
+ userRoles: user.roles,
850
+ },
851
+ 'resolve',
852
+ );
415
853
  return user as TUser;
416
854
  }
417
855
 
@@ -433,6 +871,32 @@ export default abstract class AuthService<
433
871
  featureOrTracking?: FeatureKeys | null | TAuthTrackingContext,
434
872
  action?: string,
435
873
  ): TUser | null {
874
+ const dispatch =
875
+ roleOrConditions === null
876
+ ? 'authenticated'
877
+ : this.isCheckConditions(roleOrConditions)
878
+ ? 'conditions'
879
+ : roleOrConditions === false &&
880
+ (featureOrTracking === undefined || featureOrTracking === null || typeof featureOrTracking === 'object')
881
+ ? 'guest-only'
882
+ : (roleOrConditions === true || typeof roleOrConditions === 'string') && this.config.rules
883
+ ? 'role-via-rules'
884
+ : 'legacy-role';
885
+
886
+ this.traceRequest(
887
+ request,
888
+ 'auth.check.start',
889
+ {
890
+ phase: 'dispatch',
891
+ dispatch,
892
+ roleOrConditions,
893
+ featureOrTracking: featureOrTracking ?? null,
894
+ action: action ?? null,
895
+ hasConfiguredRules: Boolean(this.config.rules),
896
+ },
897
+ 'resolve',
898
+ );
899
+
436
900
  if (roleOrConditions === null || this.isCheckConditions(roleOrConditions)) {
437
901
  const tracking = (featureOrTracking ?? null) as TAuthTrackingContext;
438
902
  return this.checkWithConditions(request, roleOrConditions, tracking);