ci/woodpecker/pr/ci Pipeline was successful
Implements the ratified hierarchy command surface per contract 1 (hierarchy-schema.md) and contract 2 (rbac-grant-model.md), brief M4-1B-II: - HierarchyRepository: the closed command family (company/estate/ platform-project create/rename/transfer/delete, grant create/change/ revoke, directory + granted-companies reads). Every mutation runs in one transaction through the M4-1b-i audit machinery (event + outbox, idempotency-key replay, causation-linked composite operations). - HierarchyGrantEvaluationService: live deny-by-default evaluation — effective role is the max over ancestor-chain user grants, fail-closed, team subjects suspended (§1.4), platform admin confers no tenant access (§1.1). - companies.visibility column (private default, directory carve-out) with migration 0020, admin-only audited visibility_change (§5.5), closed-field directory listing (§2.8), no-existence-oracle refusals (§6.7). - hierarchy_grants role CHECK pinned to the ratified vocabulary; namespaced serialized roles (hierarchy:*, §4.5). - §1.1 bypass retirement: role-derived MCP scope elevation and hasScope admin shortcuts removed; specs updated to the granted-scope path. - Witnesses: schema-level (role CHECK, visibility class/default), §6.3 closed route inventory, §6.4 per-mutation-class commit+rollback legs, §6.5 authorization, §6.7 oracle indistinguishability, §6.9 visibility, grant-evaluation semantics (chain inheritance, max-role, live revocation).
144 lines
5.3 KiB
TypeScript
144 lines
5.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { CommandDef, SlashCommandPayload } from '@mosaicstack/types';
|
|
import { CommandAuthorizationService } from './command-authorization.service.js';
|
|
|
|
const adminCommand: CommandDef = {
|
|
name: 'gc',
|
|
description: 'GC',
|
|
aliases: [],
|
|
scope: 'admin',
|
|
execution: 'socket',
|
|
available: true,
|
|
};
|
|
const payload: SlashCommandPayload = { command: 'gc', conversationId: 'conversation-1' };
|
|
|
|
function createService(
|
|
role: string,
|
|
entries: Map<string, string> = new Map<string, string>(),
|
|
): CommandAuthorizationService {
|
|
const db = {
|
|
select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }),
|
|
};
|
|
const redis = {
|
|
get: async (key: string) => entries.get(key) ?? null,
|
|
set: async (key: string, value: string) => {
|
|
entries.set(key, value);
|
|
},
|
|
del: async (key: string) => Number(entries.delete(key)),
|
|
};
|
|
return new CommandAuthorizationService(db as never, redis);
|
|
}
|
|
|
|
describe('CommandAuthorizationService', () => {
|
|
it('consumes one exact actor-bound approval once', async (): Promise<void> => {
|
|
const service = createService('admin');
|
|
const approval = await service.createApproval(adminCommand, payload, 'admin-1');
|
|
expect(approval).not.toBeNull();
|
|
expect(
|
|
(await service.authorize(adminCommand, payload, 'admin-1', approval!.approvalId)).allowed,
|
|
).toBe(true);
|
|
expect(
|
|
(await service.authorize(adminCommand, payload, 'admin-1', approval!.approvalId)).allowed,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('rejects an approval when the structured action is mutated', async (): Promise<void> => {
|
|
const service = createService('admin');
|
|
const approval = await service.createApproval(adminCommand, payload, 'admin-1');
|
|
const mutated = { ...payload, conversationId: 'other-conversation' };
|
|
expect(approval).not.toBeNull();
|
|
expect(
|
|
(await service.authorize(adminCommand, mutated, 'admin-1', approval!.approvalId)).allowed,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('denies an admin command to a member before approval is considered', async (): Promise<void> => {
|
|
const service = createService('member');
|
|
const approval = await service.createApproval(adminCommand, payload, 'member-1');
|
|
expect(approval).toBeNull();
|
|
expect(
|
|
(await service.authorize(adminCommand, payload, 'member-1', 'forged-approval-id')).allowed,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('denies non-admin scopes to a platform admin (contract 2 §1.1 bypass retirement)', async (): Promise<void> => {
|
|
const service = createService('admin');
|
|
for (const scope of ['core', 'agent', 'skill', 'plugin'] as const) {
|
|
const command: CommandDef = { ...adminCommand, name: `probe-${scope}`, scope };
|
|
expect(
|
|
(await service.authorize(command, { ...payload, command: command.name }, 'admin-1'))
|
|
.allowed,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('allows member core/agent scopes and denies skill/plugin (deny-by-default)', async (): Promise<void> => {
|
|
const service = createService('member');
|
|
for (const [scope, allowed] of [
|
|
['core', true],
|
|
['agent', true],
|
|
['skill', false],
|
|
['plugin', false],
|
|
] as const) {
|
|
const command: CommandDef = { ...adminCommand, name: `probe-${scope}`, scope };
|
|
expect(
|
|
(await service.authorize(command, { ...payload, command: command.name }, 'member-1'))
|
|
.allowed,
|
|
).toBe(allowed);
|
|
}
|
|
});
|
|
|
|
it('denies a malformed durable approval expiry instead of treating it as unexpired', async (): Promise<void> => {
|
|
const entries = new Map<string, string>();
|
|
const action = {
|
|
providerId: 'fleet',
|
|
sessionId: 'nova',
|
|
actorId: 'admin-1',
|
|
tenantId: 'tenant-1',
|
|
channelId: 'discord:operator',
|
|
correlationId: 'correlation-malformed-expiry',
|
|
agentName: 'Nova',
|
|
};
|
|
const service = createService('admin', entries);
|
|
const approval = await service.createRuntimeTerminationApproval(action);
|
|
expect(approval).not.toBeNull();
|
|
const key = `agent:Nova:command-approval:${approval!.approvalId}`;
|
|
const stored = entries.get(key);
|
|
expect(stored).toBeDefined();
|
|
entries.set(key, JSON.stringify({ ...JSON.parse(stored!), expiresAt: 'not-a-date' }));
|
|
|
|
expect(await service.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('persists and consumes one exact runtime termination approval across a service restart', async (): Promise<void> => {
|
|
const entries = new Map<string, string>();
|
|
const action = {
|
|
providerId: 'fleet',
|
|
sessionId: 'nova',
|
|
actorId: 'admin-1',
|
|
tenantId: 'tenant-1',
|
|
channelId: 'discord:operator',
|
|
correlationId: 'correlation-1',
|
|
agentName: 'Nova',
|
|
};
|
|
const beforeRestart = createService('admin', entries);
|
|
const approval = await beforeRestart.createRuntimeTerminationApproval(action);
|
|
|
|
const afterRestart = createService('admin', entries);
|
|
expect(
|
|
await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, {
|
|
...action,
|
|
sessionId: 'forged-session',
|
|
}),
|
|
).toBe(false);
|
|
expect(await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe(
|
|
true,
|
|
);
|
|
expect(await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|