mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
import {
|
|
InvalidProjectPolicyValueError,
|
|
normalizePolicySubject,
|
|
type PolicySubject,
|
|
} from './projectPolicy';
|
|
|
|
export const AUTHENTICATION_ASSURANCE_LEVELS = [
|
|
'single_factor',
|
|
'multi_factor',
|
|
'service',
|
|
'hardware',
|
|
'local_console',
|
|
] as const;
|
|
|
|
export type AuthenticationAssurance =
|
|
(typeof AUTHENTICATION_ASSURANCE_LEVELS)[number];
|
|
|
|
export interface AuthenticatedPrincipal {
|
|
subject: PolicySubject;
|
|
authenticationId: string;
|
|
authenticatedAtMs: number;
|
|
expiresAtMs: number;
|
|
assurance: AuthenticationAssurance;
|
|
}
|
|
|
|
export const MAX_AUTHENTICATION_ID_LENGTH = 128;
|
|
|
|
const AUTHENTICATION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
|
|
function assertTimestamp(name: string, value: number): void {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new InvalidProjectPolicyValueError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
export function normalizeAuthenticatedPrincipal(
|
|
value: AuthenticatedPrincipal,
|
|
): Readonly<AuthenticatedPrincipal> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new InvalidProjectPolicyValueError(
|
|
'authenticated principal must be an object',
|
|
);
|
|
}
|
|
const keys = Object.keys(value).sort();
|
|
const expected = [
|
|
'assurance',
|
|
'authenticatedAtMs',
|
|
'authenticationId',
|
|
'expiresAtMs',
|
|
'subject',
|
|
];
|
|
if (
|
|
keys.length !== expected.length ||
|
|
keys.some((key, index) => key !== expected[index])
|
|
) {
|
|
throw new InvalidProjectPolicyValueError(
|
|
'authenticated principal shape is invalid',
|
|
);
|
|
}
|
|
const subject = normalizePolicySubject(value.subject);
|
|
if (
|
|
typeof value.authenticationId !== 'string' ||
|
|
value.authenticationId.length < 1 ||
|
|
value.authenticationId.length > MAX_AUTHENTICATION_ID_LENGTH ||
|
|
!AUTHENTICATION_ID_PATTERN.test(value.authenticationId)
|
|
) {
|
|
throw new InvalidProjectPolicyValueError('authenticationId is invalid');
|
|
}
|
|
assertTimestamp('authenticatedAtMs', value.authenticatedAtMs);
|
|
assertTimestamp('expiresAtMs', value.expiresAtMs);
|
|
if (value.expiresAtMs <= value.authenticatedAtMs) {
|
|
throw new InvalidProjectPolicyValueError(
|
|
'authenticated principal lifetime is invalid',
|
|
);
|
|
}
|
|
if (!AUTHENTICATION_ASSURANCE_LEVELS.includes(value.assurance)) {
|
|
throw new InvalidProjectPolicyValueError(
|
|
'authentication assurance is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
subject,
|
|
authenticationId: value.authenticationId,
|
|
authenticatedAtMs: value.authenticatedAtMs,
|
|
expiresAtMs: value.expiresAtMs,
|
|
assurance: value.assurance,
|
|
});
|
|
}
|
|
|
|
export function assertAuthenticatedPrincipalActive(
|
|
principal: Readonly<AuthenticatedPrincipal>,
|
|
nowMs: number,
|
|
): void {
|
|
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
throw new InvalidProjectPolicyValueError('current time is invalid');
|
|
}
|
|
if (principal.authenticatedAtMs > nowMs || principal.expiresAtMs <= nowMs) {
|
|
throw new AuthenticatedPrincipalExpiredError();
|
|
}
|
|
}
|
|
|
|
export class AuthenticatedPrincipalExpiredError extends Error {
|
|
readonly code = 'AUTHENTICATED_PRINCIPAL_EXPIRED';
|
|
|
|
constructor() {
|
|
super('Authenticated principal is not active');
|
|
this.name = 'AuthenticatedPrincipalExpiredError';
|
|
}
|
|
}
|