mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): add request-scoped console task creation
This commit is contained in:
@@ -22,6 +22,7 @@ function request(overrides = {}) {
|
||||
runId: 'run_123',
|
||||
}),
|
||||
authorization: 'Bearer opaque',
|
||||
localPresence: null,
|
||||
signal: new AbortController().signal,
|
||||
...overrides,
|
||||
});
|
||||
@@ -35,6 +36,17 @@ function fixture(overrides = {}) {
|
||||
events.push('authenticate');
|
||||
return Object.freeze({
|
||||
principal: PRINCIPAL,
|
||||
credentialFence: Object.freeze({
|
||||
credentialId: 'credential-local',
|
||||
credentialVersion: 1,
|
||||
pepperKeyId: 'owner-v1',
|
||||
materialDigest: 'a'.repeat(64),
|
||||
subjectType: 'user',
|
||||
subjectId: 'usr_local',
|
||||
secretDigest: 'b'.repeat(64),
|
||||
notBeforeAtMs: 1,
|
||||
expiresAtMs: 20_000,
|
||||
}),
|
||||
async confirm() {
|
||||
events.push('confirm');
|
||||
},
|
||||
@@ -127,6 +139,12 @@ function fixture(overrides = {}) {
|
||||
return { statusCode: 202, body: { status: 'accepted' } };
|
||||
},
|
||||
},
|
||||
taskPutRoute: {
|
||||
async handle(value) {
|
||||
events.push(`task-put:${value.projectId}:${value.taskId}`);
|
||||
return { statusCode: 201, body: { status: 'created' } };
|
||||
},
|
||||
},
|
||||
now: () => 10_000,
|
||||
randomUuid: () => '019f70c0-0000-4000-8000-000000000002',
|
||||
...overrides,
|
||||
@@ -382,6 +400,28 @@ test('authorizes and audits run.start before exposing the Task body handler', as
|
||||
assert.equal(events.at(-1), 'task-start:prj_default:task-a');
|
||||
});
|
||||
|
||||
test('defers Task put Policy, audit and strong confirmation to the request-bound route', async () => {
|
||||
const { admission, events } = fixture();
|
||||
const prepared = await admission.prepare(
|
||||
request({
|
||||
operation: Object.freeze({
|
||||
operationId: 'task.put',
|
||||
projectId: 'prj_default',
|
||||
taskId: 'task-a',
|
||||
}),
|
||||
localPresence: 'ql3p_proof',
|
||||
}),
|
||||
);
|
||||
assert.equal(prepared.bodyMode, 'json');
|
||||
assert.equal(prepared.maximumBodyBytes, 72 * 1024);
|
||||
assert.deepEqual(events, ['authenticate']);
|
||||
assert.deepEqual(await prepared.handle({ name: 'Task' }), {
|
||||
statusCode: 201,
|
||||
body: { status: 'created' },
|
||||
});
|
||||
assert.deepEqual(events, ['authenticate', 'task-put:prj_default:task-a']);
|
||||
});
|
||||
|
||||
test('audits authentication rejection before returning a challenge', async () => {
|
||||
const events = [];
|
||||
const { admission } = fixture({
|
||||
|
||||
@@ -76,6 +76,15 @@ test('loads one bounded offline Console asset closure', () => {
|
||||
assert.match(text, /const LOG_READ_BYTES = 32 \* 1024/u);
|
||||
assert.match(text, /new TextDecoder\('utf-8'\)/u);
|
||||
assert.match(text, /日志已按保留策略清理/u);
|
||||
assert.match(text, /method: 'PUT'/u);
|
||||
assert.match(text, /x-qinglong-local-presence/u);
|
||||
assert.match(text, /local_presence_required/u);
|
||||
assert.match(text, /state\.pendingTaskMutation/u);
|
||||
}
|
||||
if (requestPath === '/') {
|
||||
assert.match(text, /id="task-editor-dialog"/u);
|
||||
assert.match(text, /id="presence-dialog"/u);
|
||||
assert.match(text, /保存并生成本机证明/u);
|
||||
}
|
||||
}
|
||||
assert.ok(totalBytes <= 192 * 1024);
|
||||
|
||||
@@ -52,12 +52,17 @@ function request(port, path, options = {}) {
|
||||
function preparedAdmission(handler) {
|
||||
return {
|
||||
async prepare(value) {
|
||||
const json = ['run.cancel', 'task.start'].includes(
|
||||
const json = ['run.cancel', 'task.start', 'task.put'].includes(
|
||||
value.operation.operationId,
|
||||
);
|
||||
return {
|
||||
bodyMode: json ? 'json' : 'none',
|
||||
maximumBodyBytes: json ? 512 : 0,
|
||||
maximumBodyBytes:
|
||||
value.operation.operationId === 'task.put'
|
||||
? 72 * 1024
|
||||
: json
|
||||
? 512
|
||||
: 0,
|
||||
handle(body) {
|
||||
return handler(value, body);
|
||||
},
|
||||
@@ -77,7 +82,8 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
observed.push(value);
|
||||
if (
|
||||
value.operation.operationId === 'run.cancel' ||
|
||||
value.operation.operationId === 'task.start'
|
||||
value.operation.operationId === 'task.start' ||
|
||||
value.operation.operationId === 'task.put'
|
||||
) {
|
||||
return { statusCode: 202, body: { accepted: body } };
|
||||
}
|
||||
@@ -283,12 +289,36 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
taskId: 'task_1',
|
||||
});
|
||||
|
||||
const taskPutBody = JSON.stringify({ name: 'Task one' });
|
||||
const taskPut = await request(
|
||||
port,
|
||||
'/api/v3/projects/prj_default/tasks/task_1',
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
authorization: 'Bearer opaque',
|
||||
'x-qinglong-local-presence': 'ql3p_request_bound_proof',
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(taskPutBody)),
|
||||
},
|
||||
body: taskPutBody,
|
||||
},
|
||||
);
|
||||
assert.equal(taskPut.statusCode, 202);
|
||||
assert.deepEqual(taskPut.body.accepted, JSON.parse(taskPutBody));
|
||||
assert.deepEqual(observed[8].operation, {
|
||||
operationId: 'task.put',
|
||||
projectId: 'prj_default',
|
||||
taskId: 'task_1',
|
||||
});
|
||||
assert.equal(observed[8].localPresence, 'ql3p_request_bound_proof');
|
||||
|
||||
const log = await request(
|
||||
port,
|
||||
'/api/v3/projects/prj_default/runs/run_123/attempts/attempt_1/log?offset=4&length=32',
|
||||
);
|
||||
assert.deepEqual(log.body, { range: { offset: 4, length: 32 } });
|
||||
assert.deepEqual(observed[8].operation, {
|
||||
assert.deepEqual(observed[9].operation, {
|
||||
operationId: 'run.log.read',
|
||||
projectId: 'prj_default',
|
||||
runId: 'run_123',
|
||||
@@ -374,7 +404,7 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
assert.equal(invalid.statusCode, 400);
|
||||
assert.deepEqual(invalid.body, { code: 'invalid_run_step_list_query' });
|
||||
}
|
||||
assert.equal(observed.length, 10);
|
||||
assert.equal(observed.length, 11);
|
||||
assert.deepEqual(
|
||||
await Promise.all([surface.stopAndDrain(), surface.stopAndDrain()]),
|
||||
['stopped', 'stopped'],
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
LocalPresenceProofUnavailableError,
|
||||
createLocalPresenceProofManager,
|
||||
} = require('../dist/authentication/localPresenceProof.js');
|
||||
|
||||
function root(t) {
|
||||
const value = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-presence-'));
|
||||
fs.chmodSync(value, 0o700);
|
||||
t.after(() => fs.rmSync(value, { recursive: true, force: true }));
|
||||
return value;
|
||||
}
|
||||
|
||||
function binding(overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestDigest: 'a'.repeat(64),
|
||||
credentialId: 'owner-console',
|
||||
credentialVersion: 1,
|
||||
subjectType: 'user',
|
||||
subjectId: 'owner',
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function uuidFactory() {
|
||||
let sequence = 0;
|
||||
return () => {
|
||||
sequence += 1;
|
||||
return `019f9000-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes a private request-bound proof and consumes it exactly once', (t) => {
|
||||
const deploymentRoot = root(t);
|
||||
const manager = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'edge',
|
||||
now: () => 1_000,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 7),
|
||||
});
|
||||
t.after(() => manager.close());
|
||||
|
||||
const challenge = manager.issue(binding());
|
||||
const directory = path.join(deploymentRoot, 'console-presence');
|
||||
const filePath = path.join(directory, challenge.proofFileName);
|
||||
assert.equal(fs.statSync(directory).mode & 0o777, 0o700);
|
||||
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
|
||||
const payload = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
assert.equal(payload.authorizationId, challenge.authorizationId);
|
||||
assert.equal(payload.requestDigest, challenge.requestDigest);
|
||||
assert.match(payload.proof, /^ql3p_/);
|
||||
assert.equal(JSON.stringify(payload).includes('owner-console'), false);
|
||||
assert.equal(JSON.stringify(payload).includes('owner'), false);
|
||||
|
||||
const consumed = manager.consume(payload.proof, binding());
|
||||
assert.deepEqual(consumed, {
|
||||
authorizationId: challenge.authorizationId,
|
||||
authenticatedAtMs: 1_000,
|
||||
expiresAtMs: 121_000,
|
||||
});
|
||||
assert.equal(fs.existsSync(filePath), false);
|
||||
assert.equal(manager.consume(payload.proof, binding()), null);
|
||||
});
|
||||
|
||||
test('rejects wrong request, credential and proof without consuming the valid authorization', (t) => {
|
||||
const deploymentRoot = root(t);
|
||||
const manager = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'standalone',
|
||||
now: () => 2_000,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 9),
|
||||
});
|
||||
t.after(() => manager.close());
|
||||
const challenge = manager.issue(binding());
|
||||
const filePath = path.join(
|
||||
deploymentRoot,
|
||||
'console-presence',
|
||||
challenge.proofFileName,
|
||||
);
|
||||
const proof = JSON.parse(fs.readFileSync(filePath, 'utf8')).proof;
|
||||
assert.equal(
|
||||
manager.consume(proof, binding({ requestDigest: 'b'.repeat(64) })),
|
||||
null,
|
||||
);
|
||||
assert.equal(manager.consume(proof, binding({ credentialVersion: 2 })), null);
|
||||
assert.equal(manager.consume(`${proof.slice(0, -1)}A`, binding()), null);
|
||||
assert.equal(fs.existsSync(filePath), true);
|
||||
assert.equal(
|
||||
manager.consume(proof, binding()).authorizationId,
|
||||
challenge.authorizationId,
|
||||
);
|
||||
});
|
||||
|
||||
test('bounds pending Edge authorizations and lazily removes expired proof files', (t) => {
|
||||
const deploymentRoot = root(t);
|
||||
let now = 3_000;
|
||||
const manager = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'edge',
|
||||
now: () => now,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 11),
|
||||
});
|
||||
t.after(() => manager.close());
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
manager.issue(
|
||||
binding({ requestDigest: index.toString(16).padStart(64, '0') }),
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() => manager.issue(binding({ requestDigest: 'f'.repeat(64) })),
|
||||
LocalPresenceProofUnavailableError,
|
||||
);
|
||||
assert.equal(
|
||||
fs.readdirSync(path.join(deploymentRoot, 'console-presence')).length,
|
||||
8,
|
||||
);
|
||||
now += 120_000;
|
||||
manager.issue(binding({ requestDigest: 'f'.repeat(64) }));
|
||||
assert.equal(
|
||||
fs.readdirSync(path.join(deploymentRoot, 'console-presence')).length,
|
||||
1,
|
||||
);
|
||||
manager.close();
|
||||
assert.equal(
|
||||
fs.readdirSync(path.join(deploymentRoot, 'console-presence')).length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
@@ -41,7 +41,7 @@ const {
|
||||
LocalRunAttemptLogRangeReader,
|
||||
} = require('../../ql3-local-execution/dist/artifact-read/localRunAttemptLogRangeReader.js');
|
||||
|
||||
const NOW = 1_800_000_000_000;
|
||||
const NOW = Date.now();
|
||||
const PEPPER_KEY_ID = 'local-api-pepper-v1';
|
||||
const CREDENTIAL_ID = 'local-api-owner';
|
||||
const RUN_ID = 'run_local_api_1';
|
||||
@@ -437,6 +437,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
},
|
||||
),
|
||||
taskDefinitions: runtime.taskDefinitions,
|
||||
taskDefinitionAdministrationForCredential:
|
||||
runtime.taskDefinitionAdministrationForCredential,
|
||||
apiCredentials: runtime.apiCredentials,
|
||||
ownerPepper: runtime.ownerPepper,
|
||||
projectPolicy: runtime.projectPolicy,
|
||||
@@ -526,6 +528,64 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
{ statusCode: 404, body: { code: 'task_not_found' } },
|
||||
);
|
||||
|
||||
const taskCreateBody = JSON.stringify({
|
||||
expectedRevision: null,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000701',
|
||||
name: 'Console-created Task',
|
||||
description: 'Created through request-scoped local presence',
|
||||
kind: 'command',
|
||||
spec: {
|
||||
schema: 'qinglong/command@v1',
|
||||
config: {
|
||||
command: {
|
||||
kind: 'argv',
|
||||
file: '/bin/echo',
|
||||
args: ['console-created'],
|
||||
},
|
||||
},
|
||||
},
|
||||
labels: { source: 'local-console' },
|
||||
enabled: true,
|
||||
occurredAtMs: NOW,
|
||||
});
|
||||
const taskCreatePath = '/api/v3/projects/default/tasks/task-console-created';
|
||||
const taskCreateOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(taskCreateBody)),
|
||||
},
|
||||
body: taskCreateBody,
|
||||
};
|
||||
const challenge = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
taskCreatePath,
|
||||
taskCreateOptions,
|
||||
);
|
||||
assert.equal(challenge.statusCode, 428);
|
||||
assert.equal(challenge.body.code, 'local_presence_required');
|
||||
assert.match(challenge.body.requestDigest, /^[0-9a-f]{64}$/);
|
||||
const proofDocument = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(root, 'console-presence', challenge.body.proofFileName),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const taskCreated = await request(port, `Bearer ${TOKEN}`, taskCreatePath, {
|
||||
...taskCreateOptions,
|
||||
headers: {
|
||||
...taskCreateOptions.headers,
|
||||
'x-qinglong-local-presence': proofDocument.proof,
|
||||
},
|
||||
});
|
||||
assert.equal(taskCreated.statusCode, 201);
|
||||
assert.equal(taskCreated.body.status, 'created');
|
||||
assert.equal(taskCreated.body.task.taskId, 'task-console-created');
|
||||
assert.equal(taskCreated.body.task.revision, 1);
|
||||
assert.equal(JSON.stringify(taskCreated).includes('console-created'), true);
|
||||
assert.equal(JSON.stringify(taskCreated).includes('/bin/echo'), false);
|
||||
|
||||
const taskStartBody = JSON.stringify({
|
||||
schema: 'qinglong/task-start@v1',
|
||||
mutationId: '019f7300-0000-7000-8000-000000000800',
|
||||
@@ -685,7 +745,7 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
`SELECT operation_id, outcome FROM "QingLong3SecurityAuditEvents"
|
||||
WHERE operation_id IN (
|
||||
'run.get', 'run.list', 'run.events.list', 'run.steps.list',
|
||||
'run.cancel', 'task.get', 'task.list'
|
||||
'run.cancel', 'task.create', 'task.get', 'task.list'
|
||||
, 'task.start', 'run.log.read'
|
||||
)
|
||||
ORDER BY operation_id, outcome`,
|
||||
@@ -701,6 +761,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
'run.list:allowed',
|
||||
'run.log.read:allowed',
|
||||
'run.steps.list:allowed',
|
||||
'task.create:allowed',
|
||||
'task.create:approval_required',
|
||||
'task.get:allowed',
|
||||
'task.get:allowed',
|
||||
'task.list:allowed',
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
createTaskDefinitionRecord,
|
||||
} = require('@qinglong/runtime-core/task-definition');
|
||||
const {
|
||||
createLocalPresenceProofManager,
|
||||
} = require('../dist/authentication/localPresenceProof.js');
|
||||
const { createLocalApiTaskPutRoute } = require('../dist/task/taskPutRoute.js');
|
||||
|
||||
const PRINCIPAL = Object.freeze({
|
||||
subject: Object.freeze({ type: 'user', id: 'owner' }),
|
||||
authenticationId: 'local_credential:owner-console:1',
|
||||
authenticatedAtMs: 9_000,
|
||||
expiresAtMs: 60_000,
|
||||
assurance: 'single_factor',
|
||||
});
|
||||
|
||||
const FENCE = Object.freeze({
|
||||
credentialId: 'owner-console',
|
||||
credentialVersion: 1,
|
||||
pepperKeyId: 'owner-v1',
|
||||
materialDigest: 'a'.repeat(64),
|
||||
subjectType: 'user',
|
||||
subjectId: 'owner',
|
||||
secretDigest: 'b'.repeat(64),
|
||||
notBeforeAtMs: 1,
|
||||
expiresAtMs: 60_000,
|
||||
});
|
||||
|
||||
function taskBody(overrides = {}) {
|
||||
return Object.freeze({
|
||||
expectedRevision: null,
|
||||
mutationId: '019f9000-0000-4000-8000-000000000101',
|
||||
name: 'Presence-bound Task',
|
||||
description: 'Created from the Local Console mutation route',
|
||||
kind: 'command',
|
||||
spec: Object.freeze({
|
||||
schema: 'qinglong/command@v1',
|
||||
config: Object.freeze({
|
||||
command: Object.freeze({
|
||||
kind: 'argv',
|
||||
file: '/bin/echo',
|
||||
args: Object.freeze(['hello']),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
labels: Object.freeze({ 'qinglong.test': 'presence' }),
|
||||
enabled: true,
|
||||
occurredAtMs: 10_000,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function uuidFactory() {
|
||||
let sequence = 200;
|
||||
return () => {
|
||||
sequence += 1;
|
||||
return `019f9000-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(t, overrides = {}) {
|
||||
const deploymentRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-task-put-'),
|
||||
);
|
||||
fs.chmodSync(deploymentRoot, 0o700);
|
||||
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
||||
let now = 10_000;
|
||||
const calls = [];
|
||||
const presenceProof = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'edge',
|
||||
now: () => now,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 13),
|
||||
});
|
||||
t.after(() => presenceProof.close());
|
||||
const projectPolicy = {
|
||||
async resolve(projectId, subject) {
|
||||
calls.push(['policy', projectId, subject]);
|
||||
return {
|
||||
project: {
|
||||
id: projectId,
|
||||
name: 'Default',
|
||||
slug: 'default',
|
||||
status: 'active',
|
||||
version: 3,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
},
|
||||
binding: {
|
||||
projectId,
|
||||
subject,
|
||||
version: 5,
|
||||
state: 'active',
|
||||
role: 'owner',
|
||||
mutationId: 'owner-binding',
|
||||
changedBy: { type: 'user', id: 'bootstrap-owner' },
|
||||
createdAtMs: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
async append() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
};
|
||||
const taskDefinitions = {
|
||||
async findCurrentTaskDefinition() {
|
||||
return null;
|
||||
},
|
||||
async findTaskDefinitionRevision() {
|
||||
return null;
|
||||
},
|
||||
async listTaskDefinitions() {
|
||||
return { definitions: [], truncated: false };
|
||||
},
|
||||
};
|
||||
const route = createLocalApiTaskPutRoute({
|
||||
projectPolicy,
|
||||
taskDefinitions,
|
||||
taskDefinitionAdministrationForCredential(fence) {
|
||||
calls.push(['credential-fence', fence]);
|
||||
return {
|
||||
async appendAuthorizedTaskDefinitionRevision(mutation) {
|
||||
calls.push(['mutation', mutation]);
|
||||
return {
|
||||
status:
|
||||
mutation.command.expectedRevision === null
|
||||
? 'created'
|
||||
: 'updated',
|
||||
definition: createTaskDefinitionRecord(mutation.command, now),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
securityAudit: {
|
||||
async record(record) {
|
||||
calls.push(['audit', record]);
|
||||
},
|
||||
},
|
||||
presenceProof,
|
||||
now: () => now,
|
||||
randomUuid: uuidFactory(),
|
||||
...overrides,
|
||||
});
|
||||
const authenticated = Object.freeze({
|
||||
principal: PRINCIPAL,
|
||||
credentialFence: FENCE,
|
||||
async confirm() {
|
||||
calls.push(['confirm']);
|
||||
},
|
||||
});
|
||||
return {
|
||||
route,
|
||||
calls,
|
||||
deploymentRoot,
|
||||
authenticated,
|
||||
setNow(value) {
|
||||
now = value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function request(state, body, overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestId: 'local:019f9000-0000-4000-8000-000000000301',
|
||||
projectId: 'default',
|
||||
taskId: 'task-console',
|
||||
body,
|
||||
presence: null,
|
||||
authenticated: state.authenticated,
|
||||
signal: new AbortController().signal,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function readProof(state, response) {
|
||||
const value = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
state.deploymentRoot,
|
||||
'console-presence',
|
||||
response.body.proofFileName,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
return value.proof;
|
||||
}
|
||||
|
||||
test('requires local presence, re-confirms the credential and commits Policy/audit/mutation through a request fence', async (t) => {
|
||||
const state = fixture(t);
|
||||
const body = taskBody();
|
||||
const challenge = await state.route.handle(request(state, body));
|
||||
assert.equal(challenge.statusCode, 428);
|
||||
assert.equal(challenge.body.code, 'local_presence_required');
|
||||
assert.match(challenge.body.requestDigest, /^[a-f0-9]{64}$/);
|
||||
assert.match(challenge.body.proofFileName, /^[0-9a-f-]+\.json$/);
|
||||
|
||||
const proof = readProof(state, challenge);
|
||||
const created = await state.route.handle(
|
||||
request(state, body, { presence: proof }),
|
||||
);
|
||||
assert.equal(created.statusCode, 201);
|
||||
assert.equal(created.body.status, 'created');
|
||||
assert.equal(created.body.task.taskId, 'task-console');
|
||||
assert.equal(created.body.task.revision, 1);
|
||||
assert.equal(state.calls.filter(([kind]) => kind === 'confirm').length, 1);
|
||||
assert.equal(
|
||||
state.calls.filter(([kind]) => kind === 'credential-fence').length,
|
||||
1,
|
||||
);
|
||||
const mutation = state.calls.find(([kind]) => kind === 'mutation')[1];
|
||||
assert.equal(mutation.actor.type, 'user');
|
||||
assert.equal(mutation.actor.id, 'owner');
|
||||
assert.deepEqual(mutation.fence, {
|
||||
projectVersion: 3,
|
||||
bindingVersion: 5,
|
||||
});
|
||||
assert.equal(mutation.audit.outcome, 'allowed');
|
||||
assert.equal(
|
||||
mutation.audit.authenticationId.startsWith('local_presence:'),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(
|
||||
state.calls
|
||||
.filter(([kind]) => kind === 'audit')
|
||||
.map(([, audit]) => [audit.operationId, audit.outcome, audit.reasons[0]]),
|
||||
[['task.create', 'approval_required', 'local_presence_required']],
|
||||
);
|
||||
});
|
||||
|
||||
test('binds the proof to exact Task content and leaves it usable only for the original request', async (t) => {
|
||||
const state = fixture(t);
|
||||
const body = taskBody();
|
||||
const challenge = await state.route.handle(request(state, body));
|
||||
const proof = readProof(state, challenge);
|
||||
const changed = await state.route.handle(
|
||||
request(state, taskBody({ name: 'Changed after challenge' }), {
|
||||
presence: proof,
|
||||
}),
|
||||
);
|
||||
assert.deepEqual(changed, {
|
||||
statusCode: 401,
|
||||
body: { code: 'local_presence_rejected' },
|
||||
});
|
||||
const created = await state.route.handle(
|
||||
request(state, body, { presence: proof }),
|
||||
);
|
||||
assert.equal(created.statusCode, 201);
|
||||
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 1);
|
||||
});
|
||||
|
||||
test('fails closed for malformed bodies, non-User credentials and expired presence proofs', async (t) => {
|
||||
const state = fixture(t);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(request(state, { name: 'partial' })),
|
||||
{ statusCode: 400, body: { code: 'invalid_task_definition' } },
|
||||
);
|
||||
const serviceCredential = Object.freeze({
|
||||
...state.authenticated,
|
||||
principal: Object.freeze({
|
||||
...PRINCIPAL,
|
||||
subject: Object.freeze({ type: 'system', id: 'runtime' }),
|
||||
assurance: 'service',
|
||||
}),
|
||||
credentialFence: Object.freeze({
|
||||
...FENCE,
|
||||
subjectType: 'system',
|
||||
subjectId: 'runtime',
|
||||
}),
|
||||
});
|
||||
assert.deepEqual(
|
||||
await state.route.handle(
|
||||
request(state, taskBody(), { authenticated: serviceCredential }),
|
||||
),
|
||||
{ statusCode: 401, body: { code: 'strong_authentication_required' } },
|
||||
);
|
||||
const challenge = await state.route.handle(request(state, taskBody()));
|
||||
const proof = readProof(state, challenge);
|
||||
state.setNow(challenge.body.expiresAtMs);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(request(state, taskBody(), { presence: proof })),
|
||||
{ statusCode: 401, body: { code: 'local_presence_rejected' } },
|
||||
);
|
||||
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user