mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
231 lines
6.5 KiB
JavaScript
231 lines
6.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
InvalidProjectPolicyValueError,
|
|
ProjectPolicyEngine,
|
|
ProjectPolicyUnavailableError,
|
|
normalizeProjectPermission,
|
|
normalizeProjectPolicySnapshot,
|
|
normalizeProjectRoleBinding,
|
|
} = require('@qinglong/runtime-core/project-policy');
|
|
|
|
const PROJECT = Object.freeze({
|
|
id: 'default',
|
|
name: 'Default',
|
|
slug: 'default',
|
|
status: 'active',
|
|
version: 2,
|
|
createdAtMs: 0,
|
|
updatedAtMs: 1,
|
|
});
|
|
|
|
function binding(overrides = {}) {
|
|
return {
|
|
projectId: 'default',
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
version: 3,
|
|
state: 'active',
|
|
role: 'operator',
|
|
mutationId: 'grant-1',
|
|
changedBy: { type: 'user', id: 'usr_owner' },
|
|
createdAtMs: 2,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function engine(snapshot) {
|
|
return new ProjectPolicyEngine({
|
|
async resolve() {
|
|
if (snapshot instanceof Error) throw snapshot;
|
|
return snapshot;
|
|
},
|
|
async append() {
|
|
throw new Error('not used');
|
|
},
|
|
});
|
|
}
|
|
|
|
test('normalizes active and revoked bindings with exact state/role shape', () => {
|
|
assert.equal(normalizeProjectRoleBinding(binding()).role, 'operator');
|
|
const revoked = binding({ state: 'revoked' });
|
|
delete revoked.role;
|
|
assert.deepEqual(normalizeProjectRoleBinding(revoked), {
|
|
projectId: 'default',
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
version: 3,
|
|
state: 'revoked',
|
|
mutationId: 'grant-1',
|
|
changedBy: { type: 'user', id: 'usr_owner' },
|
|
createdAtMs: 2,
|
|
});
|
|
assert.throws(
|
|
() => normalizeProjectRoleBinding(binding({ state: 'revoked' })),
|
|
InvalidProjectPolicyValueError,
|
|
);
|
|
});
|
|
|
|
test('evaluates role matrix, archived state and immutable policy fences', async () => {
|
|
const policy = engine({ project: PROJECT, binding: binding() });
|
|
assert.deepEqual(
|
|
await policy.decide({
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
projectId: 'default',
|
|
permission: 'run.start',
|
|
}),
|
|
{
|
|
effect: 'allow',
|
|
reasons: ['role_grant'],
|
|
fence: { projectVersion: 2, bindingVersion: 3 },
|
|
},
|
|
);
|
|
assert.equal(
|
|
(
|
|
await policy.decide({
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
projectId: 'default',
|
|
permission: 'project.manage',
|
|
})
|
|
).effect,
|
|
'deny',
|
|
);
|
|
const archived = engine({
|
|
project: { ...PROJECT, status: 'archived' },
|
|
binding: binding({ role: 'owner' }),
|
|
});
|
|
assert.equal(
|
|
(
|
|
await archived.decide({
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
projectId: 'default',
|
|
permission: 'run.start',
|
|
})
|
|
).reasons[0],
|
|
'project_archived',
|
|
);
|
|
});
|
|
|
|
test('requires approval for an authorized agent write', async () => {
|
|
const policy = engine({
|
|
project: PROJECT,
|
|
binding: binding({
|
|
subject: { type: 'agent', id: 'agent_planner' },
|
|
role: 'operator',
|
|
}),
|
|
});
|
|
const decision = await policy.decide({
|
|
subject: { type: 'agent', id: 'agent_planner' },
|
|
projectId: 'default',
|
|
permission: 'run.start',
|
|
});
|
|
assert.equal(decision.effect, 'require_approval');
|
|
assert.deepEqual(decision.reasons, ['agent_action_requires_approval']);
|
|
});
|
|
|
|
test('treats approval discovery as read-only without granting decisions', async () => {
|
|
assert.equal(normalizeProjectPermission('approval.read'), 'approval.read');
|
|
for (const [role, expected] of [
|
|
['owner', 'allow'],
|
|
['admin', 'allow'],
|
|
['operator', 'allow'],
|
|
['viewer', 'allow'],
|
|
]) {
|
|
const subject = { type: 'agent', id: `agent_${role}` };
|
|
const decision = await engine({
|
|
project: PROJECT,
|
|
binding: binding({ subject, role }),
|
|
}).decide({ subject, projectId: 'default', permission: 'approval.read' });
|
|
assert.equal(decision.effect, expected, role);
|
|
}
|
|
const decision = await engine({
|
|
project: PROJECT,
|
|
binding: binding({
|
|
subject: { type: 'agent', id: 'agent_operator' },
|
|
role: 'operator',
|
|
}),
|
|
}).decide({
|
|
subject: { type: 'agent', id: 'agent_operator' },
|
|
projectId: 'default',
|
|
permission: 'approval.decide',
|
|
});
|
|
assert.equal(decision.effect, 'deny');
|
|
});
|
|
|
|
test('grants model invocation only to cost-bearing roles and approval-fences agents', async () => {
|
|
assert.equal(normalizeProjectPermission('model.invoke'), 'model.invoke');
|
|
for (const [role, subjectType, expected] of [
|
|
['owner', 'user', 'allow'],
|
|
['admin', 'user', 'allow'],
|
|
['operator', 'user', 'allow'],
|
|
['viewer', 'user', 'deny'],
|
|
['operator', 'agent', 'require_approval'],
|
|
]) {
|
|
const subject = { type: subjectType, id: `${subjectType}_${role}` };
|
|
const decision = await engine({
|
|
project: PROJECT,
|
|
binding: binding({ subject, role }),
|
|
}).decide({
|
|
subject,
|
|
projectId: 'default',
|
|
permission: 'model.invoke',
|
|
});
|
|
assert.equal(decision.effect, expected, `${subjectType}/${role}`);
|
|
}
|
|
});
|
|
|
|
test('limits package administration to admin/owner and approval-fences agents', async () => {
|
|
assert.equal(normalizeProjectPermission('package.manage'), 'package.manage');
|
|
assert.throws(
|
|
() => normalizeProjectPermission('package.install'),
|
|
InvalidProjectPolicyValueError,
|
|
);
|
|
for (const [role, subjectType, expected] of [
|
|
['owner', 'user', 'allow'],
|
|
['admin', 'user', 'allow'],
|
|
['operator', 'user', 'deny'],
|
|
['viewer', 'user', 'deny'],
|
|
['admin', 'agent', 'require_approval'],
|
|
]) {
|
|
const decision = await engine({
|
|
project: PROJECT,
|
|
binding: binding({
|
|
subject: { type: subjectType, id: `${subjectType}_${role}` },
|
|
role,
|
|
}),
|
|
}).decide({
|
|
subject: { type: subjectType, id: `${subjectType}_${role}` },
|
|
projectId: 'default',
|
|
permission: 'package.manage',
|
|
});
|
|
assert.equal(decision.effect, expected, `${subjectType}/${role}`);
|
|
}
|
|
});
|
|
|
|
test('denies missing bindings and fails closed on corrupt or unavailable storage', async () => {
|
|
assert.equal(
|
|
(
|
|
await engine({ project: PROJECT }).decide({
|
|
subject: { type: 'api_app', id: 'app_reader' },
|
|
projectId: 'default',
|
|
permission: 'run.read',
|
|
})
|
|
).reasons[0],
|
|
'subject_unbound',
|
|
);
|
|
await assert.rejects(
|
|
engine(new Error('driver detail')).decide({
|
|
subject: { type: 'user', id: 'usr_primary' },
|
|
projectId: 'default',
|
|
permission: 'run.read',
|
|
}),
|
|
ProjectPolicyUnavailableError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeProjectPolicySnapshot({
|
|
project: PROJECT,
|
|
binding: binding({ projectId: 'other' }),
|
|
}),
|
|
InvalidProjectPolicyValueError,
|
|
);
|
|
});
|