mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): complete cluster secret binding authority
This commit is contained in:
@@ -40,6 +40,8 @@ export {
|
||||
} from '../schema/schemaReadiness';
|
||||
|
||||
export { PostgresPluginPackageMaterializedRevisionRepository } from '../plugin-package/installation/pluginPackageMaterializedRevisionRepository';
|
||||
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
|
||||
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
export { PostgresPluginPackageSecretBindingRepository } from '../plugin-package/installation/pluginPackageSecretBindingRepository';
|
||||
export { PostgresPluginPackageSecretBindingApprovalPlanReader } from '../plugin-package/secret-binding/pluginPackageSecretBindingApprovalPlanRepository';
|
||||
export { PostgresPluginPackageAutomationPublicationRepository } from '../plugin-package/publication/pluginPackageAutomationPublicationRepository';
|
||||
|
||||
@@ -55,6 +55,8 @@ export {
|
||||
} from '../management/pluginPackageIdentityKeysetLedgerRepository';
|
||||
|
||||
export { PostgresPluginPackagePublisherTrustAuthorityRepository } from '../plugin-package/publisher/pluginPackagePublisherTrustAuthorityRepository';
|
||||
export { PostgresApprovalRequestRepository } from '../approved-action/approvalRequestRepository';
|
||||
export { PostgresProjectPolicyRepository } from '../security/projectPolicyRepository';
|
||||
export { PostgresPluginPackageLifecyclePlanReader } from '../plugin-package/lifecycle/pluginPackageLifecyclePlanRepository';
|
||||
export {
|
||||
PostgresPluginPackageSecretBindingApprovalPlanReader,
|
||||
|
||||
+4
-1
@@ -180,7 +180,10 @@ export class PostgresPluginPackageSecretBindingRepository
|
||||
lock_digest, generation, manifest_digest, authority_kind,
|
||||
evidence_digest, bound_at_ms, binding_digest, binding_json
|
||||
)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb
|
||||
SELECT $1::char(64), $2::varchar(128), $3::varchar(63),
|
||||
$4::varchar(128), $5::char(64), $6::integer, $7::char(64),
|
||||
$8::varchar(32), $9::char(64), $10::bigint, $11::char(64),
|
||||
$12::jsonb
|
||||
FROM "ql3"."plugin_package_installs" AS install
|
||||
INNER JOIN "ql3"."plugin_package_install_heads" AS head
|
||||
ON head.installation_id = install.installation_id
|
||||
|
||||
+55
@@ -1,4 +1,9 @@
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import {
|
||||
approvalRequestDigest,
|
||||
normalizeApprovalRequestRecord,
|
||||
type ApprovalRequestRecord,
|
||||
} from '@qinglong/runtime-core/approved-action';
|
||||
import {
|
||||
normalizePluginPackageInstallProposal,
|
||||
type PluginPackageInstallProposal,
|
||||
@@ -86,6 +91,26 @@ function normalizeRow(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeApprovalRow(row: Row): Readonly<ApprovalRequestRecord> {
|
||||
try {
|
||||
const request = normalizeApprovalRequestRecord(
|
||||
postgresRequiredJsonObject(
|
||||
row.requestJson,
|
||||
unavailable,
|
||||
) as unknown as ApprovalRequestRecord,
|
||||
);
|
||||
if (
|
||||
approvalRequestDigest(request) !==
|
||||
postgresRequiredString(row.requestDigest, unavailable)
|
||||
) {
|
||||
throw unavailable();
|
||||
}
|
||||
return request;
|
||||
} catch (error) {
|
||||
throw unavailable(error);
|
||||
}
|
||||
}
|
||||
|
||||
function same(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
@@ -194,6 +219,36 @@ export class PostgresPluginPackageSecretBindingApprovalPlanReader {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async listApprovedRequests(
|
||||
limitValue: number,
|
||||
): Promise<readonly Readonly<ApprovalRequestRecord>[]> {
|
||||
if (
|
||||
!Number.isSafeInteger(limitValue) ||
|
||||
limitValue < 1 ||
|
||||
limitValue > 64
|
||||
) {
|
||||
throw new TypeError('Secret binding approval page limit is invalid');
|
||||
}
|
||||
try {
|
||||
const result = await this.pool.query<Row>(
|
||||
`SELECT request.request_json AS "requestJson",
|
||||
request.request_digest AS "requestDigest"
|
||||
FROM "ql3"."approval_requests" AS request
|
||||
JOIN "ql3"."plugin_package_secret_binding_approval_plans" AS plan
|
||||
ON plan.action_ref = request.action_ref
|
||||
WHERE request.state = 'approved'
|
||||
AND request.action_type = 'plugin_package.secret_binding.bind'
|
||||
ORDER BY request.updated_at_ms, request.request_id
|
||||
LIMIT $1`,
|
||||
[limitValue],
|
||||
);
|
||||
if (result.rows.length > limitValue) throw unavailable();
|
||||
return Object.freeze(result.rows.map(normalizeApprovalRow));
|
||||
} catch (error) {
|
||||
throw mapStorageError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PostgresPluginPackageSecretBindingApprovalPlanRepository
|
||||
|
||||
+65
@@ -3,6 +3,11 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
approvalRequestDigest,
|
||||
createApprovalRequest,
|
||||
decideApprovalRequest,
|
||||
} = require('@qinglong/runtime-core/approved-action');
|
||||
const {
|
||||
PLUGIN_PACKAGE_API_VERSION,
|
||||
PLUGIN_PACKAGE_KIND,
|
||||
@@ -292,6 +297,66 @@ test('returns absence and fails closed on replay drift or storage conflict', asy
|
||||
);
|
||||
});
|
||||
|
||||
test('lists only a bounded digest-verified approved Secret binding queue', async () => {
|
||||
const { approvalPlan } = fixture();
|
||||
const pending = createApprovalRequest({
|
||||
id: 'approval-secret-binding-list-1',
|
||||
projectId: approvalPlan.bindingPlan.target.projectId,
|
||||
action: require('@qinglong/runtime-core/plugin-package-secret-binding-approval-plan')
|
||||
.pluginPackageSecretBindingApprovedAction(approvalPlan),
|
||||
risk: 'high',
|
||||
decisionMode: 'separation_of_duty',
|
||||
requestedBy: approvalPlan.requestedBy,
|
||||
requestedAtMs: 301,
|
||||
expiresAtMs: 800,
|
||||
requestFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
});
|
||||
const approved = decideApprovalRequest(pending, {
|
||||
expectedVersion: 1,
|
||||
decisionId: 'decision-secret-binding-list-1',
|
||||
decision: 'approved',
|
||||
reasonCode: 'reviewed',
|
||||
principal: {
|
||||
subject: { type: 'user', id: 'security-reviewer' },
|
||||
authenticationId: 'auth-security-reviewer',
|
||||
authenticatedAtMs: 302,
|
||||
expiresAtMs: 700,
|
||||
assurance: 'multi_factor',
|
||||
},
|
||||
decidedAtMs: 303,
|
||||
authorizationFence: { projectVersion: 1, bindingVersion: 1 },
|
||||
});
|
||||
const calls = [];
|
||||
const reader = new PostgresPluginPackageSecretBindingApprovalPlanReader({
|
||||
async query(text, parameters) {
|
||||
calls.push({ text, parameters });
|
||||
return {
|
||||
rows: [{
|
||||
requestJson: approved,
|
||||
requestDigest: approvalRequestDigest(approved),
|
||||
}],
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await reader.listApprovedRequests(4), [approved]);
|
||||
assert.match(calls[0].text, /request\.state = 'approved'/);
|
||||
assert.match(calls[0].text, /plugin_package\.secret_binding\.bind/);
|
||||
assert.deepEqual(calls[0].parameters, [4]);
|
||||
await assert.rejects(reader.listApprovedRequests(65), TypeError);
|
||||
|
||||
const corrupt = new PostgresPluginPackageSecretBindingApprovalPlanReader({
|
||||
async query() {
|
||||
return {
|
||||
rows: [{ requestJson: approved, requestDigest: 'f'.repeat(64) }],
|
||||
};
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
corrupt.listApprovedRequests(4),
|
||||
PluginPackageSecretBindingApprovalPlanUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('exports plan creation only to the Package manager and readback to the executor', () => {
|
||||
const manager = require('@qinglong/cluster-postgres/package-manager');
|
||||
const executor = require('@qinglong/cluster-postgres/package-executor');
|
||||
|
||||
@@ -187,3 +187,21 @@ test('publishes storage through package-executor and explicit subpath', () => {
|
||||
PostgresPluginPackageSecretBindingRepository,
|
||||
);
|
||||
});
|
||||
|
||||
test('casts reused INSERT parameters to their durable PostgreSQL column types', async () => {
|
||||
let statement = '';
|
||||
const repository = new PostgresPluginPackageSecretBindingRepository({
|
||||
async query(text) {
|
||||
if (text.includes('INSERT INTO')) {
|
||||
statement = text;
|
||||
return { rows: [{ generation_digest: 'a'.repeat(64) }], rowCount: 1 };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
await assert.rejects(repository.publish(fixture()));
|
||||
assert.match(statement, /\$2::varchar\(128\)/);
|
||||
assert.match(statement, /\$3::varchar\(63\)/);
|
||||
assert.match(statement, /\$6::integer/);
|
||||
assert.match(statement, /\$10::bigint/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user