mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
584 lines
22 KiB
JavaScript
584 lines
22 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import {
|
|
mkdtemp,
|
|
mkdir,
|
|
readFile,
|
|
realpath,
|
|
rename,
|
|
rm,
|
|
writeFile,
|
|
} from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
DEFAULT_ACCESS_POLICY,
|
|
createAccessPolicy,
|
|
createAccessPolicyScope,
|
|
evaluateAccessPolicy,
|
|
normalizeAccessPolicy,
|
|
validateAccessPolicy,
|
|
} from '../src/channels/shared/access-policy.mjs';
|
|
import {
|
|
BotWorkspaceStore,
|
|
createWorkspaceAwareController,
|
|
} from '../src/channels/shared/bot-workspace-store.mjs';
|
|
|
|
function user(id, canExecuteCommands = true) {
|
|
return { id, canExecuteCommands };
|
|
}
|
|
|
|
function scope({
|
|
mode = 'open',
|
|
defaultCanExecuteCommands = true,
|
|
commandPermissionOverrides = [],
|
|
users = [],
|
|
} = {}) {
|
|
return {
|
|
mode,
|
|
open: { defaultCanExecuteCommands, commandPermissionOverrides },
|
|
allowlist: { users },
|
|
};
|
|
}
|
|
|
|
function openScope({
|
|
defaultCanExecuteCommands = true,
|
|
commandPermissionOverrides = [],
|
|
allowlistUsers = [],
|
|
} = {}) {
|
|
return scope({
|
|
mode: 'open',
|
|
defaultCanExecuteCommands,
|
|
commandPermissionOverrides,
|
|
users: allowlistUsers,
|
|
});
|
|
}
|
|
|
|
function allowlistScope(users = [], {
|
|
defaultCanExecuteCommands = false,
|
|
commandPermissionOverrides = [],
|
|
} = {}) {
|
|
return scope({
|
|
mode: 'allowlist',
|
|
defaultCanExecuteCommands,
|
|
commandPermissionOverrides,
|
|
users,
|
|
});
|
|
}
|
|
|
|
function policy({ direct = openScope(), group = openScope() } = {}) {
|
|
return { direct, group };
|
|
}
|
|
|
|
async function fixture(t) {
|
|
const root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-im-access-policy-')));
|
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
const defaultWorkspace = join(root, 'workspace');
|
|
await mkdir(defaultWorkspace);
|
|
return {
|
|
root,
|
|
defaultWorkspace,
|
|
path: join(root, 'workspaces.json'),
|
|
};
|
|
}
|
|
|
|
test('access policy validates and normalizes one complete atomic config', () => {
|
|
const normalized = validateAccessPolicy(policy({
|
|
direct: openScope({
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user(' user-one ')],
|
|
allowlistUsers: [user('inactive-member', false)],
|
|
}),
|
|
group: allowlistScope([user(8672352515, false)], {
|
|
commandPermissionOverrides: [user('inactive-admin')],
|
|
}),
|
|
}));
|
|
|
|
assert.deepEqual(normalized, {
|
|
direct: {
|
|
mode: 'open',
|
|
open: {
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('user-one')],
|
|
},
|
|
allowlist: { users: [user('inactive-member', false)] },
|
|
},
|
|
group: {
|
|
mode: 'allowlist',
|
|
open: {
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('inactive-admin')],
|
|
},
|
|
allowlist: { users: [user('8672352515', false)] },
|
|
},
|
|
});
|
|
assert.equal(Object.isFrozen(normalized), true);
|
|
assert.equal(Object.isFrozen(normalized.direct.open.commandPermissionOverrides), true);
|
|
assert.equal(Object.isFrozen(normalized.direct.allowlist.users), true);
|
|
assert.equal(normalizeAccessPolicy({ damaged: true }), null);
|
|
|
|
const invalid = [
|
|
{},
|
|
{ ...policy(), extra: true },
|
|
policy({ direct: { ...openScope(), extra: true } }),
|
|
policy({ direct: { ...openScope(), mode: 'private' } }),
|
|
policy({ direct: { ...openScope(), open: { defaultCanExecuteCommands: true } } }),
|
|
policy({ direct: openScope({ defaultCanExecuteCommands: 'yes' }) }),
|
|
policy({ direct: openScope({ commandPermissionOverrides: 'user-one' }) }),
|
|
policy({ direct: { ...openScope(), allowlist: { users: 'user-one' } } }),
|
|
policy({ direct: openScope({ commandPermissionOverrides: [user('')] }) }),
|
|
policy({ direct: openScope({ commandPermissionOverrides: [user('bad\u0000id')] }) }),
|
|
policy({ direct: openScope({ commandPermissionOverrides: [user('x'.repeat(257))] }) }),
|
|
policy({ direct: openScope({ commandPermissionOverrides: [user('user', 'yes')] }) }),
|
|
policy({ direct: openScope({
|
|
commandPermissionOverrides: [{ ...user('user'), extra: true }],
|
|
}) }),
|
|
policy({ direct: openScope({
|
|
commandPermissionOverrides: [user(' duplicate '), user('duplicate', false)],
|
|
}) }),
|
|
policy({ direct: allowlistScope([user(' duplicate '), user('duplicate', false)]) }),
|
|
{
|
|
direct: { mode: 'open', defaultCanExecuteCommands: true, users: [] },
|
|
group: { mode: 'open', defaultCanExecuteCommands: true, users: [] },
|
|
},
|
|
];
|
|
for (const input of invalid) {
|
|
assert.throws(() => validateAccessPolicy(input), { code: 'access-policy-invalid' });
|
|
}
|
|
});
|
|
|
|
test('access policy constructors default closed and accept only canonical scopes', () => {
|
|
assert.deepEqual(DEFAULT_ACCESS_POLICY, policy({
|
|
direct: allowlistScope(),
|
|
group: allowlistScope(),
|
|
}));
|
|
assert.deepEqual(createAccessPolicy({
|
|
direct: createAccessPolicyScope(allowlistScope([
|
|
user('owner'),
|
|
user(1234n),
|
|
])),
|
|
group: createAccessPolicyScope(openScope()),
|
|
}), policy({
|
|
direct: allowlistScope([
|
|
user('owner'),
|
|
user('1234'),
|
|
]),
|
|
group: openScope(),
|
|
}));
|
|
assert.deepEqual(createAccessPolicyScope({
|
|
mode: 'open',
|
|
open: {
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('admin')],
|
|
},
|
|
allowlist: { users: [user('member')] },
|
|
}), openScope({
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('admin')],
|
|
allowlistUsers: [user('member')],
|
|
}));
|
|
assert.throws(() => createAccessPolicyScope({
|
|
mode: 'allowlist',
|
|
defaultCanExecuteCommands: false,
|
|
users: [],
|
|
}), { code: 'access-policy-invalid' });
|
|
});
|
|
|
|
test('access decisions keep direct, group, ordinary-message and command permissions separate', () => {
|
|
const settings = policy({
|
|
direct: openScope({
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('admin')],
|
|
allowlistUsers: [user('guest-deny', false)],
|
|
}),
|
|
group: allowlistScope([
|
|
user('member', false),
|
|
user('operator'),
|
|
], { commandPermissionOverrides: [user('unknown')] }),
|
|
});
|
|
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds: ['guest'],
|
|
}), { allowed: true, reason: 'allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds: ['guest'], isCommand: true,
|
|
}), { allowed: false, reason: 'command-not-allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds: ['admin'], isCommand: true,
|
|
}), { allowed: true, reason: 'allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds: ['guest-deny'], isCommand: true,
|
|
}), { allowed: false, reason: 'command-not-allowed' },
|
|
'the inactive allowlist does not override open-mode command defaults');
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group', senderIds: ['unknown'],
|
|
}), { allowed: false, reason: 'sender-not-allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group', senderIds: ['member'],
|
|
}), { allowed: true, reason: 'allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group', senderIds: ['member'], isCommand: true,
|
|
}), { allowed: false, reason: 'command-not-allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group',
|
|
senderIds: ['alternate', 'OPERATOR'],
|
|
isCommand: true,
|
|
equals: (left, right) => left.toLowerCase() === right.toLowerCase(),
|
|
}), { allowed: true, reason: 'allowed' });
|
|
const nonCanonical = {
|
|
direct: {
|
|
mode: 'allowlist',
|
|
defaultCanExecuteCommands: false,
|
|
users: [user('old-member', false)],
|
|
},
|
|
group: { mode: 'open', defaultCanExecuteCommands: true, users: [] },
|
|
};
|
|
assert.equal(normalizeAccessPolicy(nonCanonical), null);
|
|
assert.deepEqual(evaluateAccessPolicy(nonCanonical, {
|
|
conversationType: 'direct', senderIds: ['old-member'],
|
|
}), { allowed: false, reason: 'policy-unavailable' });
|
|
assert.deepEqual(evaluateAccessPolicy(null, {
|
|
conversationType: 'direct', senderIds: ['admin'],
|
|
}), { allowed: false, reason: 'policy-unavailable' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'channel', senderIds: ['admin'],
|
|
}), { allowed: false, reason: 'invalid-context' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds: [null, 'bad\u0000id'],
|
|
}), { allowed: false, reason: 'sender-unavailable' });
|
|
});
|
|
|
|
test('access decisions read only the active scenario when one id exists in both lists', () => {
|
|
const sameId = policy({
|
|
direct: openScope({
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('same-user', true)],
|
|
allowlistUsers: [user('same-user', false)],
|
|
}),
|
|
group: allowlistScope([user('same-user', false)], {
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [user('same-user', true)],
|
|
}),
|
|
});
|
|
|
|
assert.doesNotThrow(() => validateAccessPolicy(sameId),
|
|
'the same id may appear once in each independent scenario');
|
|
assert.deepEqual(evaluateAccessPolicy(sameId, {
|
|
conversationType: 'direct', senderIds: ['same-user'], isCommand: true,
|
|
}), { allowed: true, reason: 'allowed' },
|
|
'open mode reads its override and ignores the conflicting allowlist row');
|
|
assert.deepEqual(evaluateAccessPolicy(sameId, {
|
|
conversationType: 'group', senderIds: ['same-user'], isCommand: true,
|
|
}), { allowed: false, reason: 'command-not-allowed' },
|
|
'allowlist mode reads its row and ignores the conflicting open override');
|
|
});
|
|
|
|
test('access decisions deny commands when equivalent sender aliases match conflicting rows', () => {
|
|
const settings = policy({
|
|
direct: openScope({
|
|
defaultCanExecuteCommands: true,
|
|
commandPermissionOverrides: [
|
|
user('configured-pn', true),
|
|
user('configured-lid', false),
|
|
],
|
|
}),
|
|
group: allowlistScope([
|
|
user('configured-pn', true),
|
|
user('configured-lid', false),
|
|
]),
|
|
});
|
|
const aliases = new Set([
|
|
'sender-pn', 'sender-lid', 'configured-pn', 'configured-lid',
|
|
]);
|
|
const equals = (left, right) => aliases.has(left) && aliases.has(right);
|
|
const senderIds = ['sender-pn', 'sender-lid'];
|
|
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'direct', senderIds, isCommand: true, equals,
|
|
}), { allowed: false, reason: 'command-not-allowed' });
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group', senderIds, equals,
|
|
}), { allowed: true, reason: 'allowed' },
|
|
'any matching allowlist alias permits an ordinary message');
|
|
assert.deepEqual(evaluateAccessPolicy(settings, {
|
|
conversationType: 'group', senderIds, isCommand: true, equals,
|
|
}), { allowed: false, reason: 'command-not-allowed' });
|
|
|
|
const allAllowed = policy({
|
|
direct: openScope({
|
|
defaultCanExecuteCommands: false,
|
|
commandPermissionOverrides: [
|
|
user('configured-pn', true),
|
|
user('configured-lid', true),
|
|
],
|
|
}),
|
|
});
|
|
assert.deepEqual(evaluateAccessPolicy(allAllowed, {
|
|
conversationType: 'direct', senderIds, isCommand: true, equals,
|
|
}), { allowed: true, reason: 'allowed' },
|
|
'all equivalent matching rows must explicitly allow commands');
|
|
});
|
|
|
|
test('BotWorkspaceStore initializes a missing policy once and upgrades v1 to v2', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
await writeFile(path, `${JSON.stringify({
|
|
version: 1,
|
|
workspaces: { bot_one: defaultWorkspace },
|
|
agentPresets: { bot_one: 'router-standard' },
|
|
})}\n`);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
const initial = policy({ direct: allowlistScope([
|
|
{ id: 'owner', canExecuteCommands: true },
|
|
]) });
|
|
|
|
await store.ensure('bot_one', { initialAccessPolicy: initial });
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), initial);
|
|
const saved = JSON.parse(await readFile(path, 'utf8'));
|
|
assert.equal(saved.version, 2);
|
|
assert.equal(saved.workspaces.bot_one, defaultWorkspace);
|
|
assert.equal(saved.agentPresets.bot_one, 'router-standard');
|
|
assert.deepEqual(saved.accessPolicies.bot_one, initial);
|
|
|
|
const replacementSeed = policy({ direct: openScope() });
|
|
await store.ensure('bot_one', { initialAccessPolicy: replacementSeed });
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), initial, 'initialization is idempotent');
|
|
|
|
const reloaded = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
assert.deepEqual(reloaded.accessPolicyFor('bot_one'), initial);
|
|
assert.deepEqual(reloaded.decorateStatus({ bots: [{ botId: 'bot_one' }] }).bots[0].accessPolicy, initial);
|
|
});
|
|
|
|
test('BotWorkspaceStore fail-closes non-canonical v2 policies without rewriting on load', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
const valid = policy({
|
|
direct: allowlistScope([user('member')]),
|
|
group: openScope({ defaultCanExecuteCommands: false }),
|
|
});
|
|
const nonCanonical = {
|
|
direct: {
|
|
mode: 'open',
|
|
defaultCanExecuteCommands: false,
|
|
users: [user('old-override')],
|
|
},
|
|
group: {
|
|
mode: 'allowlist',
|
|
defaultCanExecuteCommands: true,
|
|
users: [user('old-member', false)],
|
|
},
|
|
};
|
|
const original = JSON.stringify({
|
|
version: 2,
|
|
workspaces: {
|
|
bot_good: defaultWorkspace,
|
|
bot_noncanonical: defaultWorkspace,
|
|
},
|
|
agentPresets: { bot_good: 'router-standard' },
|
|
contextEnhancement: {
|
|
bot_good: {
|
|
group: { enabled: false, fields: ['senderId'], guidance: '' },
|
|
direct: { enabled: true, fields: ['senderId', 'senderName'], guidance: 'direct' },
|
|
},
|
|
},
|
|
deliveryTargets: {
|
|
bot_good: { target: { kind: 'user', route: { userId: 'one' } } },
|
|
},
|
|
accessPolicies: {
|
|
bot_good: valid,
|
|
bot_noncanonical: nonCanonical,
|
|
},
|
|
});
|
|
await writeFile(path, original);
|
|
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
assert.deepEqual(store.accessPolicyFor('bot_good'), valid);
|
|
assert.equal(store.accessPolicyFor('bot_noncanonical'), null);
|
|
assert.equal(store.agentPresetFor('bot_good'), 'router-standard');
|
|
assert.equal(store.deliveryTargetFor('bot_good', 'target').route.userId, 'one');
|
|
assert.equal(await readFile(path, 'utf8'), original,
|
|
'loading invalid policy data must be read-only');
|
|
await store.ensure('bot_noncanonical', { initialAccessPolicy: policy() });
|
|
assert.equal(store.accessPolicyFor('bot_noncanonical'), null,
|
|
'invalid policy data is not treated as a missing seed');
|
|
assert.equal(await readFile(path, 'utf8'), original);
|
|
});
|
|
|
|
test('BotWorkspaceStore isolates damaged policies and does not initialize over them', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
const valid = policy({ group: allowlistScope([
|
|
{ id: 'member', canExecuteCommands: true },
|
|
]) });
|
|
await writeFile(path, `${JSON.stringify({
|
|
version: 2,
|
|
workspaces: {
|
|
bot_good: defaultWorkspace,
|
|
bot_damaged: defaultWorkspace,
|
|
bot_missing: defaultWorkspace,
|
|
},
|
|
deliveryTargets: {
|
|
bot_good: { target: { kind: 'user', route: { userId: 'one' } } },
|
|
},
|
|
accessPolicies: {
|
|
bot_good: valid,
|
|
bot_damaged: { direct: { mode: 'open' } },
|
|
},
|
|
})}\n`);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
|
|
assert.deepEqual(store.accessPolicyFor('bot_good'), valid);
|
|
assert.equal(store.accessPolicyFor('bot_damaged'), null);
|
|
assert.equal(store.accessPolicyFor('bot_missing'), null);
|
|
await store.ensure('bot_damaged', { initialAccessPolicy: policy() });
|
|
await store.ensure('bot_missing', { initialAccessPolicy: policy() });
|
|
|
|
assert.equal(store.accessPolicyFor('bot_damaged'), null, 'damaged is not treated as missing');
|
|
assert.deepEqual(store.accessPolicyFor('bot_missing'), policy());
|
|
assert.equal(store.deliveryTargetFor('bot_good', 'target').route.userId, 'one');
|
|
const saved = JSON.parse(await readFile(path, 'utf8'));
|
|
assert.equal(saved.accessPolicies.bot_damaged, null);
|
|
assert.deepEqual(saved.accessPolicies.bot_good, valid);
|
|
assert.deepEqual(saved.accessPolicies.bot_missing, policy());
|
|
});
|
|
|
|
test('BotWorkspaceStore fail-closes a damaged policy section without poisoning other bot data', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
await writeFile(path, `${JSON.stringify({
|
|
version: 2,
|
|
workspaces: { bot_one: defaultWorkspace },
|
|
agentPresets: { bot_one: 'router-standard' },
|
|
deliveryTargets: {
|
|
bot_one: { target: { kind: 'user', route: { userId: 'one' } } },
|
|
},
|
|
accessPolicies: 'damaged-section',
|
|
})}\n`);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
|
|
assert.equal(store.workspaceFor('bot_one'), defaultWorkspace);
|
|
assert.equal(store.agentPresetFor('bot_one'), 'router-standard');
|
|
assert.equal(store.deliveryTargetFor('bot_one', 'target').route.userId, 'one');
|
|
assert.equal(store.accessPolicyFor('bot_one'), null);
|
|
await store.ensure('bot_one', { initialAccessPolicy: policy() });
|
|
assert.equal(store.accessPolicyFor('bot_one'), null,
|
|
'startup initialization must not overwrite a damaged policy section');
|
|
});
|
|
|
|
test('BotWorkspaceStore publishes policy snapshots only after atomic persistence', async (t) => {
|
|
const { root, defaultWorkspace } = await fixture(t);
|
|
const storeDirectory = join(root, 'store');
|
|
const storePath = join(storeDirectory, 'workspaces.json');
|
|
await mkdir(storeDirectory);
|
|
const store = await new BotWorkspaceStore(storePath, { defaultWorkspace }).load();
|
|
const initial = policy({ direct: allowlistScope([
|
|
{ id: 'owner', canExecuteCommands: true },
|
|
]) });
|
|
await store.ensure('bot_io', { initialAccessPolicy: initial });
|
|
|
|
await rename(storeDirectory, `${storeDirectory}-saved`);
|
|
await writeFile(storeDirectory, 'blocks policy persistence');
|
|
await assert.rejects(store.setAccessPolicy('bot_io', policy()));
|
|
assert.deepEqual(store.accessPolicyFor('bot_io'), initial);
|
|
|
|
await rm(storeDirectory, { force: true });
|
|
await rename(`${storeDirectory}-saved`, storeDirectory);
|
|
assert.deepEqual(JSON.parse(await readFile(storePath, 'utf8')).accessPolicies.bot_io, initial);
|
|
});
|
|
|
|
test('BotWorkspaceStore cleans policies on reconcile and fences same-id stale updates', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
await store.ensure('bot_one', { initialAccessPolicy: policy() });
|
|
await store.ensure('bot_two', { initialAccessPolicy: policy({ group: allowlistScope() }) });
|
|
const staleIncarnation = store.incarnationFor('bot_one');
|
|
|
|
await store.remove('bot_one');
|
|
await store.ensure('bot_one', {
|
|
initialAccessPolicy: policy({ direct: allowlistScope([
|
|
{ id: 'new-owner', canExecuteCommands: true },
|
|
]) }),
|
|
});
|
|
await assert.rejects(store.setAccessPolicy('bot_one', policy(), {
|
|
incarnation: staleIncarnation,
|
|
}), { code: 'workspace-bot-not-found' });
|
|
assert.equal(store.accessPolicyFor('bot_one').direct.allowlist.users[0].id, 'new-owner');
|
|
|
|
await store.reconcile(['bot_one']);
|
|
const saved = JSON.parse(await readFile(path, 'utf8'));
|
|
assert.equal(saved.workspaces.bot_two, undefined);
|
|
assert.equal(saved.accessPolicies.bot_two, undefined);
|
|
assert.equal(saved.accessPolicies.bot_one.direct.allowlist.users[0].id, 'new-owner');
|
|
});
|
|
|
|
test('workspace-aware controller preprojects a full policy status before commit', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
const initial = policy({ direct: allowlistScope() });
|
|
const updated = policy({ direct: openScope({ defaultCanExecuteCommands: false }) });
|
|
await store.ensure('bot_one', { initialAccessPolicy: initial });
|
|
const base = {
|
|
status() { return { bots: [{ botId: 'bot_one', connected: true }] }; },
|
|
};
|
|
const controller = createWorkspaceAwareController(base, {
|
|
workspaces: store,
|
|
stateFor: async () => ({ async clearSessions() {} }),
|
|
});
|
|
|
|
const result = await controller.updateAccessPolicy('bot_one', updated, async (projected) => {
|
|
assert.deepEqual(projected.bots[0].accessPolicy, updated);
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), initial, 'projection happens before commit');
|
|
return { ...projected, projected: true };
|
|
});
|
|
assert.equal(result.projected, true);
|
|
assert.deepEqual(result.bots[0].accessPolicy, updated);
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), updated);
|
|
|
|
await assert.rejects(controller.updateAccessPolicy('bot_one', initial, async () => {
|
|
throw new Error('projection failed');
|
|
}), /projection failed/);
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), updated);
|
|
assert.throws(() => controller.updateAccessPolicy('bot_one', {
|
|
direct: { mode: 'open', defaultCanExecuteCommands: true, users: [] },
|
|
group: { mode: 'open', defaultCanExecuteCommands: true, users: [] },
|
|
}), {
|
|
code: 'access-policy-invalid',
|
|
});
|
|
assert.deepEqual(store.accessPolicyFor('bot_one'), updated,
|
|
'strict writes reject non-canonical payloads without changing the committed policy');
|
|
await assert.rejects(controller.updateAccessPolicy('missing', initial), {
|
|
code: 'workspace-bot-not-found',
|
|
});
|
|
});
|
|
|
|
test('workspace-aware controller cannot write a policy into a same-id rebound bot', async (t) => {
|
|
const { path, defaultWorkspace } = await fixture(t);
|
|
const store = await new BotWorkspaceStore(path, { defaultWorkspace }).load();
|
|
const oldPolicy = policy({ direct: allowlistScope([
|
|
{ id: 'old-owner', canExecuteCommands: true },
|
|
]) });
|
|
const reboundPolicy = policy({ direct: allowlistScope([
|
|
{ id: 'new-owner', canExecuteCommands: true },
|
|
]) });
|
|
await store.ensure('bot_rebound', { initialAccessPolicy: oldPolicy });
|
|
let markStatusStarted;
|
|
let releaseStatus;
|
|
const statusStarted = new Promise((resolveStarted) => { markStatusStarted = resolveStarted; });
|
|
const statusGate = new Promise((resolveStatus) => { releaseStatus = resolveStatus; });
|
|
const controller = createWorkspaceAwareController({
|
|
async status() {
|
|
markStatusStarted();
|
|
await statusGate;
|
|
return { bots: [{ botId: 'bot_rebound' }] };
|
|
},
|
|
}, {
|
|
workspaces: store,
|
|
stateFor: async () => ({ async clearSessions() {} }),
|
|
});
|
|
|
|
const updating = controller.updateAccessPolicy('bot_rebound', policy());
|
|
await statusStarted;
|
|
await store.remove('bot_rebound');
|
|
await store.ensure('bot_rebound', { initialAccessPolicy: reboundPolicy });
|
|
releaseStatus();
|
|
|
|
await assert.rejects(updating, { code: 'workspace-bot-not-found' });
|
|
assert.deepEqual(store.accessPolicyFor('bot_rebound'), reboundPolicy);
|
|
});
|