mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 09:58:46 +08:00
feat(ql3): enforce staged secret binding persistence
This commit is contained in:
@@ -313,5 +313,10 @@ export const postgresqlMainMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'c995b7846ae8a57d3abb4b5523961e81aeba890e7405a030bcb505dfc6be3d25',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||
checksum:
|
||||
'cd4f92d8702da6b92dd9ae5153b5180400b94442f56393692b6ec038f998596b',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ import { pg0058PluginPackageAutomationDispositionEventsMigration } from './pg-00
|
||||
import { pg0059PluginPackageSecretBindingsMigration } from './pg-0059-plugin-package-secret-bindings';
|
||||
import { pg0060PluginPackageSecretMaterializationGuardMigration } from './pg-0060-plugin-package-secret-materialization-guard';
|
||||
import { pg0061PluginPackageSecretBindingApprovalPlansMigration } from './pg-0061-plugin-package-secret-binding-approval-plans';
|
||||
import { pg0062PluginPackageSecretBindingTargetGuardMigration } from './pg-0062-plugin-package-secret-binding-target-guard';
|
||||
|
||||
export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMigrationContext> =
|
||||
Object.freeze({
|
||||
@@ -133,5 +134,6 @@ export const postgresqlMainMigrationStream: MigrationStreamDefinition<PostgresMi
|
||||
pg0059PluginPackageSecretBindingsMigration,
|
||||
pg0060PluginPackageSecretMaterializationGuardMigration,
|
||||
pg0061PluginPackageSecretBindingApprovalPlansMigration,
|
||||
pg0062PluginPackageSecretBindingTargetGuardMigration,
|
||||
]),
|
||||
});
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { CAPABILITIES_V60 } from './pg-0061-plugin-package-secret-binding-approval-plans';
|
||||
import { definePostgresSqlMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V61 = CAPABILITIES_V60.replace(
|
||||
'"plugin_package_secret_binding":1,',
|
||||
'"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,',
|
||||
);
|
||||
|
||||
export const pg0062PluginPackageSecretBindingTargetGuardMigration =
|
||||
definePostgresSqlMigration({
|
||||
id: 'pg-0062-plugin-package-secret-binding-target-guard',
|
||||
statements: [
|
||||
`
|
||||
CREATE FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
VOLATILE
|
||||
SET search_path = pg_catalog, ql3
|
||||
AS $ql3$
|
||||
BEGIN
|
||||
PERFORM 1
|
||||
FROM "ql3"."plugin_package_install_heads" AS head
|
||||
JOIN "ql3"."plugin_package_installs" AS install
|
||||
ON install.installation_id = head.installation_id
|
||||
AND install.project_id = head.project_id
|
||||
AND install.package_name = head.package_name
|
||||
WHERE head.project_id = NEW.project_id
|
||||
AND head.package_name = NEW.package_name
|
||||
AND install.installation_id = NEW.installation_id
|
||||
AND install.lock_digest = NEW.lock_digest
|
||||
AND install.target_generation = NEW.generation
|
||||
AND install.lock_json ->> 'manifestDigest' = NEW.manifest_digest
|
||||
AND (
|
||||
(
|
||||
install.state = 'active' AND
|
||||
install.active_lock_digest = install.lock_digest
|
||||
) OR (
|
||||
install.state = 'staged' AND
|
||||
install.previous_active_lock_digest IS NOT NULL AND
|
||||
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||
install.target_generation = (
|
||||
SELECT MAX(history.target_generation)
|
||||
FROM "ql3"."plugin_package_installs" AS history
|
||||
WHERE history.project_id = install.project_id
|
||||
AND history.package_name = install.package_name
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_installs" AS previous
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
AND previous.state = 'active'
|
||||
AND previous.active_lock_digest = previous.lock_digest
|
||||
AND previous.target_generation < install.target_generation
|
||||
)
|
||||
)
|
||||
)
|
||||
FOR SHARE OF head, install;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION
|
||||
'Plugin Package Secret binding target is not current active or reviewed staged generation'
|
||||
USING ERRCODE = 'check_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$ql3$
|
||||
`.trim(),
|
||||
`REVOKE ALL ON FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"() FROM PUBLIC, ql3_runtime, ql3_admin, ql3_package_manager, ql3_package_executor, ql3_worker_ingress`,
|
||||
`CREATE TRIGGER ql3_plugin_package_secret_binding_target_guard BEFORE INSERT ON "ql3"."plugin_package_secret_bindings" FOR EACH ROW EXECUTE FUNCTION "ql3"."enforce_plugin_package_secret_binding_target"()`,
|
||||
`DO $ql3$ BEGIN UPDATE "ql3"."schema_capabilities" SET contract_version = 61, migration_id = 'pg-0062-plugin-package-secret-binding-target-guard', capabilities = '${CAPABILITIES_V61}'::jsonb, updated_at_ms = floor(extract(epoch FROM transaction_timestamp()) * 1000)::bigint WHERE contract_name = 'control-core' AND contract_version = 60 AND migration_id = 'pg-0061-plugin-package-secret-binding-approval-plans' AND capabilities = '${CAPABILITIES_V60}'::jsonb; IF NOT FOUND THEN RAISE EXCEPTION 'control-core capability is not at version 60' USING ERRCODE = 'check_violation'; END IF; END $ql3$`,
|
||||
],
|
||||
});
|
||||
+24
-3
@@ -193,10 +193,31 @@ export class PostgresPluginPackageSecretBindingRepository
|
||||
AND install.project_id = $2
|
||||
AND install.package_name = $3
|
||||
AND install.lock_digest = $5
|
||||
AND install.active_lock_digest = $5
|
||||
AND install.target_generation = $6
|
||||
AND install.state = 'active'
|
||||
AND install.lock_json ->> 'manifestDigest' = $7
|
||||
AND (
|
||||
(install.state = 'active' AND
|
||||
install.active_lock_digest = install.lock_digest) OR
|
||||
(install.state = 'staged' AND
|
||||
install.previous_active_lock_digest IS NOT NULL AND
|
||||
install.active_lock_digest = install.previous_active_lock_digest AND
|
||||
install.target_generation = (
|
||||
SELECT MAX(history.target_generation)
|
||||
FROM "ql3"."plugin_package_installs" AS history
|
||||
WHERE history.project_id = install.project_id
|
||||
AND history.package_name = install.package_name
|
||||
) AND
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."plugin_package_installs" AS previous
|
||||
WHERE previous.project_id = install.project_id
|
||||
AND previous.package_name = install.package_name
|
||||
AND previous.lock_digest = install.previous_active_lock_digest
|
||||
AND previous.state = 'active'
|
||||
AND previous.active_lock_digest = previous.lock_digest
|
||||
AND previous.target_generation < install.target_generation
|
||||
))
|
||||
)
|
||||
ON CONFLICT (generation_digest) DO NOTHING
|
||||
RETURNING generation_digest`,
|
||||
[
|
||||
@@ -217,7 +238,7 @@ export class PostgresPluginPackageSecretBindingRepository
|
||||
const stored = await this.findStored(binding.target.generationDigest);
|
||||
if (!stored) {
|
||||
throw new PluginPackageSecretBindingConflictError(
|
||||
'binding target is not the current active Package generation',
|
||||
'binding target is not the current active or reviewed staged Package generation',
|
||||
);
|
||||
}
|
||||
if (JSON.stringify(stored) !== bindingJson) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user