Files
qinglong/packages/ql3-cluster-admin/test/modelProviderCredentialManagement.test.cjs
T

332 lines
10 KiB
JavaScript

const assert = require('node:assert/strict');
const test = require('node:test');
const {
ModelProviderCredentialAdministrationAuthorizationFenceConflictError,
} = require('@qinglong/ai/model-provider-credential-administration');
const {
createModelProviderCredentialTransition,
} = require('@qinglong/ai/model-provider-credential-catalog');
const {
createModelProviderCredentialTestAllowlist,
} = require('@qinglong/ai/model-provider-credential-test-connection');
const {
ClusterModelProviderCredentialManagementAuthenticationError,
ClusterModelProviderCredentialManagementAuthorizationError,
ClusterModelProviderCredentialManagementConflictError,
ClusterModelProviderCredentialManagementRequestError,
createClusterModelProviderCredentialManagementService,
} = require('../dist/model-provider-credential/modelProviderCredentialManagement.js');
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
const NOW_MS = 1_000_000;
const MUTATION_ID = '019f7094-a853-4f3b-82ab-dfa08e6bd1c1';
function principal(overrides = {}) {
return {
subject: { type: 'user', id: 'owner-a' },
authenticationId: 'authentication-1',
authenticatedAtMs: NOW_MS - 1_000,
expiresAtMs: NOW_MS + 60_000,
assurance: 'multi_factor',
...overrides,
};
}
function bindRequest(overrides = {}) {
return {
requestId: 'request-1',
mutationId: MUTATION_ID,
projectId: 'project-a',
provider: 'openai-compatible',
expectedGeneration: 0,
revision: 'credential-v1',
secretRef: createSecretRef({
projectId: 'project-a',
name: 'openai-token',
}),
principal: principal(),
...overrides,
};
}
function fixture(overrides = {}) {
const calls = { policy: [], mutations: [], audits: [], testPlans: [] };
const policy = {
async authorize(...args) {
calls.policy.push(args);
return (
overrides.decision ?? {
effect: 'allow',
reasons: ['project_owner'],
fence: { projectVersion: 3, bindingVersion: 7 },
}
);
},
};
const credentials = {
async findCurrentTransition() {
return null;
},
async commit() {
throw new Error('raw commit must not be used');
},
async commitAuthorized(mutation) {
calls.mutations.push(mutation);
if (overrides.repositoryError) throw overrides.repositoryError;
return {
status: 'created',
transition: createModelProviderCredentialTransition(
mutation.command,
null,
NOW_MS,
),
};
},
};
const audit = {
async listAuthorized(query) {
calls.audits.push(query);
if (overrides.auditError) throw overrides.auditError;
return {
projectId: query.query.projectId,
records: [
{
eventId: MUTATION_ID,
requestId: 'request-1',
operation: 'provider-credential.bind',
actor: { type: 'user', id: 'owner-a' },
fence: { projectVersion: 3, bindingVersion: 7 },
occurredAtMs: NOW_MS - 1,
},
],
nextCursor: null,
};
},
};
const testAllowlist = createModelProviderCredentialTestAllowlist({
revision: 'catalog-v1',
providers: [
{
provider: 'openai-compatible',
adapter: 'openai-compatible',
baseUrl: 'https://provider.example.test/v1/',
revision: 'endpoint-v1',
deadlineMs: 5_000,
maxResponseBytes: 64 * 1_024,
maxModels: 64,
maxCostMicrousd: 0,
retryLimit: 0,
},
],
});
const testPlans = {
async createAuthorized(value) {
calls.testPlans.push(value);
if (overrides.testPlanError) throw overrides.testPlanError;
return { status: 'created', plan: value.plan };
},
};
return {
calls,
service: createClusterModelProviderCredentialManagementService({
policy,
credentials,
audit,
testPlans,
testAllowlist,
testPlanLifetimeMs: 60_000,
now: () => NOW_MS,
}),
};
}
test('test connection plan selects endpoint and hard budgets server-side', async () => {
const { service, calls } = fixture();
const testId = '319f7094-a853-4f3b-82ab-dfa08e6bd1c4';
const result = await service.planTestConnection({
requestId: 'test-request-1',
testId,
projectId: 'project-a',
provider: 'openai-compatible',
principal: principal(),
});
assert.equal(result.status, 'created');
assert.equal(result.plan.testId, testId);
assert.equal(
result.plan.endpoint.baseUrl,
'https://provider.example.test/v1/',
);
assert.equal(result.plan.endpoint.maxCostMicrousd, 0);
assert.equal(result.plan.endpoint.retryLimit, 0);
assert.equal(result.plan.expiresAtMs - result.plan.plannedAtMs, 60_000);
assert.equal(calls.policy[0][2], 'secret.manage');
assert.equal(calls.testPlans.length, 1);
assert.equal(
calls.testPlans[0].audit.operationId,
'model_provider_credential.test.plan',
);
assert.doesNotMatch(JSON.stringify(calls.testPlans[0]), /secretRef|token/i);
});
test('test connection request cannot supply URL, budgets or SecretRef', async () => {
const { service, calls } = fixture();
const base = {
requestId: 'test-request-1',
testId: '419f7094-a853-4f3b-82ab-dfa08e6bd1c5',
projectId: 'project-a',
provider: 'openai-compatible',
principal: principal(),
};
for (const widened of [
{ baseUrl: 'https://attacker.example/v1/' },
{ deadlineMs: 60_000 },
{ secretRef: 'qlsecret:project-a/openai-token' },
]) {
await assert.rejects(
service.planTestConnection({ ...base, ...widened }),
ClusterModelProviderCredentialManagementRequestError,
);
}
assert.equal(calls.policy.length, 0);
assert.equal(calls.testPlans.length, 0);
});
test('bind derives actor and exact secret.manage authority server-side', async () => {
const { service, calls } = fixture();
const result = await service.bind(bindRequest());
assert.equal(result.status, 'created');
assert.equal(result.transition.activeBindingRevision, 'credential-v1');
assert.equal(calls.policy.length, 1);
assert.equal(calls.policy[0][1], 'project-a');
assert.equal(calls.policy[0][2], 'secret.manage');
assert.equal(calls.mutations.length, 1);
assert.deepEqual(calls.mutations[0].command.changedBy, {
type: 'user',
id: 'owner-a',
});
assert.equal(calls.mutations[0].audit.outcome, 'allowed');
assert.equal(
calls.mutations[0].audit.operationId,
'model_provider_credential.bind',
);
assert.equal(
JSON.stringify(calls.mutations[0]).includes('authorization'),
false,
);
});
test('revoke is Project-fenced and cannot carry binding material', async () => {
const { service, calls } = fixture();
const request = bindRequest();
const result = await service.revoke({
requestId: 'request-2',
mutationId: '119f7094-a853-4f3b-82ab-dfa08e6bd1c2',
projectId: request.projectId,
provider: request.provider,
expectedGeneration: 0,
principal: request.principal,
});
assert.equal(result.transition.action, 'revoke');
assert.equal(calls.mutations[0].command.binding, null);
assert.equal(
calls.mutations[0].audit.operationId,
'model_provider_credential.revoke',
);
});
test('weak, stale and denied principals fail before repository mutation', async () => {
for (const scenario of [
{
request: bindRequest({
principal: principal({ assurance: 'single_factor' }),
}),
error: ClusterModelProviderCredentialManagementAuthenticationError,
},
{
request: bindRequest({
principal: principal({ authenticatedAtMs: NOW_MS - 300_001 }),
}),
error: ClusterModelProviderCredentialManagementAuthenticationError,
},
{
decision: { effect: 'deny', reasons: ['policy_denied'], fence: null },
request: bindRequest(),
error: ClusterModelProviderCredentialManagementAuthorizationError,
},
]) {
const { service, calls } = fixture({ decision: scenario.decision });
await assert.rejects(service.bind(scenario.request), scenario.error);
assert.equal(calls.mutations.length, 0);
}
});
test('request widening and durable fence drift fail closed', async () => {
const { service } = fixture();
await assert.rejects(
service.bind({
...bindRequest(),
changedBy: { type: 'user', id: 'owner-b' },
}),
ClusterModelProviderCredentialManagementRequestError,
);
await assert.rejects(
service.bind({ ...bindRequest(), secretRef: 'not-a-canonical-secret-ref' }),
ClusterModelProviderCredentialManagementRequestError,
);
const conflict = fixture({
repositoryError:
new ModelProviderCredentialAdministrationAuthorizationFenceConflictError(),
});
await assert.rejects(
conflict.service.bind(bindRequest()),
ClusterModelProviderCredentialManagementConflictError,
);
});
test('audit query reuses secret.manage and returns only content-free events', async () => {
const { service, calls } = fixture();
const result = await service.listAudit({
requestId: 'audit-request-1',
queryId: '219f7094-a853-4f3b-82ab-dfa08e6bd1c3',
projectId: 'project-a',
limit: 8,
principal: principal(),
});
assert.equal(calls.policy.length, 1);
assert.equal(calls.policy[0][2], 'secret.manage');
assert.equal(calls.audits.length, 1);
assert.equal(
calls.audits[0].audit.operationId,
'model_provider_credential.audit.list',
);
assert.deepEqual(calls.audits[0].fence, {
projectVersion: 3,
bindingVersion: 7,
});
assert.equal(result.records[0].operation, 'provider-credential.bind');
assert.doesNotMatch(
JSON.stringify(result),
/secretRef|provider-token|bindingDigest|authenticationId|openai/i,
);
});
test('invalid audit cursors fail before policy or storage', async () => {
const { service, calls } = fixture();
await assert.rejects(
service.listAudit({
requestId: 'audit-request-1',
queryId: '219f7094-a853-4f3b-82ab-dfa08e6bd1c3',
projectId: 'project-a',
limit: 8,
before: { occurredAtMs: 1, eventId: 'not-a-uuid' },
principal: principal(),
}),
ClusterModelProviderCredentialManagementRequestError,
);
assert.equal(calls.policy.length, 0);
assert.equal(calls.audits.length, 0);
});