mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): add secret-backed console automation
This commit is contained in:
@@ -86,6 +86,51 @@ test('defers Trigger put Policy, presence and mutation to the route', async () =
|
||||
]);
|
||||
});
|
||||
|
||||
test('uses secret.manage for bounded Secret metadata and never widens the route input', async () => {
|
||||
const { admission, events } = fixture();
|
||||
assert.deepEqual(
|
||||
await execute(
|
||||
admission,
|
||||
request({
|
||||
operation: {
|
||||
operationId: 'secret.list',
|
||||
projectId: 'prj_default',
|
||||
limit: 16,
|
||||
after: { name: 'alpha' },
|
||||
},
|
||||
}),
|
||||
),
|
||||
{ statusCode: 200, body: { secrets: [], truncated: false } },
|
||||
);
|
||||
assert.deepEqual(events, [
|
||||
'authenticate',
|
||||
'authorize:secret.manage:prj_default',
|
||||
'audit:allowed:secret.list',
|
||||
'confirm',
|
||||
'secrets:prj_default:16',
|
||||
]);
|
||||
});
|
||||
|
||||
test('defers Secret put Policy, presence and plaintext body to the fenced route', async () => {
|
||||
const { admission, events } = fixture();
|
||||
const prepared = await admission.prepare(
|
||||
request({
|
||||
localPresence: 'ql3p_bound',
|
||||
operation: {
|
||||
operationId: 'secret.put',
|
||||
projectId: 'prj_default',
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(prepared.bodyMode, 'json');
|
||||
assert.equal(prepared.maximumBodyBytes, 20 * 1024);
|
||||
assert.deepEqual(await prepared.handle({ plaintext: 'ephemeral' }), {
|
||||
statusCode: 201,
|
||||
body: { status: 'inserted' },
|
||||
});
|
||||
assert.deepEqual(events, ['authenticate', 'secret-put:prj_default']);
|
||||
});
|
||||
|
||||
function request(overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestId: 'local:019f70c0-0000-7000-8000-000000000001',
|
||||
@@ -246,6 +291,18 @@ function fixture(overrides = {}) {
|
||||
return { statusCode: 201, body: { status: 'created' } };
|
||||
},
|
||||
},
|
||||
secretListRoute: {
|
||||
async handle(value) {
|
||||
events.push(`secrets:${value.projectId}:${value.limit}`);
|
||||
return { statusCode: 200, body: { secrets: [], truncated: false } };
|
||||
},
|
||||
},
|
||||
secretPutRoute: {
|
||||
async handle(value) {
|
||||
events.push(`secret-put:${value.projectId}`);
|
||||
return { statusCode: 201, body: { status: 'inserted' } };
|
||||
},
|
||||
},
|
||||
now: () => 10_000,
|
||||
randomUuid: () => '019f70c0-0000-4000-8000-000000000002',
|
||||
...overrides,
|
||||
|
||||
@@ -90,6 +90,15 @@ test('loads one bounded offline Console asset closure', () => {
|
||||
assert.match(text, /triggers\/\$\{mutation\.triggerId\}/u);
|
||||
assert.match(text, /state\.view === 'triggers'/u);
|
||||
assert.match(text, /trigger_fence_rejected/u);
|
||||
assert.match(text, /state\.view === 'secrets'/u);
|
||||
assert.match(text, /secret-mutation/u);
|
||||
assert.match(text, /createSecretRef/u);
|
||||
assert.match(text, /kind: 'secret'/u);
|
||||
assert.match(text, /secret_query_unavailable/u);
|
||||
assert.equal(
|
||||
/localStorage.*plaintext|sessionStorage.*plaintext/u.test(text),
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (requestPath === '/') {
|
||||
assert.match(text, /id="task-editor-dialog"/u);
|
||||
@@ -99,6 +108,10 @@ test('loads one bounded offline Console asset closure', () => {
|
||||
assert.match(text, /id="presence-copy"/u);
|
||||
assert.match(text, /id="trigger-editor-dialog"/u);
|
||||
assert.match(text, /data-view="triggers"/u);
|
||||
assert.match(text, /data-view="secrets"/u);
|
||||
assert.match(text, /id="secret-editor-dialog"/u);
|
||||
assert.match(text, /id="task-secret-bindings-input"/u);
|
||||
assert.match(text, /AES-256-GCM/u);
|
||||
}
|
||||
}
|
||||
assert.ok(totalBytes <= 192 * 1024);
|
||||
|
||||
@@ -52,14 +52,19 @@ function request(port, path, options = {}) {
|
||||
function preparedAdmission(handler) {
|
||||
return {
|
||||
async prepare(value) {
|
||||
const json = ['run.cancel', 'task.start', 'task.put'].includes(
|
||||
value.operation.operationId,
|
||||
);
|
||||
const json = [
|
||||
'run.cancel',
|
||||
'task.start',
|
||||
'task.put',
|
||||
'secret.put',
|
||||
].includes(value.operation.operationId);
|
||||
return {
|
||||
bodyMode: json ? 'json' : 'none',
|
||||
maximumBodyBytes:
|
||||
value.operation.operationId === 'task.put'
|
||||
? 72 * 1024
|
||||
: value.operation.operationId === 'secret.put'
|
||||
? 20 * 1024
|
||||
: json
|
||||
? 512
|
||||
: 0,
|
||||
@@ -83,7 +88,8 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
if (
|
||||
value.operation.operationId === 'run.cancel' ||
|
||||
value.operation.operationId === 'task.start' ||
|
||||
value.operation.operationId === 'task.put'
|
||||
value.operation.operationId === 'task.put' ||
|
||||
value.operation.operationId === 'secret.put'
|
||||
) {
|
||||
return { statusCode: 202, body: { accepted: body } };
|
||||
}
|
||||
@@ -140,6 +146,12 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
body: { task: { taskId: value.operation.taskId } },
|
||||
};
|
||||
}
|
||||
if (value.operation.operationId === 'secret.list') {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { secrets: [], truncated: false },
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: { runs: [], hasMore: false, input: value.operation.input },
|
||||
@@ -356,6 +368,41 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
});
|
||||
assert.equal(observed[11].localPresence, 'ql3p_authoring_read_proof');
|
||||
|
||||
const secrets = await request(
|
||||
port,
|
||||
'/api/v3/projects/prj_default/secrets?limit=8&after=YWxwaGE',
|
||||
);
|
||||
assert.deepEqual(secrets.body, { secrets: [], truncated: false });
|
||||
assert.deepEqual(observed[12].operation, {
|
||||
operationId: 'secret.list',
|
||||
projectId: 'prj_default',
|
||||
limit: 8,
|
||||
after: { name: 'alpha' },
|
||||
});
|
||||
|
||||
const secretPutBody = JSON.stringify({ name: 'github-token' });
|
||||
const secretPut = await request(
|
||||
port,
|
||||
'/api/v3/projects/prj_default/secrets',
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
authorization: 'Bearer opaque',
|
||||
'x-qinglong-local-presence': 'ql3p_secret_bound_proof',
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(secretPutBody)),
|
||||
},
|
||||
body: secretPutBody,
|
||||
},
|
||||
);
|
||||
assert.equal(secretPut.statusCode, 202);
|
||||
assert.deepEqual(secretPut.body.accepted, JSON.parse(secretPutBody));
|
||||
assert.deepEqual(observed[13].operation, {
|
||||
operationId: 'secret.put',
|
||||
projectId: 'prj_default',
|
||||
});
|
||||
assert.equal(observed[13].localPresence, 'ql3p_secret_bound_proof');
|
||||
|
||||
for (const invalidPath of [
|
||||
'/api/v3/projects/prj_default/runs/run_123?expanded=true',
|
||||
'/api/v3/projects/prj_default/runs/run%5f123',
|
||||
@@ -402,6 +449,17 @@ test('serves only the fixed canonical loopback Run route and drains idempotently
|
||||
assert.equal(invalid.statusCode, 400);
|
||||
assert.deepEqual(invalid.body, { code: 'invalid_task_list_query' });
|
||||
}
|
||||
for (const invalidQuery of [
|
||||
'/api/v3/projects/prj_default/secrets?',
|
||||
'/api/v3/projects/prj_default/secrets?limit=08',
|
||||
'/api/v3/projects/prj_default/secrets?limit=65',
|
||||
'/api/v3/projects/prj_default/secrets?after=Y',
|
||||
'/api/v3/projects/prj_default/secrets?unknown=value',
|
||||
]) {
|
||||
const invalid = await request(port, invalidQuery);
|
||||
assert.equal(invalid.statusCode, 400);
|
||||
assert.deepEqual(invalid.body, { code: 'invalid_secret_list_query' });
|
||||
}
|
||||
for (const invalidQuery of [
|
||||
'/api/v3/projects/prj_default/runs/run_123/events?',
|
||||
'/api/v3/projects/prj_default/runs/run_123/events?after_sequence=07',
|
||||
@@ -425,7 +483,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, 12);
|
||||
assert.equal(observed.length, 14);
|
||||
assert.deepEqual(
|
||||
await Promise.all([surface.stopAndDrain(), surface.stopAndDrain()]),
|
||||
['stopped', 'stopped'],
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
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 {
|
||||
createLocalPresenceProofManager,
|
||||
} = require('../dist/authentication/localPresenceProof.js');
|
||||
const {
|
||||
createLocalApiSecretListRoute,
|
||||
createLocalApiSecretPutRoute,
|
||||
} = require('../dist/secret/secretRoutes.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 uuidFactory() {
|
||||
let sequence = 400;
|
||||
return () => {
|
||||
sequence += 1;
|
||||
return `019f9200-0000-4000-8000-${String(sequence).padStart(12, '0')}`;
|
||||
};
|
||||
}
|
||||
|
||||
function body(overrides = {}) {
|
||||
return Object.freeze({
|
||||
name: 'github-token',
|
||||
plaintext: 'never-return-this-value',
|
||||
mutationId: '019f9200-0000-4000-8000-000000000101',
|
||||
expectedCurrentVersion: 0,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function fixture(t) {
|
||||
const deploymentRoot = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'ql3-secret-put-'),
|
||||
);
|
||||
fs.chmodSync(deploymentRoot, 0o700);
|
||||
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
||||
const calls = [];
|
||||
const presenceProof = createLocalPresenceProofManager({
|
||||
deploymentRoot,
|
||||
profile: 'edge',
|
||||
now: () => 10_000,
|
||||
randomUuid: uuidFactory(),
|
||||
randomSecret: () => Buffer.alloc(32, 17),
|
||||
});
|
||||
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 route = createLocalApiSecretPutRoute({
|
||||
projectPolicy,
|
||||
async secretAdministrationForCredential(fence) {
|
||||
calls.push(['credential-fence', fence]);
|
||||
return {
|
||||
async resolveLocalSecretAdministrationMutation() {
|
||||
return null;
|
||||
},
|
||||
async appendAuthorizedLocalSecretEnvelope(command) {
|
||||
calls.push(['mutation', command]);
|
||||
return {
|
||||
status: 'inserted',
|
||||
envelope: command.envelope,
|
||||
audit: command.audit,
|
||||
};
|
||||
},
|
||||
async record() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
};
|
||||
},
|
||||
securityAudit: {
|
||||
async record(record) {
|
||||
calls.push(['audit', record]);
|
||||
},
|
||||
},
|
||||
secretKeys: {
|
||||
async active() {
|
||||
calls.push(['active-key']);
|
||||
return { keyId: 'active-key', key: Buffer.alloc(32, 23) };
|
||||
},
|
||||
async resolve() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
presenceProof,
|
||||
now: () => 10_000,
|
||||
randomUuid: uuidFactory(),
|
||||
});
|
||||
const authenticated = Object.freeze({
|
||||
principal: PRINCIPAL,
|
||||
credentialFence: FENCE,
|
||||
async confirm() {
|
||||
calls.push(['confirm']);
|
||||
},
|
||||
});
|
||||
return { route, calls, deploymentRoot, authenticated };
|
||||
}
|
||||
|
||||
function request(state, requestBody, overrides = {}) {
|
||||
return Object.freeze({
|
||||
requestId: 'local:019f9200-0000-4000-8000-000000000301',
|
||||
projectId: 'default',
|
||||
body: requestBody,
|
||||
presence: null,
|
||||
authenticated: state.authenticated,
|
||||
signal: new AbortController().signal,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function readProof(state, challenge) {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
state.deploymentRoot,
|
||||
'console-presence',
|
||||
challenge.body.proofFileName,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
).proof;
|
||||
}
|
||||
|
||||
test('lists bounded Secret metadata without storage or mutation material', async () => {
|
||||
const route = createLocalApiSecretListRoute({
|
||||
async listLocalSecretMetadata(options) {
|
||||
assert.deepEqual(options, { projectId: 'default', limit: 1 });
|
||||
return {
|
||||
secrets: [
|
||||
{
|
||||
projectId: 'default',
|
||||
name: 'github-token',
|
||||
currentVersion: 2,
|
||||
createdAtMs: 10_000,
|
||||
},
|
||||
],
|
||||
truncated: true,
|
||||
next: { name: 'github-token' },
|
||||
};
|
||||
},
|
||||
});
|
||||
const result = await route.handle({ projectId: 'default', limit: 1 });
|
||||
assert.equal(result.statusCode, 200);
|
||||
assert.deepEqual(Object.keys(result.body.secrets[0]).sort(), [
|
||||
'createdAtMs',
|
||||
'currentVersion',
|
||||
'name',
|
||||
'secretRef',
|
||||
]);
|
||||
assert.equal(
|
||||
result.body.secrets[0].secretRef.startsWith('qlsecret:v1:'),
|
||||
true,
|
||||
);
|
||||
assert.equal(result.body.next.after, 'Z2l0aHViLXRva2Vu');
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(result.body),
|
||||
/cipher|plaintext|mutation|keyId/u,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed on widened, cross-Project and over-budget metadata', async () => {
|
||||
for (const page of [
|
||||
{
|
||||
secrets: [
|
||||
{
|
||||
projectId: 'other',
|
||||
name: 'github-token',
|
||||
currentVersion: 2,
|
||||
createdAtMs: 10_000,
|
||||
},
|
||||
],
|
||||
truncated: false,
|
||||
},
|
||||
{
|
||||
secrets: [
|
||||
{
|
||||
projectId: 'default',
|
||||
name: 'github-token',
|
||||
currentVersion: 2,
|
||||
createdAtMs: 10_000,
|
||||
ciphertext: 'forbidden',
|
||||
},
|
||||
],
|
||||
truncated: false,
|
||||
},
|
||||
{
|
||||
secrets: [
|
||||
{
|
||||
projectId: 'default',
|
||||
name: 'first',
|
||||
currentVersion: 1,
|
||||
createdAtMs: 10_000,
|
||||
},
|
||||
{
|
||||
projectId: 'default',
|
||||
name: 'second',
|
||||
currentVersion: 1,
|
||||
createdAtMs: 10_001,
|
||||
},
|
||||
],
|
||||
truncated: true,
|
||||
next: { name: 'second' },
|
||||
},
|
||||
]) {
|
||||
const route = createLocalApiSecretListRoute({
|
||||
async listLocalSecretMetadata() {
|
||||
return page;
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await route.handle({ projectId: 'default', limit: 1 }), {
|
||||
statusCode: 503,
|
||||
body: { code: 'secret_query_unavailable' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('requires exact local presence and returns no Secret plaintext', async (t) => {
|
||||
const state = fixture(t);
|
||||
const command = body();
|
||||
const challenge = await state.route.handle(request(state, command));
|
||||
assert.equal(challenge.statusCode, 428);
|
||||
const result = await state.route.handle(
|
||||
request(state, command, { presence: readProof(state, challenge) }),
|
||||
);
|
||||
assert.equal(result.statusCode, 201);
|
||||
assert.deepEqual(result.body, {
|
||||
status: 'inserted',
|
||||
secret: {
|
||||
name: 'github-token',
|
||||
currentVersion: 1,
|
||||
secretRef: result.body.secret.secretRef,
|
||||
},
|
||||
});
|
||||
assert.equal(JSON.stringify(result).includes(command.plaintext), false);
|
||||
const mutation = state.calls.find(([kind]) => kind === 'mutation')[1];
|
||||
assert.equal(
|
||||
Buffer.from(mutation.envelope.ciphertext, 'base64url').includes(
|
||||
Buffer.from(command.plaintext),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(
|
||||
state.calls
|
||||
.filter(([kind]) => kind === 'audit')
|
||||
.map(([, audit]) => [audit.operationId, audit.outcome, audit.reasons[0]]),
|
||||
[['secret.create', 'approval_required', 'local_presence_required']],
|
||||
);
|
||||
});
|
||||
|
||||
test('binds proof to exact plaintext digest and rejects widened bodies', async (t) => {
|
||||
const state = fixture(t);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(request(state, { ...body(), extra: true })),
|
||||
{ statusCode: 400, body: { code: 'invalid_secret' } },
|
||||
);
|
||||
const command = body();
|
||||
const challenge = await state.route.handle(request(state, command));
|
||||
const proof = readProof(state, challenge);
|
||||
assert.deepEqual(
|
||||
await state.route.handle(
|
||||
request(state, body({ plaintext: 'changed-value' }), { presence: proof }),
|
||||
),
|
||||
{ statusCode: 401, body: { code: 'local_presence_rejected' } },
|
||||
);
|
||||
assert.equal(state.calls.filter(([kind]) => kind === 'mutation').length, 0);
|
||||
});
|
||||
@@ -187,8 +187,8 @@ function seed(databasePath, materialDigest) {
|
||||
"role", "mutation_id", "changed_by_type", "changed_by_id",
|
||||
"created_at_ms"
|
||||
) VALUES (
|
||||
'default', 'user', 'local-api-user', 1, 'active', 'operator',
|
||||
'grant-local-api-operator', 'user', 'local-api-user', ?
|
||||
'default', 'user', 'local-api-user', 1, 'active', 'owner',
|
||||
'grant-local-api-owner', 'user', 'local-api-user', ?
|
||||
)`,
|
||||
)
|
||||
.run(NOW - 500);
|
||||
@@ -442,6 +442,19 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
triggers: runtime.triggers,
|
||||
triggerAdministrationForCredential:
|
||||
runtime.triggerAdministrationForCredential,
|
||||
localSecretMetadata: runtime.localSecretMetadata,
|
||||
localSecretAdministrationForCredential:
|
||||
runtime.localSecretAdministrationForCredential,
|
||||
localSecretKeys: {
|
||||
async active() {
|
||||
return { keyId: 'integration-key', key: Buffer.alloc(32, 83) };
|
||||
},
|
||||
async resolve(keyId) {
|
||||
return keyId === 'integration-key'
|
||||
? { keyId, key: Buffer.alloc(32, 83) }
|
||||
: null;
|
||||
},
|
||||
},
|
||||
apiCredentials: runtime.apiCredentials,
|
||||
ownerPepper: runtime.ownerPepper,
|
||||
projectPolicy: runtime.projectPolicy,
|
||||
@@ -531,6 +544,63 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
{ statusCode: 404, body: { code: 'task_not_found' } },
|
||||
);
|
||||
|
||||
const secretPlaintext = 'local-api-secret-value';
|
||||
const secretBody = JSON.stringify({
|
||||
name: 'github-token',
|
||||
plaintext: secretPlaintext,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000700',
|
||||
expectedCurrentVersion: 0,
|
||||
});
|
||||
const secretPath = '/api/v3/projects/default/secrets';
|
||||
const secretOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(secretBody)),
|
||||
},
|
||||
body: secretBody,
|
||||
};
|
||||
const secretChallenge = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
secretPath,
|
||||
secretOptions,
|
||||
);
|
||||
assert.equal(secretChallenge.statusCode, 428);
|
||||
assert.equal(secretChallenge.body.code, 'local_presence_required');
|
||||
const secretProof = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(root, 'console-presence', secretChallenge.body.proofFileName),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const secretCreated = await request(port, `Bearer ${TOKEN}`, secretPath, {
|
||||
...secretOptions,
|
||||
headers: {
|
||||
...secretOptions.headers,
|
||||
'x-qinglong-local-presence': secretProof.proof,
|
||||
},
|
||||
});
|
||||
assert.equal(secretCreated.statusCode, 201);
|
||||
assert.equal(secretCreated.body.secret.currentVersion, 1);
|
||||
assert.match(secretCreated.body.secret.secretRef, /^qlsecret:v1:/u);
|
||||
assert.equal(JSON.stringify(secretCreated).includes(secretPlaintext), false);
|
||||
|
||||
const secretList = await request(
|
||||
port,
|
||||
`Bearer ${TOKEN}`,
|
||||
`${secretPath}?limit=64`,
|
||||
);
|
||||
assert.equal(secretList.statusCode, 200);
|
||||
assert.deepEqual(secretList.body.secrets, [
|
||||
{ ...secretCreated.body.secret, createdAtMs: NOW },
|
||||
]);
|
||||
assert.equal(secretList.body.truncated, false);
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(secretList),
|
||||
new RegExp(`${secretPlaintext}|ciphertext|keyId|mutationId`, 'u'),
|
||||
);
|
||||
|
||||
const taskCreateBody = JSON.stringify({
|
||||
expectedRevision: null,
|
||||
mutationId: '019f7300-0000-4000-8000-000000000701',
|
||||
@@ -545,6 +615,13 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
file: '/bin/echo',
|
||||
args: ['console-created'],
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'API_TOKEN',
|
||||
kind: 'secret',
|
||||
secretRef: secretCreated.body.secret.secretRef,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
labels: { source: 'local-console' },
|
||||
@@ -973,6 +1050,40 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
);
|
||||
const auditReader = new DatabaseSync(databasePath, { readOnly: true });
|
||||
try {
|
||||
const encryptedSecret = auditReader
|
||||
.prepare(
|
||||
`SELECT ciphertext FROM "QingLong3LocalSecretEnvelopes"
|
||||
WHERE project_id = 'default' AND secret_name = 'github-token'
|
||||
AND version = 1`,
|
||||
)
|
||||
.get();
|
||||
assert.equal(
|
||||
Buffer.from(encryptedSecret.ciphertext).includes(
|
||||
Buffer.from(secretPlaintext),
|
||||
),
|
||||
false,
|
||||
);
|
||||
const createdSpec = JSON.parse(
|
||||
auditReader
|
||||
.prepare(
|
||||
`SELECT revision.spec_json AS specJson
|
||||
FROM "QingLong3TaskDefinitions" AS head
|
||||
JOIN "QingLong3TaskDefinitionRevisions" AS revision
|
||||
ON revision.project_id = head.project_id
|
||||
AND revision.task_id = head.task_id
|
||||
AND revision.revision = head.current_revision
|
||||
WHERE head.project_id = 'default'
|
||||
AND head.task_id = 'task-console-created'`,
|
||||
)
|
||||
.get().specJson,
|
||||
);
|
||||
assert.deepEqual(createdSpec.config.environment, [
|
||||
{
|
||||
name: 'API_TOKEN',
|
||||
kind: 'secret',
|
||||
secretRef: secretCreated.body.secret.secretRef,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
auditReader
|
||||
.prepare(
|
||||
@@ -981,7 +1092,8 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
'run.get', 'run.list', 'run.events.list', 'run.steps.list',
|
||||
'run.cancel', 'task.authoring.read', 'task.create', 'task.get',
|
||||
'task.list', 'task.start', 'task.update', 'run.log.read',
|
||||
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update'
|
||||
'trigger.create', 'trigger.get', 'trigger.list', 'trigger.update',
|
||||
'secret.create', 'secret.list'
|
||||
)
|
||||
ORDER BY operation_id, outcome`,
|
||||
)
|
||||
@@ -996,6 +1108,9 @@ test('serves an authenticated Run through one real SQLite authority and durable
|
||||
'run.list:allowed',
|
||||
'run.log.read:allowed',
|
||||
'run.steps.list:allowed',
|
||||
'secret.create:allowed',
|
||||
'secret.create:approval_required',
|
||||
'secret.list:allowed',
|
||||
'task.authoring.read:allowed',
|
||||
'task.authoring.read:approval_required',
|
||||
'task.create:allowed',
|
||||
|
||||
Reference in New Issue
Block a user