Files
stack/packages/mosaic/src/credentials/grant.spec.ts
T

231 lines
7.7 KiB
TypeScript

import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { listCredentialJournals } from './audit-journal.js';
import { grantDirectRepositoryPermission } from './grant.js';
import type { ResolvedCredential } from './credential-provider.dto.js';
import type { GiteaGrantProvider } from './grant.js';
import type { CredentialValidationDependencies } from './validate.js';
let cleanup: string | undefined;
const authority: ResolvedCredential = Object.freeze({
identity: 'provisioner',
estate: 'homelab',
host: 'git.example.invalid',
resolutionId: 'authority',
secret: new TextEncoder().encode('authority-canary'),
});
async function stateRoot(): Promise<string> {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-grant-'));
return join(cleanup, 'state');
}
afterEach(async (): Promise<void> => {
if (cleanup !== undefined) await rm(cleanup, { recursive: true, force: true });
cleanup = undefined;
});
function validationDependencies(permission: 'read' | 'write'): CredentialValidationDependencies {
const subject: ResolvedCredential = Object.freeze({
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
resolutionId: 'subject',
secret: new TextEncoder().encode('subject-canary'),
});
const control: ResolvedCredential = Object.freeze({
identity: 'read-control',
estate: 'homelab',
host: 'git.example.invalid',
resolutionId: 'control',
secret: new TextEncoder().encode('control-canary'),
});
return {
estateRegistry: { matches: (): boolean => true },
resolver: {
async resolve(identity: string): Promise<ResolvedCredential | undefined> {
if (identity === 'seat-name') return subject;
if (identity === 'read-control') return control;
return undefined;
},
},
provider: {
async readIdentity(resolved: ResolvedCredential) {
return {
login: resolved.identity,
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
};
},
async readRepositoryPermission(resolved: ResolvedCredential) {
return {
effective: resolved.identity === 'seat-name' ? permission : 'read',
endpoint: 'GET /api/v1/repos/owner/repo',
contentType: 'application/json',
};
},
async probeReceivePack(resolved: ResolvedCredential | undefined) {
const subjectWrite = resolved?.identity === 'seat-name' && permission === 'write';
return {
state: subjectWrite ? 'advertised' : 'refused',
principal: resolved?.identity ?? null,
resolutionId: resolved?.resolutionId ?? null,
contentType: subjectWrite ? 'application/x-git-receive-pack-advertisement' : 'text/plain',
};
},
},
};
}
describe('direct repository grant', (): void => {
it('opens the journal before mutation and accepts only matching provider read-back', async (): Promise<void> => {
const root = await stateRoot();
const provider: GiteaGrantProvider = {
async readBasicIdentity() {
return {
login: 'provisioner',
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
};
},
async grantCollaborator(): Promise<void> {
expect((await listCredentialJournals(root))[0]?.state).toBe('open');
},
async readCollaboratorPermission() {
return {
identity: 'seat-name',
permission: 'write',
endpoint: 'GET /api/v1/repos/owner/repo/collaborators/seat-name/permission',
contentType: 'application/json',
};
},
async readOrganizationMembership() {
return {
state: 'absent',
endpoint: 'GET /api/v1/users/seat-name/orgs',
contentType: 'application/json',
};
},
};
const result = await grantDirectRepositoryPermission(
{
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
repo: 'owner/repo',
permission: 'write',
readOnlyControlIdentity: 'read-control',
},
authority,
provider,
validationDependencies('write'),
{ stateRoot: root, actor: 'provisioner' },
);
expect(result.outcome).toBe('ok');
expect(result.mutation).toBe('applied');
expect(result.evidence.repositoryPermission?.effective).toBe('write');
expect(result.evidence.organizationMembership?.state).toBe('absent');
expect(result.audit.state).toBe('sealed');
const [sealed] = await listCredentialJournals(root);
const source = await readFile(sealed?.path ?? '', 'utf8');
expect(source).toContain('"phase":"mutation"');
expect(source).toContain('"decision":"collaborator-grant-applied"');
expect(source).toContain('"decision":"identity-verified"');
expect(source).toContain('"decision":"organization-member-absent"');
expect(source).toContain('"decision":"transport-write-verified"');
});
it('preserves applied mutation and journal context when post-grant read-back fails', async (): Promise<void> => {
const root = await stateRoot();
const provider: GiteaGrantProvider = {
async readBasicIdentity() {
return {
login: 'provisioner',
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
};
},
async grantCollaborator(): Promise<void> {},
async readCollaboratorPermission() {
throw new Error('read-back unavailable');
},
async readOrganizationMembership() {
throw new Error('must not be reached');
},
};
const result = await grantDirectRepositoryPermission(
{
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
repo: 'owner/repo',
permission: 'write',
readOnlyControlIdentity: 'read-control',
},
authority,
provider,
validationDependencies('write'),
{ stateRoot: root, actor: 'provisioner' },
);
expect(result.outcome).toBe('indeterminate');
expect(result.mutation).toBe('applied');
expect(result.reason.code).toBe('readback-missing');
expect(result.audit.journalId).not.toBeNull();
expect(result.audit.state).toBe('sealed');
});
it('is indeterminate when grant read-back disagrees with the requested permission', async (): Promise<void> => {
const root = await stateRoot();
const provider: GiteaGrantProvider = {
async readBasicIdentity() {
return {
login: 'provisioner',
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
};
},
async grantCollaborator(): Promise<void> {},
async readCollaboratorPermission() {
return {
identity: 'seat-name',
permission: 'read',
endpoint: 'GET /api/v1/repos/owner/repo/collaborators/seat-name/permission',
contentType: 'application/json',
};
},
async readOrganizationMembership() {
return {
state: 'absent',
endpoint: 'GET /api/v1/users/seat-name/orgs',
contentType: 'application/json',
};
},
};
const result = await grantDirectRepositoryPermission(
{
identity: 'seat-name',
estate: 'homelab',
host: 'git.example.invalid',
repo: 'owner/repo',
permission: 'write',
readOnlyControlIdentity: 'read-control',
},
authority,
provider,
validationDependencies('read'),
{ stateRoot: root, actor: 'provisioner' },
);
expect(result.outcome).toBe('indeterminate');
expect(result.reason.code).toBe('permission-evidence-disagrees');
expect(result.mutation).toBe('applied');
});
});