fix(auth): make diagnostics bounded and portable
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user