From 508e1bdb2842957ae4400b4c2a49e3fecbf7c473 Mon Sep 17 00:00:00 2001 From: fwazb Date: Tue, 21 Jul 2026 18:54:51 -0700 Subject: [PATCH] Handle Cloud SQL provider database residuals --- docs/gcp-leoclean-runtime-reconciliation.md | 44 ++-- ops/gcp_leoclean_runtime_role.sql | 216 +++++++++++++++++- ...verify_gcp_leoclean_runtime_permissions.py | 169 +++++++++++++- .../test_gcp_leoclean_runtime_permissions.py | 34 +++ ...test_gcp_leoclean_runtime_role_postgres.py | 188 ++++++++++++++- .../test_hermes_leoclean_kb_bridge_source.py | 9 +- 6 files changed, 634 insertions(+), 26 deletions(-) diff --git a/docs/gcp-leoclean-runtime-reconciliation.md b/docs/gcp-leoclean-runtime-reconciliation.md index 3ac69de..1f10f8d 100644 --- a/docs/gcp-leoclean-runtime-reconciliation.md +++ b/docs/gcp-leoclean-runtime-reconciliation.md @@ -92,12 +92,22 @@ cleartext runtime password. The migration: database ACL, canonical grant, routine, large-object, and topology checks; - creates a dedicated `NOLOGIN` function owner with no role memberships; - removes stale table, column, sequence, function, and role-membership grants; -- inventories existing principals, replaces `PUBLIC CONNECT` with explicit - preserved grants, gives the runtime `CONNECT` only to `teleo_canonical`, and - leaves the staging owner with no database connection target; +- inventories existing principals, replaces operator-controlled `PUBLIC + CONNECT` with explicit preserved grants, gives the runtime a sole direct + `CONNECT` grant on `teleo_canonical`, and leaves the staging owner with no + direct database grant; +- validates and reports the unavoidable Cloud SQL system connection residual: + `cloudsqladmin` and `template0`, both owned by `cloudsqladmin`, retain their + provider-owned `PUBLIC CONNECT`; owner, connection/template flags, public + connection semantics, unexpected grantees, or extra-database drift fails + closed, while the raw provider-owned `template0` ACL is preserved rather than + reconstructed; - inventories and preserves existing non-scoped application-routine and - large-object access while removing `PUBLIC` and both scoped roles from all - other application routines and every persistent large-object mutator; + large-object access while removing both scoped roles from unexpected direct + routine ACLs and explicitly reporting the provider-owned public + large-object-mutator residual; +- rejects nonempty session/local preload-library settings before enabling the + scoped runtime login; - grants exact canonical reads; - creates a locked security-definer staging function that hard-codes both `pending_review` and canonical proposer `leo`; @@ -116,12 +126,13 @@ by a `pg_catalog, pg_temp` search path, schema-qualified relations, a fixed The administrator password is used only for this bounded bootstrap. Retain no password output. -`teleo_kb`, `template1`, and every other noncanonical database are actual -negative connection checks. PostgreSQL's startup protocol does not expose a -SQLSTATE through `psql`, so the verifier requires the exact C-locale +`postgres`, `teleo_kb`, `template1`, and every other operator-controlled +noncanonical database are denied. PostgreSQL's startup protocol does not expose +a SQLSTATE through `psql`, so the verifier requires the exact C-locale `permission denied for database` and `does not have CONNECT privilege` -diagnostics in addition to the catalog proof that the runtime's sole effective -target is `teleo_canonical`. Audit fallback remains disabled with +diagnostics in addition to the catalog proof. The receipt separately exposes +the exact provider-owned `cloudsqladmin`/`template0` residual; it never labels +the runtime production-eligible. Audit fallback remains disabled with `TELEO_CLOUDSQL_ENABLE_AUDIT_FALLBACK=0`. ### 5. Preflight and deploy the merged runtime @@ -279,17 +290,20 @@ Retain a redacted receipt containing: both before and after the transaction; - denied direct insert, update, and delete on `kb_stage.kb_proposals`; - denied insert, update, and delete on canonical `public.claims`; -- denied `lo_creat`, `lo_create`, and `lo_from_bytea`, zero effective - large-object mutator execution, and zero scoped large-object ownership/ACLs; +- zero direct scoped large-object mutator ACLs and zero scoped large-object + ownership/ACLs; the known provider-owned public mutator residual is reported + explicitly and exercised only inside a rolled-back probe, so this receipt + does not claim arbitrary database writes are categorically denied; - denied reviewer/apply security-definer functions; - exact function-catalog posture: only the five-argument staging function is executable, the forged six-argument overload is absent, and each expected reviewer/apply function exists but is not executable by the runtime role; - unavailable forged-proposer overload and denied `SET ROLE` escalation into the staging owner, reviewer, apply, or administrator roles; -- denied startup connections to both `teleo_kb` and `template1`, with the - catalog showing `teleo_canonical` as the sole runtime connection target and - zero remaining `PUBLIC CONNECT` grants; +- denied startup connections to `postgres`, `teleo_kb`, and `template1`, with + the catalog showing a sole direct runtime grant on `teleo_canonical`, the + exact two-row Cloud SQL system residual, and zero unexpected `PUBLIC CONNECT` + grants; - readable scoped runtime secret and an IAM permission denial for the administrator secret, with neither secret value retained; - zero Telegram messages and zero committed canary rows. diff --git a/ops/gcp_leoclean_runtime_role.sql b/ops/gcp_leoclean_runtime_role.sql index 5793ab8..6d226d0 100644 --- a/ops/gcp_leoclean_runtime_role.sql +++ b/ops/gcp_leoclean_runtime_role.sql @@ -153,8 +153,181 @@ begin; -- REVOKE. Preserve every existing non-scoped principal's effective CONNECT -- set as explicit ACLs, remove the cluster-wide PUBLIC grant from every -- database (including templates), and then grant the runtime its sole target. +-- Cloud SQL reserves cloudsqladmin and template0 under the immutable +-- cloudsqladmin owner. Their exact provider-owned PUBLIC CONNECT residual +-- cannot be changed by the customer administrator, so validate that pair +-- field-for-field and exclude only those two rows from ACL normalization. -- This runs in the same transaction as every remaining privilege/topology -- assertion, so a later failure restores the original ACLs atomically. +create temporary table leoclean_expected_provider_database_connect_residual ( + database_name name primary key, + owner_name name not null, + allow_connections boolean not null, + is_template boolean not null, + database_acl_is_default boolean, + public_connect_acl_entries integer not null, + public_connect_grantable boolean not null +) on commit drop; + +insert into pg_temp.leoclean_expected_provider_database_connect_residual ( + database_name, + owner_name, + allow_connections, + is_template, + database_acl_is_default, + public_connect_acl_entries, + public_connect_grantable +) +select expected.* + from (values + ('cloudsqladmin'::name, 'cloudsqladmin'::name, true, false, true, 1, false), + ('template0'::name, 'cloudsqladmin'::name, false, true, null::boolean, 1, false) + ) expected( + database_name, + owner_name, + allow_connections, + is_template, + database_acl_is_default, + public_connect_acl_entries, + public_connect_grantable + ) + where pg_catalog.to_regrole('cloudsqladmin') is not null; + +do $$ +declare + unexpected text; +begin + with actual as ( + select database_row.datname as database_name, + owner_role.rolname as owner_name, + database_row.datallowconn as allow_connections, + database_row.datistemplate as is_template, + database_row.datacl is null as database_acl_is_default, + pg_catalog.count(*) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + )::integer as public_connect_acl_entries, + coalesce( + pg_catalog.bool_or(acl.is_grantable) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + ), + false + ) as public_connect_grantable + from pg_catalog.pg_database database_row + join pg_catalog.pg_roles owner_role on owner_role.oid = database_row.datdba + cross join lateral pg_catalog.aclexplode( + coalesce( + database_row.datacl, + pg_catalog.acldefault('d', database_row.datdba) + ) + ) acl + where pg_catalog.to_regrole('cloudsqladmin') is not null + and database_row.datname in ('cloudsqladmin', 'template0') + group by database_row.datname, + owner_role.rolname, + database_row.datallowconn, + database_row.datistemplate, + database_row.datacl + ) + select pg_catalog.string_agg( + pg_catalog.format( + '%s:expected=%s/%s/%s/%s/%s/%s:actual=%s/%s/%s/%s/%s/%s', + coalesce(expected.database_name, actual.database_name), + expected.owner_name, + expected.allow_connections, + expected.is_template, + expected.database_acl_is_default, + expected.public_connect_acl_entries, + expected.public_connect_grantable, + actual.owner_name, + actual.allow_connections, + actual.is_template, + actual.database_acl_is_default, + actual.public_connect_acl_entries, + actual.public_connect_grantable + ), + ', ' + order by coalesce(expected.database_name, actual.database_name) + ) + into unexpected + from pg_temp.leoclean_expected_provider_database_connect_residual expected + full join actual using (database_name) + where expected.database_name is null + or actual.database_name is null + or expected.owner_name is distinct from actual.owner_name + or expected.allow_connections is distinct from actual.allow_connections + or expected.is_template is distinct from actual.is_template + or ( + expected.database_acl_is_default is not null + and expected.database_acl_is_default is distinct from actual.database_acl_is_default + ) + or expected.public_connect_acl_entries is distinct from actual.public_connect_acl_entries + or expected.public_connect_grantable is distinct from actual.public_connect_grantable; + if unexpected is not null then + raise exception 'Cloud SQL provider database CONNECT residual drifted: %', unexpected; + end if; + + select pg_catalog.string_agg( + pg_catalog.format( + '%I:%I:%s:grantable=%s', + database_row.datname, + grantee.rolname, + acl.privilege_type, + acl.is_grantable + ), + ', ' + order by database_row.datname, grantee.rolname, acl.privilege_type + ) + into unexpected + from pg_catalog.pg_database database_row + join pg_temp.leoclean_expected_provider_database_connect_residual expected + on expected.database_name = database_row.datname + cross join lateral pg_catalog.aclexplode(database_row.datacl) acl + join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee + where grantee.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner'); + if unexpected is not null then + raise exception 'scoped Leo roles retain direct ACLs on provider databases: %', unexpected; + end if; + + select pg_catalog.string_agg( + pg_catalog.format( + '%I:%s:%s:grantable=%s', + database_row.datname, + case when acl.grantee = 0 then 'PUBLIC' else grantee.rolname end, + acl.privilege_type, + acl.is_grantable + ), + ', ' + order by database_row.datname, acl.grantee, acl.privilege_type + ) + into unexpected + from pg_catalog.pg_database database_row + join pg_temp.leoclean_expected_provider_database_connect_residual expected + on expected.database_name = database_row.datname + cross join lateral pg_catalog.aclexplode( + coalesce( + database_row.datacl, + pg_catalog.acldefault('d', database_row.datdba) + ) + ) acl + left join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee + where not ( + ( + acl.grantee = 0 + and acl.privilege_type in ('CONNECT', 'TEMPORARY') + and not acl.is_grantable + ) or ( + acl.grantee = database_row.datdba + and acl.privilege_type in ('CREATE', 'CONNECT', 'TEMPORARY') + ) + ); + if unexpected is not null then + raise exception 'provider databases retain unexpected ACL entries: %', unexpected; + end if; +end +$$; + create temporary table leoclean_preserved_database_connect ( role_name name not null, database_name name not null, @@ -166,10 +339,20 @@ select role_row.rolname, database_row.datname from pg_catalog.pg_roles role_row cross join pg_catalog.pg_database database_row where role_row.rolname not in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner') + and not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ) and pg_catalog.has_database_privilege(role_row.oid, database_row.oid, 'CONNECT'); select pg_catalog.format('revoke connect on database %I from public', database_row.datname) from pg_catalog.pg_database database_row + where not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ) \gexec select pg_catalog.format( @@ -177,6 +360,11 @@ select pg_catalog.format( database_row.datname ) from pg_catalog.pg_database database_row + where not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ) \gexec select pg_catalog.format( @@ -986,8 +1174,13 @@ begin select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname) into unexpected - from pg_catalog.pg_database database_row + from pg_catalog.pg_database database_row where database_row.datname <> 'teleo_canonical' + and not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ) and has_database_privilege('leoclean_kb_runtime', database_row.oid, 'CONNECT'); if unexpected is not null then raise exception 'runtime CONNECT reaches a noncanonical database: %', unexpected; @@ -996,7 +1189,12 @@ begin select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname) into unexpected from pg_catalog.pg_database database_row - where has_database_privilege('leoclean_kb_stage_owner', database_row.oid, 'CONNECT'); + where not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ) + and has_database_privilege('leoclean_kb_stage_owner', database_row.oid, 'CONNECT'); if unexpected is not null then raise exception 'stage owner CONNECT reaches a database: %', unexpected; end if; @@ -1008,9 +1206,14 @@ begin coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba)) ) acl where acl.grantee = 0 - and acl.privilege_type = 'CONNECT'; + and acl.privilege_type = 'CONNECT' + and not exists ( + select 1 + from pg_temp.leoclean_expected_provider_database_connect_residual expected + where expected.database_name = database_row.datname + ); if unexpected is not null then - raise exception 'PUBLIC retains database CONNECT: %', unexpected; + raise exception 'PUBLIC retains unexpected database CONNECT: %', unexpected; end if; select pg_catalog.string_agg( @@ -1187,6 +1390,11 @@ begin raise exception 'lo_compat_privileges must remain off for scoped large-object ACL enforcement'; end if; + if pg_catalog.current_setting('session_preload_libraries') <> '' + or pg_catalog.current_setting('local_preload_libraries') <> '' then + raise exception 'session and local preload libraries must remain empty for the scoped runtime'; + end if; + select pg_catalog.string_agg(parameter_acl.parname, ', ' order by parameter_acl.parname) into unexpected from pg_catalog.pg_parameter_acl parameter_acl diff --git a/ops/verify_gcp_leoclean_runtime_permissions.py b/ops/verify_gcp_leoclean_runtime_permissions.py index ee0b6c3..9e69803 100644 --- a/ops/verify_gcp_leoclean_runtime_permissions.py +++ b/ops/verify_gcp_leoclean_runtime_permissions.py @@ -32,6 +32,7 @@ PRIVATE_CLOUDSQL_PORT = 5432 EXPECTED_SYSTEM_IDENTIFIER = "7659718422914359312" CANONICAL_DATABASE = "teleo_canonical" LEGACY_DATABASE = "teleo_kb" +OPERATOR_DATABASE = "postgres" TEMPLATE_DATABASE = "template1" RUNTIME_DATABASE_ROLE = "leoclean_kb_runtime" STAGE_OWNER_DATABASE_ROLE = "leoclean_kb_stage_owner" @@ -248,6 +249,31 @@ LARGE_OBJECT_MUTATION_ROUTINE_RESIDUAL: tuple[tuple[str, bool], ...] = ( ("lowrite(integer, bytea)", True), ) +PROVIDER_DATABASE_CONNECT_RESIDUAL: tuple[dict[str, object], ...] = ( + { + "allow_connections": True, + "database": "cloudsqladmin", + "is_template": False, + "owner": "cloudsqladmin", + "public_connect": True, + "public_connect_acl_entries": 1, + "public_connect_grantable": False, + "runtime_connect": True, + "stage_owner_connect": True, + }, + { + "allow_connections": False, + "database": "template0", + "is_template": True, + "owner": "cloudsqladmin", + "public_connect": True, + "public_connect_acl_entries": 1, + "public_connect_grantable": False, + "runtime_connect": True, + "stage_owner_connect": True, + }, +) + CATALOG_ZERO_COUNT_FIELDS: tuple[str, ...] = ( "column_dml_grants", "database_create_grants", @@ -300,6 +326,7 @@ CATALOG_ZERO_COUNT_FIELDS: tuple[str, ...] = ( CATALOG_TRUE_FIELDS: tuple[str, ...] = ( "canonical_connect", + "cloudsqladmin_database_acl_is_default", "owner_grant_contract", "owner_schema_usage", "proposal_table_contract", @@ -864,6 +891,82 @@ with runtime_role as ( from pg_catalog.pg_namespace where nspname <> 'information_schema' and nspname !~ '^pg_' +), provider_database_connect_observed as ( + select database_row.oid, + database_row.datname as database_name, + owner_role.rolname as owner_name, + database_row.datallowconn as allow_connections, + database_row.datistemplate as is_template, + pg_catalog.count(*) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + )::integer as public_connect_acl_entries, + coalesce( + pg_catalog.bool_or(true) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + ), + false + ) as public_connect, + coalesce( + pg_catalog.bool_or(acl.is_grantable) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + ), + false + ) as public_connect_grantable, + coalesce( + pg_catalog.has_database_privilege( + (select oid from runtime_role), + database_row.oid, + 'CONNECT' + ), + false + ) as runtime_connect, + coalesce( + pg_catalog.has_database_privilege( + (select oid from owner_role), + database_row.oid, + 'CONNECT' + ), + false + ) as stage_owner_connect + from pg_catalog.pg_database database_row + join pg_catalog.pg_roles owner_role on owner_role.oid = database_row.datdba + cross join lateral pg_catalog.aclexplode( + coalesce( + database_row.datacl, + pg_catalog.acldefault('d', database_row.datdba) + ) + ) acl + where pg_catalog.to_regrole('cloudsqladmin') is not null + and database_row.datname in ('cloudsqladmin', 'template0') + group by database_row.oid, + database_row.datname, + owner_role.rolname, + database_row.datallowconn, + database_row.datistemplate +), allowed_provider_database_connect_residual as ( + select observed.oid + from provider_database_connect_observed observed + where ( + ( + observed.database_name = 'cloudsqladmin' + and observed.owner_name = 'cloudsqladmin' + and observed.allow_connections + and not observed.is_template + ) or ( + observed.database_name = 'template0' + and observed.owner_name = 'cloudsqladmin' + and not observed.allow_connections + and observed.is_template + ) + ) + and observed.public_connect + and observed.public_connect_acl_entries = 1 + and not observed.public_connect_grantable + and observed.runtime_connect + and observed.stage_owner_connect ), allowed_select(nspname, relname) as ( values {allowlist_values} @@ -928,7 +1031,33 @@ with runtime_role as ( where attribute.attnum > 0 and not attribute.attisdropped ) -select pg_catalog.jsonb_build_object( +select (pg_catalog.jsonb_build_object( + 'provider_database_connect_residual', coalesce(( + select pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allow_connections', observed.allow_connections, + 'database', observed.database_name, + 'is_template', observed.is_template, + 'owner', observed.owner_name, + 'public_connect', observed.public_connect, + 'public_connect_acl_entries', observed.public_connect_acl_entries, + 'public_connect_grantable', observed.public_connect_grantable, + 'runtime_connect', observed.runtime_connect, + 'stage_owner_connect', observed.stage_owner_connect + ) + order by observed.database_name + ) + from provider_database_connect_observed observed + ), '[]'::pg_catalog.jsonb), + 'cloudsqladmin_database_acl_is_default', coalesce(( + select database_row.datacl is null + from pg_catalog.pg_database database_row + join pg_catalog.pg_roles owner_role on owner_role.oid = database_row.datdba + where database_row.datname = 'cloudsqladmin' + and owner_role.rolname = 'cloudsqladmin' + ), false) +) +|| pg_catalog.jsonb_build_object( 'canonical_connect', coalesce( pg_catalog.has_database_privilege(current_user, pg_catalog.current_database(), 'CONNECT'), false @@ -985,11 +1114,12 @@ select pg_catalog.jsonb_build_object( ) ), 'effective_setting_contract', ( + -- Do not grant pg_read_all_settings merely to inspect restricted preload + -- settings. The exact pg_db_role_setting contract above rejects scoped + -- role overrides; provider/global settings remain a separate admin check. pg_catalog.current_setting('search_path') = 'pg_catalog, public, kb_stage' and pg_catalog.current_setting('statement_timeout') = '15s' and pg_catalog.current_setting('lock_timeout') = '2s' - and pg_catalog.current_setting('session_preload_libraries') = '' - and pg_catalog.current_setting('local_preload_libraries') = '' and pg_catalog.current_setting('lo_compat_privileges') = 'off' and pg_catalog.current_setting('role') = 'none' ), @@ -997,6 +1127,11 @@ select pg_catalog.jsonb_build_object( select pg_catalog.count(*)::int from pg_catalog.pg_database database_row where database_row.datname <> {_sql_literal(CANONICAL_DATABASE)} + and not exists ( + select 1 + from allowed_provider_database_connect_residual allowed + where allowed.oid = database_row.oid + ) and pg_catalog.has_database_privilege(current_user, database_row.oid, 'CONNECT') ), 'owner_database_connect_grants', ( @@ -1007,6 +1142,11 @@ select pg_catalog.jsonb_build_object( database_row.oid, 'CONNECT' ) + and not exists ( + select 1 + from allowed_provider_database_connect_residual allowed + where allowed.oid = database_row.oid + ) ), 'public_database_connect_grants', ( select pg_catalog.count(*)::int @@ -1016,6 +1156,11 @@ select pg_catalog.jsonb_build_object( ) acl where acl.grantee = 0 and acl.privilege_type = 'CONNECT' + and not exists ( + select 1 + from allowed_provider_database_connect_residual allowed + where allowed.oid = database_row.oid + ) ), 'unexpected_direct_database_acl_entries', ( select pg_catalog.count(*)::int @@ -1184,7 +1329,9 @@ select pg_catalog.jsonb_build_object( or actual.attgenerated <> '' or actual.atttypmod <> -1 or actual.type_namespace <> 'pg_catalog' - ), + ) +) +|| pg_catalog.jsonb_build_object( 'proposal_table_contract', coalesce(( select pg_catalog.count(*) = 1 and pg_catalog.bool_and( @@ -1625,7 +1772,7 @@ select pg_catalog.jsonb_build_object( 'EXECUTE' ) ) -)::text; +))::text; """.strip() @@ -1833,6 +1980,13 @@ def _negative_checks(run_id: str, source_ref: str) -> tuple[NegativeCheck, ...]: "42501", expected_connection_denial=True, ), + NegativeCheck( + "operator_database_connect", + OPERATOR_DATABASE, + "select 1 /* operator_database_connect */;", + "42501", + expected_connection_denial=True, + ), NegativeCheck( "template_database_connect", TEMPLATE_DATABASE, @@ -2066,6 +2220,10 @@ def _assert_catalog_privilege_posture(posture: dict[str, Any]) -> dict[str, Any] if posture.get("large_object_mutation_routine_residual") != expected_residual: raise VerificationError("large_object_residual_mismatch", "catalog_privilege_posture") sanitized["large_object_mutation_routine_residual"] = expected_residual + expected_provider_residual = [dict(row) for row in PROVIDER_DATABASE_CONNECT_RESIDUAL] + if posture.get("provider_database_connect_residual") != expected_provider_residual: + raise VerificationError("provider_database_residual_mismatch", "catalog_privilege_posture") + sanitized["provider_database_connect_residual"] = expected_provider_residual return sanitized @@ -2337,6 +2495,7 @@ def verify_runtime_permissions( "direct_stage_table_writes_denied": True, "production_eligible": False, "production_promotion_attempted": False, + "provider_database_connect_residual": "exact_cloud_sql_system_pair", "proposal_staging": "function_only", "proposal_write_committed": False, "public_large_object_mutation_residual": "present", diff --git a/tests/test_gcp_leoclean_runtime_permissions.py b/tests/test_gcp_leoclean_runtime_permissions.py index c270f52..ee19fa5 100644 --- a/tests/test_gcp_leoclean_runtime_permissions.py +++ b/tests/test_gcp_leoclean_runtime_permissions.py @@ -184,6 +184,9 @@ class FakeRunner: } for signature, public_execute in verifier.LARGE_OBJECT_MUTATION_ROUTINE_RESIDUAL ], + "provider_database_connect_residual": [ + dict(row) for row in verifier.PROVIDER_DATABASE_CONNECT_RESIDUAL + ], **{field: True for field in verifier.CATALOG_TRUE_FIELDS}, } ) @@ -372,6 +375,9 @@ def test_live_receipt_contract_is_sanitized_and_uses_exact_child_environments() } for signature, public_execute in verifier.LARGE_OBJECT_MUTATION_ROUTINE_RESIDUAL ], + "provider_database_connect_residual": [ + dict(row) for row in verifier.PROVIDER_DATABASE_CONNECT_RESIDUAL + ], } assert receipt["checks"]["stage_function_definition"] == { "acl_exact": True, @@ -400,6 +406,7 @@ def test_live_receipt_contract_is_sanitized_and_uses_exact_child_environments() "direct_stage_table_writes_denied": True, "production_eligible": False, "production_promotion_attempted": False, + "provider_database_connect_residual": "exact_cloud_sql_system_pair", "proposal_staging": "function_only", "proposal_write_committed": False, "public_large_object_mutation_residual": "present", @@ -1146,6 +1153,9 @@ def test_catalog_queries_use_exact_regprocedure_signatures_and_both_membership_d assert "nspname <> 'information_schema'" in catalog_sql assert verifier.STAGE_OWNER_DATABASE_ROLE in catalog_sql assert "public_database_connect_grants" in catalog_sql + assert "provider_database_connect_residual" in catalog_sql + assert "cloudsqladmin" in catalog_sql + assert "template0" in catalog_sql assert "direct_large_object_mutation_routine_acl_entries" in catalog_sql assert "large_object_mutation_routine_residual" in catalog_sql assert "current_setting('lo_compat_privileges') = 'off'" in catalog_sql @@ -1191,6 +1201,30 @@ def test_large_object_provider_residual_drift_fails_closed() -> None: assert caught.value.check == "catalog_privilege_posture" +@pytest.mark.parametrize( + ("field", "drifted_value"), + ( + ("allow_connections", False), + ("database", "cloudsqladmin-shadow"), + ("is_template", True), + ("owner", "postgres"), + ("public_connect", False), + ("public_connect_acl_entries", 2), + ("public_connect_grantable", True), + ), +) +def test_provider_database_connect_residual_drift_fails_closed(field: str, drifted_value: object) -> None: + drifted = [dict(row) for row in verifier.PROVIDER_DATABASE_CONNECT_RESIDUAL] + drifted[0][field] = drifted_value + runner = FakeRunner(catalog_posture_override={"provider_database_connect_residual": drifted}) + + with pytest.raises(verifier.VerificationError) as caught: + run_success(runner) + + assert caught.value.code == "provider_database_residual_mismatch" + assert caught.value.check == "catalog_privilege_posture" + + def test_large_object_residual_probe_must_be_rollback_only() -> None: with pytest.raises(verifier.VerificationError) as caught: verifier._assert_large_object_residual_probe({"created": True, "owned_inside": False}) diff --git a/tests/test_gcp_leoclean_runtime_role_postgres.py b/tests/test_gcp_leoclean_runtime_role_postgres.py index b7c440e..bc79fde 100644 --- a/tests/test_gcp_leoclean_runtime_role_postgres.py +++ b/tests/test_gcp_leoclean_runtime_role_postgres.py @@ -24,6 +24,7 @@ POSTGRES_IMAGE = "postgres@sha256:eb4759788a2182f08257135e61a34f2cfc3c2914079f34 POSTGRES_VERSION_NUM = "160014" DATABASE = "teleo_canonical" RUNTIME_ROLE = "leoclean_kb_runtime" +CONNECT_SENTINEL_DATABASE = "leoclean_connect_sentinel" LOCAL_RUNTIME_PASSWORD = "disposable-r2-runtime-password" IDENTIFIER_RE = re.compile(r"[a-z_][a-z0-9_]*\Z") @@ -126,6 +127,97 @@ def _assert_denied(container: str, sql: str, sqlstate: str = "42501") -> None: ) +def _assert_database_connect_denied(container: str, database: str) -> None: + completed = _psql(container, "select 1;", role=RUNTIME_ROLE, database=database) + assert completed.returncode != 0, f"unexpected database connection success: {database}" + assert f'permission denied for database "{database}"' in completed.stderr + assert "does not have CONNECT privilege" in completed.stderr + + +def _bootstrap_provider_database_connect_residual(container: str) -> list[dict[str, object]]: + _require_success(_psql(container, "create role cloudsqladmin nologin;"), "provider owner fixture") + _require_success( + _psql(container, "alter database template0 owner to cloudsqladmin;"), + "provider template owner fixture", + ) + _require_success( + _psql(container, "create database cloudsqladmin owner cloudsqladmin template template0;"), + "provider system database fixture", + ) + _require_success( + _psql(container, f"create database {CONNECT_SENTINEL_DATABASE};"), + "operator-owned connect sentinel fixture", + ) + return _read_provider_database_connect_residual(container) + + +def _read_provider_database_connect_residual(container: str) -> list[dict[str, object]]: + output = _require_success( + _psql( + container, + """ +select coalesce( + pg_catalog.jsonb_agg( + pg_catalog.jsonb_build_object( + 'allow_connections', observed.allow_connections, + 'database', observed.database_name, + 'database_acl', observed.database_acl, + 'is_template', observed.is_template, + 'owner', observed.owner_name, + 'public_connect', observed.public_connect, + 'public_connect_acl_entries', observed.public_connect_acl_entries, + 'public_connect_grantable', observed.public_connect_grantable + ) + order by observed.database_name + ), + '[]'::pg_catalog.jsonb + )::text + from ( + select database_row.datname as database_name, + owner_role.rolname as owner_name, + database_row.datallowconn as allow_connections, + database_row.datistemplate as is_template, + database_row.datacl::text as database_acl, + pg_catalog.count(*) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + )::integer as public_connect_acl_entries, + coalesce( + pg_catalog.bool_or(true) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + ), + false + ) as public_connect, + coalesce( + pg_catalog.bool_or(acl.is_grantable) filter ( + where acl.grantee = 0 + and acl.privilege_type = 'CONNECT' + ), + false + ) as public_connect_grantable + from pg_catalog.pg_database database_row + join pg_catalog.pg_roles owner_role on owner_role.oid = database_row.datdba + cross join lateral pg_catalog.aclexplode( + coalesce( + database_row.datacl, + pg_catalog.acldefault('d', database_row.datdba) + ) + ) acl + where database_row.datname in ('cloudsqladmin', 'template0') + group by database_row.datname, + owner_role.rolname, + database_row.datallowconn, + database_row.datistemplate, + database_row.datacl + ) observed; +""", + ), + "provider database-connect residual inventory", + ) + return json.loads(output) + + def _generated_read_tables_sql() -> str: statements: list[str] = [] for schema, relation, columns in verifier.READ_COLUMN_ALLOWLIST: @@ -897,6 +989,16 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p f"{probe_output}\ncontainer logs:\n{logs.stdout}\n{logs.stderr}" ) + provider_residual_before = _bootstrap_provider_database_connect_residual(container) + expected_provider_core = [ + {key: value for key, value in row.items() if key not in {"runtime_connect", "stage_owner_connect"}} + for row in verifier.PROVIDER_DATABASE_CONNECT_RESIDUAL + ] + assert [ + {key: value for key, value in row.items() if key != "database_acl"} + for row in provider_residual_before + ] == expected_provider_core + assert provider_residual_before[0]["database_acl"] is None _require_success(_psql(container, "create database teleo_kb;"), "legacy database fixture") _require_success(_psql(container, _bootstrap_sql()), "permission fixture bootstrap") _assert_cloudsql_admin_attribute_compatibility(container) @@ -953,6 +1055,62 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p "direct large-object ACL recovery", ) + _require_success( + _psql( + container, + "create role provider_acl_probe nologin; " + "grant connect on database template0 to provider_acl_probe;", + ), + "provider extra-principal ACL fixture", + ) + provider_acl_drift = _provision(container, "/tmp/gcp_leoclean_runtime_role.sql") + assert provider_acl_drift.returncode != 0 + assert "provider databases retain unexpected ACL entries" in provider_acl_drift.stderr + assert _require_success( + _psql(container, f"select rolcanlogin from pg_catalog.pg_roles where rolname = '{RUNTIME_ROLE}';"), + "provider ACL drift role fence", + ) == "f" + _require_success( + _psql( + container, + "revoke connect on database template0 from provider_acl_probe; " + "drop role provider_acl_probe;", + ), + "provider extra-principal ACL recovery", + ) + + _require_success( + _psql(container, "alter database teleo_canonical set session_preload_libraries = 'auto_explain';"), + "preload-library drift fixture", + ) + preload_drift = _provision(container, "/tmp/gcp_leoclean_runtime_role.sql") + assert preload_drift.returncode != 0 + assert "session and local preload libraries must remain empty" in preload_drift.stderr + assert _require_success( + _psql(container, f"select rolcanlogin from pg_catalog.pg_roles where rolname = '{RUNTIME_ROLE}';"), + "preload-library drift role fence", + ) == "f" + _require_success( + _psql(container, "alter database teleo_canonical reset session_preload_libraries;"), + "preload-library drift recovery", + ) + + _require_success( + _psql(container, "alter database cloudsqladmin allow_connections false;"), + "provider residual drift fixture", + ) + provider_drift = _provision(container, "/tmp/gcp_leoclean_runtime_role.sql") + assert provider_drift.returncode != 0 + assert "Cloud SQL provider database CONNECT residual drifted" in provider_drift.stderr + assert _require_success( + _psql(container, f"select rolcanlogin from pg_catalog.pg_roles where rolname = '{RUNTIME_ROLE}';"), + "provider residual drift role fence", + ) == "f" + _require_success( + _psql(container, "alter database cloudsqladmin allow_connections true;"), + "provider residual drift recovery", + ) + successful_states: list[dict[str, object]] = [] for provision_number in range(1, 4): _require_success( @@ -972,15 +1130,22 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p assert role_state["leoclean_kb_stage_owner"]["password_is_scram"] is False assert role_state["leoclean_kb_stage_owner"]["password_is_null"] is True _assert_large_object_provider_residual_rolls_back(container) + assert _read_provider_database_connect_residual(container) == provider_residual_before successful_states.append(state) assert successful_states[1:] == successful_states[:1] * 2 stable_state = successful_states[0] + _require_success( + _psql(container, f"grant connect on database {CONNECT_SENTINEL_DATABASE} to public;"), + "rollback database ACL fixture", + ) + pre_failure_state = _read_provisioning_state(container) failed = _provision(container, "/tmp/gcp_leoclean_runtime_role_failure.sql") assert failed.returncode != 0 assert re.search(r"ERROR:\s+22012:", failed.stderr) - assert _read_provisioning_state(container) == _state_with_runtime_login(stable_state, can_login=False) + assert _read_provisioning_state(container) == _state_with_runtime_login(pre_failure_state, can_login=False) + assert _read_provider_database_connect_residual(container) == provider_residual_before disabled_login = _psql(container, "select 1;", role=RUNTIME_ROLE) assert disabled_login.returncode != 0 assert f'role "{RUNTIME_ROLE}" is not permitted to log in' in disabled_login.stderr @@ -997,6 +1162,14 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p ) assert identity == f"{RUNTIME_ROLE}|{DATABASE}" + for denied_database in ("postgres", "teleo_kb", "template1", CONNECT_SENTINEL_DATABASE): + _assert_database_connect_denied(container, denied_database) + provider_identity = _require_success( + _psql(container, "select current_user || '|' || current_database();", role=RUNTIME_ROLE, database="cloudsqladmin"), + "provider-owned residual connection", + ) + assert provider_identity == f"{RUNTIME_ROLE}|cloudsqladmin" + stage_definition = json.loads( _require_success( _psql(container, verifier._stage_function_definition_sql(), role=RUNTIME_ROLE), @@ -1005,6 +1178,19 @@ def test_digest_pinned_network_disabled_postgres_16_14_permission_contract(tmp_p ) assert verifier._assert_stage_function_definition(stage_definition)["owner_execute"] is False + catalog_output = _require_success( + _psql(container, verifier._catalog_privilege_posture_sql(), role=RUNTIME_ROLE), + "external verifier catalog query", + ) + assert len(catalog_output.splitlines()) == 1, repr(catalog_output) + _parsed, parsed_end = json.JSONDecoder().raw_decode(catalog_output) + assert not catalog_output[parsed_end:].strip(), repr(catalog_output[parsed_end : parsed_end + 200]) + catalog_posture = json.loads(catalog_output) + verified_catalog = verifier._assert_catalog_privilege_posture(catalog_posture) + assert verified_catalog["provider_database_connect_residual"] == [ + dict(row) for row in verifier.PROVIDER_DATABASE_CONNECT_RESIDUAL + ] + before = _read_fingerprint(container) assert before == { f"{schema}.{relation}": (1 if (schema, relation) == ("public", "agents") else 0) diff --git a/tests/test_hermes_leoclean_kb_bridge_source.py b/tests/test_hermes_leoclean_kb_bridge_source.py index 1801119..09be5a7 100644 --- a/tests/test_hermes_leoclean_kb_bridge_source.py +++ b/tests/test_hermes_leoclean_kb_bridge_source.py @@ -1172,7 +1172,14 @@ def test_gcp_runtime_role_uses_one_narrow_staging_function() -> None: assert "unexpectedly has database CREATE" in sql assert "unexpectedly has schema CREATE" in sql assert "runtime CONNECT reaches a noncanonical database" in sql - assert "PUBLIC retains database CONNECT" in sql + assert "PUBLIC retains unexpected database CONNECT" in sql + assert "Cloud SQL provider database CONNECT residual drifted" in sql + assert "provider databases retain unexpected ACL entries" in sql + assert "('cloudsqladmin'::name, 'cloudsqladmin'::name, true, false, true, 1, false)" in sql + assert "('template0'::name, 'cloudsqladmin'::name, false, true, null::boolean, 1, false)" in sql + assert "cloudsql%" not in sql + assert "current_setting('session_preload_libraries') <> ''" in sql + assert "current_setting('local_preload_libraries') <> ''" in sql assert "provider-owned large-object routine residual drifted" in sql assert "scoped Leo roles have direct large-object routine ACLs" in sql assert "lo_compat_privileges must remain off" in sql