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

374 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
/**
* Red-first owner-authority resolver contract for #1051.
*
* Fixtures are operator-agnostic. The HOMELAB owner name belongs in the local
* estate policy, never in framework source. Anonymous lookup is intentional:
* the ruled owner class is PUBLIC and least-privilege seats may lack read:user.
*/
interface MigrationOwnerResolution {
readonly verdict: 'resolved' | 'refused' | 'not-measured';
readonly reasonCode: string;
readonly principal: {
readonly name: string;
readonly kind: 'durable-human';
} | null;
readonly authority: {
readonly system: 'gitea';
readonly endpoint: string;
readonly contentType: 'application/json';
} | null;
}
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
interface OwnerResolverModule {
resolveProviderDurableOwner(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly host: string;
readonly requestedOwner: string;
},
dependencies: {
readonly fetch: FetchLike;
readonly absentControlName: () => string;
},
): Promise<MigrationOwnerResolution>;
}
const MODULE_PATH = './brain-owner-resolver.js';
async function loadResolver(requirement: string): Promise<OwnerResolverModule> {
try {
return (await import(MODULE_PATH)) as OwnerResolverModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: brain owner resolver is absent (${detail})`);
}
}
function estateRegistry(): string {
return JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
readOnlyControlIdentity: 'read-control',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
});
}
function ownerPolicy(): string {
return JSON.stringify({
version: 1,
estates: [
{
estate: 'homelab',
laneArchiveOwners: [{ kind: 'provider-user', login: 'durable-owner' }],
standingProcess: { kind: 'glpi-queue', queue: 'mosaic-brain-remediation' },
controls: {
publicIdentity: 'public-control',
privateIdentity: 'private-control',
},
},
],
});
}
function jsonResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json; charset=utf-8' },
});
}
function publicUser(login: string, active = false): Response {
return jsonResponse(200, {
id: 42,
login,
visibility: 'public',
active,
});
}
function identityFromUrl(input: string | URL | Request): string {
const value = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
return decodeURIComponent(new URL(value).pathname.split('/').at(-1) ?? '');
}
function controlledFetch(
overrides: Readonly<Record<string, Response>> = {},
calls: Array<{ identity: string; authorization: string | null }> = [],
): FetchLike {
return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const identity = identityFromUrl(input);
const headers = new Headers(init?.headers);
calls.push({ identity, authorization: headers.get('authorization') });
const override = overrides[identity];
if (override !== undefined) return override.clone();
if (identity === 'public-control') return publicUser('public-control');
if (identity === 'private-control' || identity === 'generated-absent-control') {
return jsonResponse(404, { message: 'not found' });
}
if (identity === 'durable-owner') return publicUser('durable-owner', false);
return jsonResponse(404, { message: 'not found' });
};
}
describe('provider-backed durable owner resolver', (): void => {
it('resolves an allowlisted PUBLIC owner by exact login with public/private/absent controls and ignores active=false', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 provider owner resolution');
const calls: Array<{ identity: string; authorization: string | null }> = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch({}, calls),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toEqual({
verdict: 'resolved',
reasonCode: 'owner-verified',
principal: { name: 'user:durable-owner', kind: 'durable-human' },
authority: {
system: 'gitea',
endpoint: 'GET /api/v1/users/durable-owner',
contentType: 'application/json',
},
});
expect(calls.map((call) => call.identity)).toEqual([
'public-control',
'private-control',
'generated-absent-control',
'durable-owner',
]);
expect(calls.every((call) => call.authorization === null)).toBe(true);
});
it('refuses provider redirects and configures a bounded no-redirect request', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner lookup SSRF boundary');
const requests: RequestInit[] = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: async (_input, init): Promise<Response> => {
requests.push(init ?? {});
return new Response(JSON.stringify({ message: 'redirect' }), {
status: 302,
headers: {
'content-type': 'application/json',
location: 'http://127.0.0.1/internal',
},
});
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'not-measured', reasonCode: 'owner-control-invalid' });
expect(requests).toHaveLength(1);
expect(requests[0]?.redirect).toBe('manual');
expect(requests[0]?.signal).toBeInstanceOf(AbortSignal);
});
it('cancels a chunked provider body as soon as it exceeds the byte ceiling', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 bounded owner response stream');
let cancelled = false;
const oversized = new ReadableStream<Uint8Array>({
start(controller): void {
controller.enqueue(new Uint8Array(200_000));
controller.enqueue(new Uint8Array(100_000));
},
cancel(): void {
cancelled = true;
},
});
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: async (): Promise<Response> =>
new Response(oversized, {
status: 200,
headers: { 'content-type': 'application/json' },
}),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({
verdict: 'not-measured',
reasonCode: 'owner-unexpected-provider-shape',
});
expect(cancelled).toBe(true);
});
it('requires the GLPI standing remediation queue in the local estate policy', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 standing process policy');
const raw = JSON.parse(ownerPolicy()) as { estates: Array<Record<string, unknown>> };
delete raw.estates[0]?.['standingProcess'];
let fetchCalls = 0;
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: JSON.stringify(raw),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: async (): Promise<Response> => {
fetchCalls += 1;
return publicUser('durable-owner');
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode: 'owner-policy-invalid' });
expect(fetchCalls).toBe(0);
});
it('rejects a provider-valid but unlisted principal before provider lookup', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 provider-valid unlisted owner refusal');
const calls: Array<{ identity: string; authorization: string | null }> = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:other-public-user',
},
{
fetch: controlledFetch({ 'other-public-user': publicUser('other-public-user') }, calls),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode: 'owner-not-allowlisted' });
expect(calls).toHaveLength(0);
});
it.each([
['user:durableowner', 'owner-name-invalid'],
[' user:durable-owner ', 'owner-name-invalid'],
['user:durable.owner', 'owner-not-allowlisted'],
['user:durable owner', 'owner-name-invalid'],
['user:urable-owner', 'owner-name-invalid'],
['user:be-coder-07@mission-seat', 'owner-name-invalid'],
] as const)(
'rejects non-canonical, unlisted, or transient-seat presentation %s before lookup',
async (name, reasonCode): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner allowlist grammar');
let fetchCalls = 0;
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: name,
},
{
fetch: async (): Promise<Response> => {
fetchCalls += 1;
return publicUser('durable-owner');
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode });
expect(fetchCalls).toBe(0);
},
);
it('fails closed as not-resolvable rather than claiming a private-or-absent owner does not exist', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 private/absent ambiguity');
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch({ 'durable-owner': jsonResponse(404, { message: 'hidden' }) }),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({
verdict: 'not-measured',
reasonCode: 'owner-not-resolvable',
principal: null,
});
expect(JSON.stringify(result)).not.toMatch(/owner-not-found|does-not-exist/);
});
it.each([
['public control hidden', { 'public-control': jsonResponse(404, {}) }],
['public control login mismatch', { 'public-control': publicUser('other') }],
['private control unexpectedly public', { 'private-control': publicUser('private-control') }],
[
'generated absent control unexpectedly resolves',
{ 'generated-absent-control': publicUser('generated-absent-control') },
],
] as const)(
'makes the whole result not-measured when %s',
async (_caseName, overrides): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner resolver controls');
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch(overrides),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({
verdict: 'not-measured',
reasonCode: 'owner-control-invalid',
});
},
);
});