fix(auth): make diagnostics bounded and portable

This commit is contained in:
2026-08-17 12:19:19 +02:00
parent 7cfbee36fa
commit be1724890a
23 changed files with 1088 additions and 116 deletions
+37 -3
View File
@@ -21,6 +21,7 @@ import type {
Role,
} from "./types.js";
import { parseConfiguredTransportUrl } from "./url-policy.js";
import { createWindowsAuthStorageBridge, type WindowsAuthStorageBridge } from "./windows-auth-storage.js";
export type {
AuthenticationConfig,
@@ -32,6 +33,8 @@ export type {
} from "./types.js";
const MAX_AUTH_CONFIG_BYTES = 1024 * 1024;
// Keep live catalog work within the same deterministic bound as the mandatory direct groups claim.
const MAX_MAPPED_GROUPS = 128;
const ROLES = ["user", "admin"] as const;
export const PERMISSION_CATALOG: readonly Permission[] = [
"session.use", "session.read_all", "session.manage_all", "settings.manage",
@@ -52,7 +55,8 @@ const sessionSchema = z.strictObject({
});
const roleSchema = z.enum(ROLES);
const groupNameSchema = nonEmptyText.max(256);
const groupRolesSchema = z.record(groupNameSchema, z.array(roleSchema).min(1));
const groupRolesSchema = z.record(groupNameSchema, z.array(roleSchema).min(1))
.refine((value) => Object.keys(value).length <= MAX_MAPPED_GROUPS);
const localSchema = z.strictObject({
version: z.literal(1), mode: z.literal("local"), publicUrl: nonEmptyText, session: sessionSchema.optional(),
@@ -94,6 +98,11 @@ interface StorageIdentity {
directory: DirectoryIdentity;
}
export interface AuthenticationConfigLoadOptions {
/** Test seam; production creates the existing bounded internal tht auth-storage bridge. */
windowsStorageBridge?: Pick<WindowsAuthStorageBridge, "readAuthConfig">;
}
function validateCanonicalPath(path: string): void {
if (typeof path !== "string" || path.length === 0 || path.trim() !== path
|| path.includes("\0") || !isAbsolute(path) || normalize(path) !== path
@@ -249,11 +258,36 @@ function loadAuthenticationConfigWithIdentity(path: string): { loaded: LoadedAut
return { loaded: { value, revision: canonicalRevision(value), sourcePath: path }, identity: read.identity };
}
export function loadAuthenticationConfig(path: string): LoadedAuthConfig {
function loadWindowsAuthenticationConfig(
path: string,
bridge: Pick<WindowsAuthStorageBridge, "readAuthConfig">,
): LoadedAuthConfig {
try {
const contents = bridge.readAuthConfig(path);
if (!Buffer.isBuffer(contents) || contents.length === 0 || contents.length > MAX_AUTH_CONFIG_BYTES) throw invalid();
const source = new TextDecoder("utf-8", { fatal: true }).decode(contents);
const value = parseAuthenticationConfig(source);
return { value, revision: canonicalRevision(value), sourcePath: path };
} catch {
throw invalid();
}
}
export function loadAuthenticationConfig(path: string, options: AuthenticationConfigLoadOptions = {}): LoadedAuthConfig {
if (process.platform === "win32") {
return loadWindowsAuthenticationConfig(path, options.windowsStorageBridge ?? createWindowsAuthStorageBridge());
}
return loadAuthenticationConfigWithIdentity(path).loaded;
}
export function createAuthenticationConfigProvider(path: string): AuthenticationConfigProvider {
export function createAuthenticationConfigProvider(
path: string,
options: AuthenticationConfigLoadOptions = {},
): AuthenticationConfigProvider {
if (process.platform === "win32") {
const bridge = options.windowsStorageBridge ?? createWindowsAuthStorageBridge();
return { current: () => loadWindowsAuthenticationConfig(path, bridge) };
}
let cached: { identity: StorageIdentity; loaded: LoadedAuthConfig } | undefined;
return { current(): LoadedAuthConfig {
const before = storageIdentity(path);