feat: add unified IM access policies

This commit is contained in:
xmanrui 2026-09-01 10:45:12 +08:00
parent 075d2713c1
commit 840e5aa857
89 changed files with 5573 additions and 1676 deletions

View file

@ -15,6 +15,10 @@ import {
OutboundArtifactRegistry,
createOutboundArtifactTool,
} from '../../../src/channels/shared/semantic/artifact.mjs';
import {
COMMAND_PERMISSION_DENIED_MESSAGE,
directAccessPolicy,
} from '../access-policy-fixture.mjs';
const PNG_1X1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
@ -894,6 +898,79 @@ test('bridge downloads an inbound Feishu image once and submits structured Harne
assert.deepEqual(sent, ['看到了一张图片']);
});
test('Feishu applies the unified access policy before attachments or Harness work', async () => {
const fixture = stateFixture([['p2p:ou_member', 'session-member']]);
let downloads = 0;
const harnessCalls = [];
const sent = [];
const accessPolicy = directAccessPolicy({
users: [{ id: 'ou_member', canExecuteCommands: false }],
privilegedIds: ['ou_owner'],
});
const client = {
im: { v1: {
messageResource: { get: async () => {
downloads += 1;
return { getReadableStream: () => Readable.from([PNG_1X1]) };
} },
message: { create: async (request) => {
sent.push(JSON.parse(request.data.content).text);
return { code: 0, data: { message_id: `om_policy_${sent.length}` } };
} },
} },
};
const bridge = new FeishuHarnessBridge({
client,
channel: {},
accessPolicy,
harness: {
sessionExists: async (sessionId) => {
harnessCalls.push(['sessionExists', sessionId]);
return true;
},
ask: async (sessionId, prompt) => {
harnessCalls.push(['ask', sessionId, prompt]);
return '白名单消息已处理';
},
},
state: fixture.state,
status: bridgeStatus(),
});
await bridge.accept(event('policy-blocked-image', '', {
senderOpenId: 'ou_blocked',
message_type: 'image',
content: JSON.stringify({ image_key: 'img_blocked' }),
}));
await bridge.waitForIdle();
assert.equal(downloads, 0);
assert.deepEqual(harnessCalls, []);
assert.deepEqual(sent, []);
await bridge.accept(event('policy-member-text', '普通消息', {
senderOpenId: 'ou_member',
}));
await bridge.waitForIdle();
assert.equal(harnessCalls.some(([operation]) => operation === 'ask'), true);
assert.deepEqual(sent, ['白名单消息已处理']);
const callsBeforeDeniedCommand = harnessCalls.length;
const repliesBeforeDeniedCommand = sent.length;
await bridge.accept(event('policy-member-command', '/help', {
senderOpenId: 'ou_member',
}));
await bridge.waitForIdle();
assert.equal(harnessCalls.length, callsBeforeDeniedCommand);
assert.deepEqual(sent.slice(repliesBeforeDeniedCommand), [COMMAND_PERMISSION_DENIED_MESSAGE]);
accessPolicy.getSettings().direct.allowlist.users = [];
await bridge.accept(event('policy-owner-command', '/help', {
senderOpenId: 'ou_owner',
}));
await bridge.waitForIdle();
assert.match(sent.at(-1), /\/status/);
});
test('bridge hands a native Feishu file source to the current Harness turn', async () => {
const fixture = stateFixture([['p2p:ou_user', 'session-file']]);
const bytes = Buffer.from('feishu-native-file');
@ -3533,6 +3610,32 @@ test('card buttons from an unallowed sender are ignored', async () => {
assert.equal(sent.length, 1, 'a card action without an operator must fail closed');
});
test('a card callback without a trusted route stays silent before access evaluation', async () => {
const sent = [];
const bridge = new FeishuHarnessBridge({
client: cardClient(async (outgoing) => sent.push(outgoing)),
channel: {},
accessPolicy: directAccessPolicy({
users: [{ id: 'ou_member', canExecuteCommands: true }],
privilegedIds: ['ou_owner'],
}),
harness: sessionsHarness(1),
state: stateFixture().state,
status: bridgeStatus(),
});
await bridge.onCardAction({
...cardActionEvent('om_stale_after_restart', 'new', 'ou_member'),
context: {
open_message_id: 'om_stale_after_restart',
open_chat_id: 'oc_untrusted_scope',
},
});
await bridge.waitForIdle();
assert.deepEqual(sent, [], 'missing direct/group scope must fail closed without a reply');
});
test('card buttons from an allowed sender work', async () => {
const fixture = stateFixture();
const sent = [];