feat(ql3): enforce staged secret binding persistence

This commit is contained in:
whyour
2026-08-13 19:20:39 +08:00
parent f111d8b10c
commit 326783ff14
32 changed files with 1071 additions and 71 deletions
@@ -12,11 +12,17 @@ export interface PostgresSchemaContractFunction {
readonly configuration: readonly string[];
}
export interface PostgresSchemaContractTrigger {
readonly name: string;
readonly tableName: string;
readonly functionName: string;
}
export interface PostgresSchemaContract {
readonly schema: 'ql3';
readonly contractName: 'control-core';
readonly contractVersion: 60;
readonly migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans';
readonly contractVersion: 61;
readonly migrationId: 'pg-0062-plugin-package-secret-binding-target-guard';
readonly minimumServerMajor: 16;
readonly maximumServerMajor: 18;
readonly capabilities: Readonly<{
@@ -57,6 +63,7 @@ export interface PostgresSchemaContract {
plugin_package_materialized_revision: 1;
plugin_package_secret_binding: 1;
plugin_package_secret_binding_approval_plan: 1;
plugin_package_secret_binding_transition: 1;
plugin_package_secret_materialization: 1;
plugin_package_proposal: 1;
plugin_package_publisher_provenance: 1;
@@ -94,6 +101,7 @@ export interface PostgresSchemaContract {
readonly checks: readonly string[];
readonly foreignKeys: readonly string[];
readonly functions: readonly PostgresSchemaContractFunction[];
readonly triggers: readonly PostgresSchemaContractTrigger[];
}
function table(
@@ -107,8 +115,8 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
Object.freeze({
schema: 'ql3',
contractName: 'control-core',
contractVersion: 60,
migrationId: 'pg-0061-plugin-package-secret-binding-approval-plans',
contractVersion: 61,
migrationId: 'pg-0062-plugin-package-secret-binding-target-guard',
minimumServerMajor: 16,
maximumServerMajor: 18,
capabilities: Object.freeze({
@@ -142,6 +150,7 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
plugin_package_materialized_revision: 1,
plugin_package_secret_binding: 1,
plugin_package_secret_binding_approval_plan: 1,
plugin_package_secret_binding_transition: 1,
plugin_package_secret_materialization: 1,
plugin_package_proposal: 1,
plugin_package_publisher_provenance: 1,
@@ -2384,6 +2393,14 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_binding_target',
identityArguments: '',
owner: 'ql3_migration',
securityDefiner: false,
volatility: 'volatile',
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
Object.freeze({
name: 'enforce_plugin_package_secret_materialization',
identityArguments: '',
@@ -2516,4 +2533,16 @@ export const postgresqlControlSchemaContract: PostgresSchemaContract =
configuration: Object.freeze(['search_path=pg_catalog, ql3']),
}),
]),
triggers: Object.freeze([
Object.freeze({
name: 'ql3_plugin_package_secret_binding_target_guard',
tableName: 'plugin_package_secret_bindings',
functionName: 'enforce_plugin_package_secret_binding_target',
}),
Object.freeze({
name: 'ql3_plugin_package_secret_materialization_guard',
tableName: 'plugin_package_materialized_revisions',
functionName: 'enforce_plugin_package_secret_materialization',
}),
]),
});
@@ -89,6 +89,13 @@ interface FunctionRow extends Record<string, unknown> {
publicExecute: unknown;
}
interface TriggerRow extends Record<string, unknown> {
triggerName: unknown;
tableName: unknown;
functionName: unknown;
enabled: unknown;
}
interface SchemaPrivilegeRow extends Record<string, unknown> {
schemaUsage: unknown;
schemaCreate: unknown;
@@ -1523,6 +1530,7 @@ const REQUIRED_RUNTIME_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_quarantine: false,
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: false,
@@ -1544,6 +1552,7 @@ const REQUIRED_PACKAGE_MANAGER_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges =
commit_plugin_package_quarantine: false,
commit_plugin_package_task_reconciliation: false,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: false,
lock_approval_policy_fence: true,
@@ -1565,6 +1574,7 @@ const REQUIRED_PACKAGE_EXECUTOR_FUNCTION_PRIVILEGES: RequiredFunctionPrivileges
commit_plugin_package_quarantine: true,
commit_plugin_package_task_reconciliation: true,
enforce_plugin_package_secret_materialization: false,
enforce_plugin_package_secret_binding_target: false,
enforce_plugin_package_stage_provenance: false,
lock_active_plugin_package_project: true,
lock_approval_policy_fence: true,
@@ -1728,7 +1738,13 @@ async function assertSchemaContract(
queryable: PostgresMigrationQueryable,
contract: PostgresSchemaContract,
): Promise<void> {
const [columnsResult, indexesResult, constraintsResult, functionsResult] =
const [
columnsResult,
indexesResult,
constraintsResult,
functionsResult,
triggersResult,
] =
await Promise.all([
queryable.query<ColumnRow>(
`
@@ -1803,6 +1819,27 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
`.trim(),
[contract.schema],
),
queryable.query<TriggerRow>(
`
SELECT
triggers.tgname AS "triggerName",
tables.relname AS "tableName",
routines.proname AS "functionName",
triggers.tgenabled AS "enabled"
FROM pg_trigger triggers
JOIN pg_class tables ON tables.oid = triggers.tgrelid
JOIN pg_namespace schemas ON schemas.oid = tables.relnamespace
JOIN pg_proc routines ON routines.oid = triggers.tgfoid
WHERE schemas.nspname = $1
AND NOT triggers.tgisinternal
AND triggers.tgname = ANY($2::text[])
ORDER BY triggers.tgname
`.trim(),
[
contract.schema,
contract.triggers.map(({ name }) => name),
],
),
]);
const actualTables = new Map<string, Set<string>>();
for (const row of columnsResult.rows) {
@@ -1943,6 +1980,35 @@ ORDER BY routines.proname, pg_get_function_identity_arguments(routines.oid)
findings.push(`unknown-function:${identity}`);
}
}
const actualTriggers = new Map<string, TriggerRow>();
for (const row of triggersResult.rows) {
if (
typeof row.triggerName !== 'string' ||
typeof row.tableName !== 'string' ||
typeof row.functionName !== 'string' ||
typeof row.enabled !== 'string'
) {
throw new PostgresSchemaReadinessError('schema_contract_invalid');
}
actualTriggers.set(row.triggerName, row);
}
for (const expected of contract.triggers) {
const actual = actualTriggers.get(expected.name);
if (!actual) {
findings.push(`missing-trigger:${expected.name}`);
continue;
}
if (
actual.tableName !== expected.tableName ||
actual.functionName !== expected.functionName ||
actual.enabled !== 'O'
) {
findings.push(`trigger-contract:${expected.name}`);
}
}
if (actualTriggers.size !== contract.triggers.length) {
findings.push('trigger-contract-row-count');
}
if (findings.length > 0) {
throw new PostgresSchemaReadinessError(
'schema_contract_invalid',