unrip/src/core/operator-dashboard-auth.mjs
philipp b8d731408e
All checks were successful
deploy / deploy (push) Successful in 22s
Fix dashboard runtime deploy dependencies
Proof: Include operator dashboard auth module and runtime package manifest updates required for the deployed operator-dashboard and ops-sentinel processes to start successfully.

Assumptions: React and ws belong in production dependencies because operator-dashboard now ships in the runtime image; operator-dashboard-auth is part of the deployed backend surface.

Still fake: External alert receiver remains unconfigured; this commit only fixes rollout blockers for the runtime-health/dashboard deploy.
2026-04-08 19:36:45 +02:00

149 lines
3.8 KiB
JavaScript

import crypto from 'node:crypto';
const DEFAULT_SESSION_COOKIE_NAME = 'operator_dashboard_session';
export function resolveDashboardRequestAuth({
mode = 'stub',
authorizationHeader = '',
cookieHeader = '',
username = 'admin',
password = '',
sessionCookieName = DEFAULT_SESSION_COOKIE_NAME,
} = {}) {
if (mode === 'stub') {
return {
authenticated: true,
subject: 'local-operator',
mode,
roles: ['operator'],
via: 'stub',
setSessionCookie: false,
sessionCookieName,
sessionToken: buildDashboardSessionToken({ username, password }),
};
}
if (mode !== 'basic') {
return {
authenticated: false,
subject: null,
mode,
roles: [],
via: 'unsupported',
failure_reason: 'unsupported_auth_mode',
setSessionCookie: false,
sessionCookieName,
sessionToken: buildDashboardSessionToken({ username, password }),
};
}
const sessionToken = buildDashboardSessionToken({ username, password });
const cookies = parseCookieHeader(cookieHeader);
if (cookies[sessionCookieName] && cookies[sessionCookieName] === sessionToken) {
return {
authenticated: true,
subject: username,
mode,
roles: ['operator'],
via: 'session_cookie',
setSessionCookie: false,
sessionCookieName,
sessionToken,
};
}
const basic = parseBasicAuthorizationHeader(authorizationHeader);
if (!basic) {
return {
authenticated: false,
subject: null,
mode,
roles: [],
via: 'missing',
failure_reason: 'missing_basic_auth',
setSessionCookie: false,
sessionCookieName,
sessionToken,
};
}
if (basic.username !== username || basic.password !== password) {
return {
authenticated: false,
subject: null,
mode,
roles: [],
via: 'basic_auth',
failure_reason: 'invalid_credentials',
setSessionCookie: false,
sessionCookieName,
sessionToken,
};
}
return {
authenticated: true,
subject: username,
mode,
roles: ['operator'],
via: 'basic_auth',
setSessionCookie: cookies[sessionCookieName] !== sessionToken,
sessionCookieName,
sessionToken,
};
}
export function buildDashboardSessionToken({ username = '', password = '' } = {}) {
return crypto
.createHash('sha256')
.update(`${username}:${password}`)
.digest('hex');
}
export function buildDashboardSessionCookie({
sessionCookieName = DEFAULT_SESSION_COOKIE_NAME,
sessionToken,
} = {}) {
return `${sessionCookieName}=${sessionToken}; Path=/; HttpOnly; SameSite=Lax`;
}
export function buildDashboardAuthChallengeHeader({ realm = 'unrip operator dashboard' } = {}) {
return `Basic realm="${realm}"`;
}
export function parseBasicAuthorizationHeader(value = '') {
if (!value || typeof value !== 'string') return null;
const [scheme, token] = value.split(/\s+/, 2);
if (!scheme || !token || scheme.toLowerCase() !== 'basic') return null;
try {
const decoded = Buffer.from(token, 'base64').toString('utf8');
const separatorIndex = decoded.indexOf(':');
if (separatorIndex === -1) return null;
return {
username: decoded.slice(0, separatorIndex),
password: decoded.slice(separatorIndex + 1),
};
} catch {
return null;
}
}
function parseCookieHeader(value = '') {
if (!value || typeof value !== 'string') return {};
return Object.fromEntries(
value
.split(';')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const separatorIndex = part.indexOf('=');
if (separatorIndex === -1) return [part, ''];
return [
part.slice(0, separatorIndex).trim(),
decodeURIComponent(part.slice(separatorIndex + 1).trim()),
];
}),
);
}