Files
stack/packages/mosaic/src/commands/brain-owner-resolver.ts
T

281 lines
9.0 KiB
TypeScript

import { z } from 'zod';
import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js';
import type { MigrationOwnerResolution } from './brain-store.js';
const MAX_BODY_BYTES = 256 * 1024;
const LOGIN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/;
const REQUESTED_OWNER = /^user:(.+)$/;
const ownerPolicySchema = z
.object({
version: z.literal(1),
estates: z
.array(
z
.object({
estate: z.string().min(1),
laneArchiveOwners: z
.array(
z
.object({
kind: z.literal('provider-user'),
login: z.string().min(1),
})
.strict(),
)
.min(1),
standingProcess: z
.object({
kind: z.literal('glpi-queue'),
queue: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
})
.strict(),
controls: z
.object({
publicIdentity: z.string().min(1),
privateIdentity: z.string().min(1),
})
.strict(),
})
.strict(),
)
.min(1),
})
.strict();
const providerUserSchema = z
.object({
id: z.number().int(),
login: z.string().min(1),
visibility: z.literal('public'),
})
.passthrough();
export type OwnerFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
function unresolved(reasonCode: string): MigrationOwnerResolution {
return {
verdict: 'not-measured',
reasonCode,
principal: null,
authority: null,
};
}
function refused(reasonCode: string): MigrationOwnerResolution {
return {
verdict: 'refused',
reasonCode,
principal: null,
authority: null,
};
}
function exactCanonicalLogin(value: string): boolean {
return value.normalize('NFKC') === value && LOGIN.test(value);
}
async function boundedJson(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') ?? '';
if (!contentType.toLowerCase().startsWith('application/json')) {
throw new Error('owner-unexpected-content-type');
}
const declared = response.headers.get('content-length');
let declaredSize: number | null = null;
if (declared !== null) {
if (!/^\d+$/.test(declared)) throw new Error('owner-unexpected-provider-shape');
declaredSize = Number.parseInt(declared, 10);
if (!Number.isSafeInteger(declaredSize) || declaredSize > MAX_BODY_BYTES) {
throw new Error('owner-unexpected-provider-shape');
}
}
if (response.body === null) throw new Error('owner-unexpected-provider-shape');
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const next = await reader.read();
if (next.done) break;
total += next.value.byteLength;
if (total > MAX_BODY_BYTES) {
await reader.cancel('owner response exceeds byte ceiling');
throw new Error('owner-unexpected-provider-shape');
}
chunks.push(next.value);
}
if (declaredSize !== null && declaredSize !== total) {
throw new Error('owner-unexpected-provider-shape');
}
const body = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
try {
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body));
} catch {
throw new Error('owner-unexpected-provider-shape');
}
}
async function readPublicIdentity(
origin: string,
identity: string,
fetchImpl: OwnerFetch,
): Promise<{ readonly status: number; readonly user: unknown }> {
let response: Response;
try {
response = await fetchImpl(`${origin}/api/v1/users/${encodeURIComponent(identity)}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'User-Agent': 'mosaic-brain-owner/1',
},
redirect: 'manual',
signal: AbortSignal.timeout(5_000),
});
} catch {
throw new Error('owner-provider-unavailable');
}
return { status: response.status, user: await boundedJson(response) };
}
function publicIdentityMatches(value: unknown, identity: string): boolean {
const parsed = providerUserSchema.safeParse(value);
return parsed.success && parsed.data.login === identity;
}
export interface BrainOwnerPolicyBinding {
readonly brainNamespace: string;
readonly publicControl: string;
readonly privateControl: string;
readonly standingQueue: string;
}
export function resolveBrainOwnerPolicy(
ownerPolicySource: string,
estate: string,
): BrainOwnerPolicyBinding | undefined {
let rawPolicy: unknown;
try {
rawPolicy = JSON.parse(ownerPolicySource);
} catch {
return undefined;
}
const policy = ownerPolicySchema.safeParse(rawPolicy);
if (!policy.success) return undefined;
const estatePolicies = policy.data.estates.filter(
(candidate): boolean => candidate.estate === estate,
);
if (estatePolicies.length !== 1) return undefined;
const estatePolicy = estatePolicies[0];
if (estatePolicy === undefined || estatePolicy.laneArchiveOwners.length !== 1) return undefined;
const brainNamespace = estatePolicy.laneArchiveOwners[0]?.login;
if (brainNamespace === undefined || !exactCanonicalLogin(brainNamespace)) return undefined;
return {
brainNamespace,
publicControl: estatePolicy.controls.publicIdentity,
privateControl: estatePolicy.controls.privateIdentity,
standingQueue: estatePolicy.standingProcess.queue,
};
}
export function parseRequestedOwner(requestedOwner: string): string | null {
if (requestedOwner.normalize('NFKC') !== requestedOwner) return null;
const match = REQUESTED_OWNER.exec(requestedOwner);
const login = match?.[1];
if (login === undefined || !exactCanonicalLogin(login)) return null;
return login;
}
export async function resolveProviderDurableOwner(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly host: string;
readonly requestedOwner: string;
},
dependencies: {
readonly fetch: OwnerFetch;
readonly absentControlName: () => string;
},
): Promise<MigrationOwnerResolution> {
const requestedLogin = parseRequestedOwner(input.requestedOwner);
if (requestedLogin === null) return refused('owner-name-invalid');
const target = parseCredentialEstateRegistry(input.estateRegistrySource).resolveByHost(
input.host,
);
if (target === undefined) return refused('estate-host-unmapped');
const policy = resolveBrainOwnerPolicy(input.ownerPolicySource, target.estate);
if (policy === undefined) return refused('owner-policy-invalid');
if (policy.brainNamespace !== requestedLogin) return refused('owner-not-allowlisted');
const publicControl = policy.publicControl;
const privateControl = policy.privateControl;
const absentControl = dependencies.absentControlName();
if (
!exactCanonicalLogin(publicControl) ||
!exactCanonicalLogin(privateControl) ||
!exactCanonicalLogin(absentControl) ||
new Set([publicControl, privateControl, absentControl, requestedLogin]).size !== 4
) {
return refused('owner-policy-invalid');
}
try {
const publicResult = await readPublicIdentity(
target.host.apiBaseUrl,
publicControl,
dependencies.fetch,
);
if (publicResult.status !== 200 || !publicIdentityMatches(publicResult.user, publicControl)) {
return unresolved('owner-control-invalid');
}
const privateResult = await readPublicIdentity(
target.host.apiBaseUrl,
privateControl,
dependencies.fetch,
);
if (privateResult.status !== 404) return unresolved('owner-control-invalid');
const absentResult = await readPublicIdentity(
target.host.apiBaseUrl,
absentControl,
dependencies.fetch,
);
if (absentResult.status !== 404) return unresolved('owner-control-invalid');
const ownerResult = await readPublicIdentity(
target.host.apiBaseUrl,
requestedLogin,
dependencies.fetch,
);
if (ownerResult.status === 401 || ownerResult.status === 403 || ownerResult.status === 404) {
return unresolved('owner-not-resolvable');
}
if (ownerResult.status !== 200) return unresolved('owner-provider-unavailable');
if (!publicIdentityMatches(ownerResult.user, requestedLogin)) {
return unresolved('owner-provider-identity-mismatch');
}
return {
verdict: 'resolved',
reasonCode: 'owner-verified',
principal: { name: `user:${requestedLogin}`, kind: 'durable-human' },
authority: {
system: 'gitea',
endpoint: `GET /api/v1/users/${requestedLogin}`,
contentType: 'application/json',
},
};
} catch (error: unknown) {
const reason = error instanceof Error ? error.message : 'owner-provider-unavailable';
if (reason === 'owner-unexpected-content-type') return unresolved(reason);
if (reason === 'owner-unexpected-provider-shape') return unresolved(reason);
return unresolved('owner-provider-unavailable');
}
}