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.
- package/README.md +25 -6
- package/agents/framework/AGENTS.md +14 -1
- package/agents/project/AGENTS.md +3 -0
- package/cli/commands/create.ts +5 -0
- package/cli/commands/dev.ts +2 -1
- package/cli/commands/init.ts +2 -94
- package/cli/index.ts +1 -4
- package/cli/presentation/commands.ts +45 -9
- package/cli/presentation/devSession.ts +17 -3
- package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
- package/cli/runtime/commands.ts +61 -3
- package/cli/scaffold/index.ts +720 -0
- package/cli/scaffold/templates.ts +344 -0
- package/cli/scaffold/types.ts +26 -0
- package/client/dev/profiler/index.tsx +1230 -230
- package/common/dev/profiler.ts +1 -0
- package/common/dev/requestTrace.ts +6 -0
- package/docs/dev-commands.md +7 -0
- package/docs/diagnostics.md +88 -0
- package/docs/request-tracing.md +10 -0
- package/eslint.js +11 -6
- package/package.json +3 -2
- package/server/app/index.ts +2 -2
- package/server/index.ts +0 -1
- package/server/services/auth/index.ts +525 -61
- package/server/services/auth/router/index.ts +106 -7
- package/server/services/router/http/index.ts +22 -6
|
@@ -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(
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
174
|
-
|
|
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)
|
|
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.
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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')
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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)
|
|
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)
|
|
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
|
|
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
|
-
|
|
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.
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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);
|