fix(auth): pin native auth storage operations
This commit is contained in:
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user