mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 10:32:40 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user