fix(auth): pin native auth storage operations

This commit is contained in:
2026-08-17 14:52:21 +02:00
parent 6d438f4c7e
commit cb0e873ed7
23 changed files with 3016 additions and 1385 deletions
+30 -9
View File
@@ -13,6 +13,7 @@ import { parseDocument } from "yaml";
import { z } from "zod";
import { isValidPasswordHash, verifyPassword, verifyWithDummy } from "./password.js";
import type { LoadedAuthConfig, Role } from "./types.js";
import { createWindowsAuthStorageBridge, type WindowsAuthStorageBridge } from "./windows-auth-storage.js";
const MAX_USERS_YAML_BYTES = 1 << 20;
const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._@-]{2,63}$/;
@@ -44,6 +45,11 @@ export interface CurrentLocalUserRegistryResolver {
resolve(loaded: LoadedAuthConfig): LocalUserRegistry | undefined;
}
/** Native Windows obtains protected registry bytes only from the hidden tht bridge. */
export interface LocalUserRegistryOptions {
windowsStorageBridge?: Pick<WindowsAuthStorageBridge, "readLocalUsers">;
}
interface FileIdentity {
dev: number;
ino: number;
@@ -220,10 +226,13 @@ function load(path: string): { records: LocalUserRecord[]; identity: RegistryIde
return { records: parseRegistry(read.source), identity: read.identity };
}
export function createLocalUserRegistry(usersPath: string): LocalUserRegistry {
export function createLocalUserRegistry(usersPath: string, options: LocalUserRegistryOptions = {}): LocalUserRegistry {
let cached: { records: LocalUserRecord[]; identity: RegistryIdentity } | undefined;
const windowsStorage = process.platform === "win32"
? options.windowsStorageBridge ?? createWindowsAuthStorageBridge()
: undefined;
function current(): LocalUserRecord[] {
function currentPosix(): LocalUserRecord[] {
try {
const before = registryIdentity(usersPath);
if (cached && sameIdentity(cached.identity, before)) return cached.records;
@@ -240,22 +249,34 @@ export function createLocalUserRegistry(usersPath: string): LocalUserRegistry {
throw invalid();
}
function operationalRecords(): LocalUserRecord[] {
const records = current();
async function current(): Promise<LocalUserRecord[]> {
if (process.platform !== "win32") return currentPosix();
try {
if (!windowsStorage) throw invalid();
const contents = await windowsStorage.readLocalUsers(usersPath);
if (!Buffer.isBuffer(contents) || contents.length === 0 || contents.length > MAX_USERS_YAML_BYTES) throw invalid();
return parseRegistry(new TextDecoder("utf-8", { fatal: true }).decode(contents));
} catch {
throw invalid();
}
}
async function operationalRecords(): Promise<LocalUserRecord[]> {
const records = await current();
if (!records.some((user) => user.enabled && user.roles.includes("admin"))) throw invalid();
return records;
}
return {
async hasEnabledAdmin(): Promise<boolean> {
return current().some((user) => user.enabled && user.roles.includes("admin"));
return (await current()).some((user) => user.enabled && user.roles.includes("admin"));
},
async findByUsername(username: string): Promise<LocalUserRecord | undefined> {
const normalized = normalizeUsername(username);
return operationalRecords().find((user) => user.normalizedUsername === normalized);
return (await operationalRecords()).find((user) => user.normalizedUsername === normalized);
},
async findBySubject(id: string): Promise<LocalUserRecord | undefined> {
return operationalRecords().find((user) => user.id === id);
return (await operationalRecords()).find((user) => user.id === id);
},
async verify(user: LocalUserRecord | undefined, password: string): Promise<boolean> {
if (!user || !user.enabled) {
@@ -267,14 +288,14 @@ export function createLocalUserRegistry(usersPath: string): LocalUserRegistry {
};
}
export function createCurrentLocalUserRegistryResolver(): CurrentLocalUserRegistryResolver {
export function createCurrentLocalUserRegistryResolver(options: LocalUserRegistryOptions = {}): CurrentLocalUserRegistryResolver {
let current: { usersPath: string; registry: LocalUserRegistry } | undefined;
return {
resolve(loaded: LoadedAuthConfig): LocalUserRegistry | undefined {
if (loaded.value.mode !== "local") return undefined;
const usersPath = join(dirname(loaded.sourcePath), loaded.value.local.usersFile);
if (current?.usersPath === usersPath) return current.registry;
const registry = createLocalUserRegistry(usersPath);
const registry = createLocalUserRegistry(usersPath, options);
current = { usersPath, registry };
return registry;
},