Some checks are pending
CI / lint-and-test (push) Waiting to run
PostgreSQL grants EXECUTE to PUBLIC on every new function by default. The verification block correctly rejects this, so revoke it before granting the runtime role.
1790 lines
74 KiB
PL/PgSQL
1790 lines
74 KiB
PL/PgSQL
\set ON_ERROR_STOP on
|
|
|
|
-- One-time GCP staging provisioning for Leo's Cloud SQL identity. Run this
|
|
-- only through ops/provision_gcp_leoclean_runtime_role.sh; psql's \password
|
|
-- command encrypts the supplied value client-side, so cleartext is never SQL.
|
|
--
|
|
-- The login can read the canonical allowlist and can stage a pending proposal
|
|
-- through one narrowly-scoped SECURITY DEFINER function. It cannot inherit or
|
|
-- SET ROLE into another principal, forge the proposer identity, write directly
|
|
-- to canonical/staging tables, or inspect the legacy teleo_restore audit schema.
|
|
-- Keep the runtime unable to authenticate until both database phases have
|
|
-- completed and verified. Any failure before the final transaction therefore
|
|
-- leaves a disabled role instead of a partially provisioned login.
|
|
select 'create role leoclean_kb_runtime nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8'
|
|
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_runtime')
|
|
\gexec
|
|
|
|
select 'create role leoclean_kb_stage_owner nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1'
|
|
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'leoclean_kb_stage_owner')
|
|
\gexec
|
|
|
|
-- Cloud SQL's postgres administrator has CREATEROLE but is deliberately not a
|
|
-- true PostgreSQL superuser. It may not spell SUPERUSER, REPLICATION, or
|
|
-- BYPASSRLS attributes in ALTER ROLE, even when setting their safe false form.
|
|
-- CREATE ROLE above establishes those attributes as false; the verification
|
|
-- blocks below fail closed if an existing role ever carries any of them.
|
|
alter role leoclean_kb_runtime
|
|
with nologin nocreatedb nocreaterole noinherit connection limit 8;
|
|
alter role leoclean_kb_stage_owner
|
|
with nologin nocreatedb nocreaterole noinherit connection limit -1
|
|
password null;
|
|
|
|
-- Snapshot every non-superuser principal connected to either scoped role before
|
|
-- removing the direct membership edges. Existing sessions can retain an
|
|
-- already-selected SET ROLE identity after NOLOGIN/revocation, so all other
|
|
-- connected sessions must be terminated before password or privilege work.
|
|
create temporary table leoclean_fenced_roles (
|
|
role_oid oid primary key,
|
|
role_name name unique not null
|
|
) on commit preserve rows;
|
|
|
|
with recursive connected(role_oid) as (
|
|
values
|
|
('leoclean_kb_runtime'::pg_catalog.regrole::oid),
|
|
('leoclean_kb_stage_owner'::pg_catalog.regrole::oid)
|
|
union
|
|
select membership.member
|
|
from connected
|
|
join pg_catalog.pg_auth_members membership
|
|
on membership.roleid = connected.role_oid
|
|
)
|
|
insert into pg_temp.leoclean_fenced_roles (role_oid, role_name)
|
|
select role_row.oid, role_row.rolname
|
|
from connected
|
|
join pg_catalog.pg_roles role_row on role_row.oid = connected.role_oid
|
|
where not role_row.rolsuper;
|
|
|
|
-- NOINHERIT does not prevent SET ROLE. Remove every direct membership edge in
|
|
-- either direction before terminating the snapshotted sessions, closing the
|
|
-- race for newly connected membership principals.
|
|
select format('revoke %I from %I', granted_role.rolname, member_role.rolname)
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
where granted_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
or member_role.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
\gexec
|
|
|
|
do $session_fence$
|
|
declare
|
|
target record;
|
|
remaining text;
|
|
begin
|
|
for target in
|
|
select activity.pid
|
|
from pg_catalog.pg_stat_activity activity
|
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = activity.usename
|
|
where activity.pid <> pg_catalog.pg_backend_pid()
|
|
and activity.backend_type = 'client backend'
|
|
loop
|
|
if not pg_catalog.pg_terminate_backend(target.pid, 5000) then
|
|
raise exception 'could not terminate a pre-existing scoped-role session';
|
|
end if;
|
|
end loop;
|
|
|
|
select pg_catalog.string_agg(activity.pid::text, ',' order by activity.pid)
|
|
into remaining
|
|
from pg_catalog.pg_stat_activity activity
|
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = activity.usename
|
|
where activity.pid <> pg_catalog.pg_backend_pid()
|
|
and activity.backend_type = 'client backend';
|
|
if remaining is not null then
|
|
raise exception 'pre-existing scoped-role sessions remain after termination';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(prepared.gid, ',' order by prepared.gid)
|
|
into remaining
|
|
from pg_catalog.pg_prepared_xacts prepared
|
|
join pg_temp.leoclean_fenced_roles fenced on fenced.role_name = prepared.owner;
|
|
if remaining is not null then
|
|
raise exception 'scoped-role prepared transactions must be resolved before provisioning';
|
|
end if;
|
|
end
|
|
$session_fence$;
|
|
|
|
-- This is deliberately after the autocommitted NOLOGIN/session/membership
|
|
-- fence. Interruption can leave a disabled role, never a still-enabled old
|
|
-- login or an old connected session. The wrapper supplies the two prompts.
|
|
\password leoclean_kb_runtime
|
|
|
|
alter role leoclean_kb_runtime reset all;
|
|
alter role leoclean_kb_stage_owner reset all;
|
|
drop table pg_temp.leoclean_fenced_roles;
|
|
|
|
select pg_catalog.format(
|
|
'alter role %I in database %I reset all',
|
|
role_row.rolname,
|
|
database_row.datname
|
|
)
|
|
from pg_catalog.pg_db_role_setting setting_row
|
|
join pg_catalog.pg_roles role_row on role_row.oid = setting_row.setrole
|
|
join pg_catalog.pg_database database_row on database_row.oid = setting_row.setdatabase
|
|
where role_row.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
\gexec
|
|
|
|
-- PostgreSQL normally grants TEMP to PUBLIC. A role-specific REVOKE cannot
|
|
-- override that inherited grant, while REVOKE TEMP FROM PUBLIC would be a
|
|
-- database-wide change that could break kb_apply/review or operator workflows.
|
|
-- Leave PUBLIC TEMP policy unchanged here and surface it in the final receipt.
|
|
-- TEMP cannot shadow this SECURITY DEFINER boundary: pg_catalog is first,
|
|
-- pg_temp is explicitly last, and every relation reference is schema-qualified.
|
|
-- Database CREATE, direct TEMP grants, and all unexpected direct database ACLs
|
|
-- are rejected below. A database-wide TEMP removal requires a separate role-use
|
|
-- inventory and explicit grants to every workload that still needs it.
|
|
|
|
alter role leoclean_kb_runtime in database teleo_canonical reset all;
|
|
alter role leoclean_kb_stage_owner in database teleo_canonical reset all;
|
|
alter role leoclean_kb_runtime in database teleo_kb reset all;
|
|
alter role leoclean_kb_stage_owner in database teleo_kb reset all;
|
|
|
|
alter role leoclean_kb_runtime in database teleo_canonical
|
|
set search_path = pg_catalog, public, kb_stage;
|
|
alter role leoclean_kb_runtime in database teleo_canonical
|
|
set statement_timeout = '15s';
|
|
alter role leoclean_kb_runtime in database teleo_canonical
|
|
set lock_timeout = '2s';
|
|
|
|
\connect teleo_canonical
|
|
|
|
begin;
|
|
|
|
-- CONNECT granted to PUBLIC cannot be denied to one role with a role-specific
|
|
-- 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,
|
|
primary key (role_name, database_name)
|
|
) on commit drop;
|
|
|
|
insert into pg_temp.leoclean_preserved_database_connect (role_name, database_name)
|
|
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(
|
|
'revoke all privileges on database %I from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
|
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(
|
|
'grant connect on database %I to %I',
|
|
preserved.database_name,
|
|
preserved.role_name
|
|
)
|
|
from pg_temp.leoclean_preserved_database_connect preserved
|
|
order by preserved.database_name, preserved.role_name
|
|
\gexec
|
|
|
|
grant connect on database teleo_canonical to leoclean_kb_runtime;
|
|
|
|
-- The deploy principal needs temporary ownership authority to replace an
|
|
-- existing function on idempotent reruns. This membership is transaction-local
|
|
-- and is revoked before verification/commit.
|
|
select format('grant leoclean_kb_stage_owner to %I', current_user)
|
|
where current_user <> 'leoclean_kb_stage_owner'
|
|
\gexec
|
|
|
|
revoke all on schema public, kb_stage
|
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
|
revoke all privileges on all tables in schema public, kb_stage
|
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
|
revoke all privileges on all sequences in schema public, kb_stage
|
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
|
revoke all privileges on all functions in schema public, kb_stage
|
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
|
revoke all privileges on all procedures in schema public, kb_stage
|
|
from leoclean_kb_runtime, leoclean_kb_stage_owner;
|
|
|
|
-- Built-in defaults also grant PUBLIC EXECUTE on newly created application
|
|
-- routines. Preserve every existing non-scoped, non-superuser role's effective access,
|
|
-- remove PUBLIC/scoped execution from the complete application routine set,
|
|
-- and restore the inventory explicitly. The reviewed staging function is
|
|
-- normalized again after CREATE OR REPLACE below. Superusers retain their
|
|
-- inherent access and must not receive redundant explicit ACLs that would make
|
|
-- the guarded review/apply prerequisite non-composable on a local PG16 canary.
|
|
create temporary table leoclean_preserved_app_routine_execute (
|
|
role_name name not null,
|
|
function_oid oid not null,
|
|
primary key (role_name, function_oid)
|
|
) on commit drop;
|
|
|
|
insert into pg_temp.leoclean_preserved_app_routine_execute (role_name, function_oid)
|
|
select role_row.rolname, function_row.oid
|
|
from pg_catalog.pg_roles role_row
|
|
cross join pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where role_row.rolname not in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
and not role_row.rolsuper
|
|
and namespace.nspname in ('public', 'kb_stage')
|
|
and pg_catalog.has_function_privilege(role_row.oid, function_row.oid, 'EXECUTE');
|
|
|
|
select pg_catalog.format(
|
|
'revoke execute on %s %I.%I(%s) from public, leoclean_kb_runtime, leoclean_kb_stage_owner',
|
|
case when function_row.prokind = 'p' then 'procedure' else 'function' end,
|
|
namespace.nspname,
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
|
)
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
\gexec
|
|
|
|
select pg_catalog.format(
|
|
'grant execute on %s %I.%I(%s) to %I',
|
|
case when function_row.prokind = 'p' then 'procedure' else 'function' end,
|
|
namespace.nspname,
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid),
|
|
preserved.role_name
|
|
)
|
|
from pg_temp.leoclean_preserved_app_routine_execute preserved
|
|
join pg_catalog.pg_proc function_row on function_row.oid = preserved.function_oid
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
order by namespace.nspname, function_row.proname, preserved.role_name
|
|
\gexec
|
|
|
|
select pg_catalog.format(
|
|
'revoke all privileges on large object %s from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
|
metadata.oid
|
|
)
|
|
from pg_catalog.pg_largeobject_metadata metadata
|
|
\gexec
|
|
|
|
-- Cloud SQL owns pg_catalog and does not expose its true PostgreSQL
|
|
-- superuser. Its customer administrator therefore cannot revoke the built-in
|
|
-- PUBLIC EXECUTE grants on large-object mutators. Keep that provider-owned
|
|
-- residual immutable and explicit: reject inventory drift and any direct ACL
|
|
-- granted to either scoped role, while separately proving that neither role
|
|
-- owns or can reach an existing large object. The no-send staging runtime has
|
|
-- no raw-SQL surface and its query-contract checker rejects every routine in
|
|
-- this inventory. This is a staging-only compensating boundary, not a claim
|
|
-- that arbitrary use of the raw database credential is write-free.
|
|
create temporary table leoclean_expected_lo_routine_residual (
|
|
signature text primary key,
|
|
public_execute boolean not null
|
|
) on commit drop;
|
|
|
|
insert into pg_temp.leoclean_expected_lo_routine_residual (signature, public_execute) values
|
|
('lo_creat(integer)', true),
|
|
('lo_create(oid)', true),
|
|
('lo_export(oid, text)', false),
|
|
('lo_from_bytea(oid, bytea)', true),
|
|
('lo_import(text)', false),
|
|
('lo_import(text, oid)', false),
|
|
('lo_open(oid, integer)', true),
|
|
('lo_put(oid, bigint, bytea)', true),
|
|
('lo_truncate(integer, integer)', true),
|
|
('lo_truncate64(integer, bigint)', true),
|
|
('lo_unlink(oid)', true),
|
|
('lowrite(integer, bytea)', true);
|
|
|
|
do $large_object_residual$
|
|
declare
|
|
unexpected text;
|
|
begin
|
|
with actual as (
|
|
select function_row.oid,
|
|
pg_catalog.format(
|
|
'%I(%s)',
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
|
) as signature,
|
|
exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(function_row.proacl, pg_catalog.acldefault('f', function_row.proowner))
|
|
) acl
|
|
where acl.grantee = 0
|
|
and acl.privilege_type = 'EXECUTE'
|
|
) as public_execute,
|
|
pg_catalog.has_function_privilege('leoclean_kb_runtime', function_row.oid, 'EXECUTE') as runtime_execute,
|
|
pg_catalog.has_function_privilege('leoclean_kb_stage_owner', function_row.oid, 'EXECUTE') as owner_execute
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where namespace.nspname = 'pg_catalog'
|
|
and function_row.proname in (
|
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
|
)
|
|
)
|
|
select pg_catalog.string_agg(
|
|
coalesce(expected.signature, actual.signature),
|
|
', '
|
|
order by coalesce(expected.signature, actual.signature)
|
|
)
|
|
into unexpected
|
|
from pg_temp.leoclean_expected_lo_routine_residual expected
|
|
full join actual using (signature)
|
|
where expected.signature is null
|
|
or actual.signature is null
|
|
or actual.public_execute is distinct from expected.public_execute
|
|
or actual.runtime_execute is distinct from expected.public_execute
|
|
or actual.owner_execute is distinct from expected.public_execute;
|
|
if unexpected is not null then
|
|
raise exception 'provider-owned large-object routine residual drifted: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I(%s):%s',
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid),
|
|
role_row.rolname
|
|
),
|
|
', '
|
|
order by function_row.proname, role_row.rolname
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(function_row.proacl) acl
|
|
join pg_catalog.pg_roles role_row on role_row.oid = acl.grantee
|
|
where namespace.nspname = 'pg_catalog'
|
|
and function_row.proname in (
|
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
|
)
|
|
and role_row.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
and acl.privilege_type = 'EXECUTE';
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles have direct large-object routine ACLs: %', unexpected;
|
|
end if;
|
|
end
|
|
$large_object_residual$;
|
|
|
|
select pg_catalog.format(
|
|
'revoke all privileges on parameter %I from leoclean_kb_runtime, leoclean_kb_stage_owner',
|
|
parameter_acl.parname
|
|
)
|
|
from pg_catalog.pg_parameter_acl parameter_acl
|
|
\gexec
|
|
|
|
-- Table-level REVOKE does not clear column ACLs. Clear every column ACL for both
|
|
-- principals before rebuilding the exact allowlist below.
|
|
select format(
|
|
'revoke all privileges (%s) on table %I.%I from %I',
|
|
pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum),
|
|
namespace.nspname,
|
|
relation.relname,
|
|
target.rolname
|
|
)
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
cross join (
|
|
values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')
|
|
) as target(rolname)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
group by namespace.nspname, relation.relname, target.rolname
|
|
\gexec
|
|
|
|
grant usage on schema public, kb_stage to leoclean_kb_runtime;
|
|
|
|
-- Bind runtime reads to the exact 92 columns used by the reviewed adapter.
|
|
-- Table-level SELECT is deliberately absent so later columns do not become
|
|
-- readable before this manifest and the adapter are reviewed together.
|
|
create temporary table leoclean_runtime_read_column_allowlist (
|
|
schema_name name not null,
|
|
relation_name name not null,
|
|
column_name name not null,
|
|
column_ordinal smallint not null,
|
|
primary key (schema_name, relation_name, column_name),
|
|
unique (schema_name, relation_name, column_ordinal)
|
|
) on commit drop;
|
|
|
|
insert into pg_temp.leoclean_runtime_read_column_allowlist (
|
|
schema_name,
|
|
relation_name,
|
|
column_name,
|
|
column_ordinal
|
|
) values
|
|
('public', 'agents', 'id', 1),
|
|
('public', 'agents', 'handle', 2),
|
|
('public', 'claims', 'id', 1),
|
|
('public', 'claims', 'type', 2),
|
|
('public', 'claims', 'text', 3),
|
|
('public', 'claims', 'status', 4),
|
|
('public', 'claims', 'confidence', 5),
|
|
('public', 'claims', 'tags', 6),
|
|
('public', 'claims', 'superseded_by', 7),
|
|
('public', 'claims', 'created_at', 8),
|
|
('public', 'claims', 'updated_at', 9),
|
|
('public', 'claim_evidence', 'claim_id', 1),
|
|
('public', 'claim_evidence', 'source_id', 2),
|
|
('public', 'claim_evidence', 'role', 3),
|
|
('public', 'claim_evidence', 'weight', 4),
|
|
('public', 'claim_edges', 'id', 1),
|
|
('public', 'claim_edges', 'from_claim', 2),
|
|
('public', 'claim_edges', 'to_claim', 3),
|
|
('public', 'claim_edges', 'edge_type', 4),
|
|
('public', 'sources', 'id', 1),
|
|
('public', 'sources', 'source_type', 2),
|
|
('public', 'sources', 'url', 3),
|
|
('public', 'sources', 'storage_path', 4),
|
|
('public', 'sources', 'excerpt', 5),
|
|
('public', 'sources', 'hash', 6),
|
|
('public', 'personas', 'agent_id', 1),
|
|
('public', 'personas', 'name', 2),
|
|
('public', 'personas', 'voice', 3),
|
|
('public', 'personas', 'role', 4),
|
|
('public', 'personas', 'source_ref', 5),
|
|
('public', 'personas', 'lens', 6),
|
|
('public', 'strategies', 'agent_id', 1),
|
|
('public', 'strategies', 'diagnosis', 2),
|
|
('public', 'strategies', 'guiding_policy', 3),
|
|
('public', 'strategies', 'proximate_objectives', 4),
|
|
('public', 'strategies', 'version', 5),
|
|
('public', 'strategies', 'active', 6),
|
|
('public', 'beliefs', 'agent_id', 1),
|
|
('public', 'beliefs', 'level', 2),
|
|
('public', 'beliefs', 'statement', 3),
|
|
('public', 'beliefs', 'falsifier', 4),
|
|
('public', 'beliefs', 'rank', 5),
|
|
('public', 'beliefs', 'status', 6),
|
|
('public', 'blindspots', 'agent_id', 1),
|
|
('public', 'blindspots', 'name', 2),
|
|
('public', 'blindspots', 'description', 3),
|
|
('public', 'blindspots', 'correction', 4),
|
|
('public', 'blindspots', 'kind', 5),
|
|
('public', 'blindspots', 'status', 6),
|
|
('public', 'agent_roles', 'agent_id', 1),
|
|
('public', 'agent_roles', 'title', 2),
|
|
('public', 'agent_roles', 'description', 3),
|
|
('public', 'peer_models', 'subject_id', 1),
|
|
('public', 'peer_models', 'peer_id', 2),
|
|
('public', 'peer_models', 'domain', 3),
|
|
('public', 'peer_models', 'outranks_on', 4),
|
|
('public', 'peer_models', 'deference_rule', 5),
|
|
('public', 'behavioral_rules', 'agent_id', 1),
|
|
('public', 'behavioral_rules', 'category', 2),
|
|
('public', 'behavioral_rules', 'rule', 3),
|
|
('public', 'behavioral_rules', 'rationale', 4),
|
|
('public', 'contributor_rules', 'agent_id', 1),
|
|
('public', 'contributor_rules', 'name', 2),
|
|
('public', 'contributor_rules', 'directive', 3),
|
|
('public', 'contributor_rules', 'ci_tier', 4),
|
|
('public', 'contributor_rules', 'weighting', 5),
|
|
('public', 'contributor_rules', 'rationale', 6),
|
|
('public', 'reasoning_tools', 'agent_id', 1),
|
|
('public', 'reasoning_tools', 'name', 2),
|
|
('public', 'reasoning_tools', 'description', 3),
|
|
('public', 'reasoning_tools', 'category', 4),
|
|
('public', 'governance_gates', 'agent_id', 1),
|
|
('public', 'governance_gates', 'name', 2),
|
|
('public', 'governance_gates', 'criteria', 3),
|
|
('public', 'governance_gates', 'evidence_bar', 4),
|
|
('public', 'governance_gates', 'pass_condition', 5),
|
|
('kb_stage', 'kb_proposals', 'id', 1),
|
|
('kb_stage', 'kb_proposals', 'proposal_type', 2),
|
|
('kb_stage', 'kb_proposals', 'status', 3),
|
|
('kb_stage', 'kb_proposals', 'proposed_by_handle', 4),
|
|
('kb_stage', 'kb_proposals', 'proposed_by_agent_id', 5),
|
|
('kb_stage', 'kb_proposals', 'channel', 6),
|
|
('kb_stage', 'kb_proposals', 'source_ref', 7),
|
|
('kb_stage', 'kb_proposals', 'rationale', 8),
|
|
('kb_stage', 'kb_proposals', 'payload', 9),
|
|
('kb_stage', 'kb_proposals', 'reviewed_by_handle', 10),
|
|
('kb_stage', 'kb_proposals', 'reviewed_at', 11),
|
|
('kb_stage', 'kb_proposals', 'review_note', 12),
|
|
('kb_stage', 'kb_proposals', 'applied_by_handle', 13),
|
|
('kb_stage', 'kb_proposals', 'applied_at', 14),
|
|
('kb_stage', 'kb_proposals', 'created_at', 15),
|
|
('kb_stage', 'kb_proposals', 'updated_at', 16);
|
|
|
|
select pg_catalog.format(
|
|
'grant select (%s) on table %I.%I to leoclean_kb_runtime',
|
|
pg_catalog.string_agg(
|
|
pg_catalog.quote_ident(allowlist.column_name),
|
|
', '
|
|
order by allowlist.column_ordinal
|
|
),
|
|
allowlist.schema_name,
|
|
allowlist.relation_name
|
|
)
|
|
from pg_temp.leoclean_runtime_read_column_allowlist allowlist
|
|
group by allowlist.schema_name, allowlist.relation_name
|
|
order by allowlist.schema_name, allowlist.relation_name
|
|
\gexec
|
|
|
|
-- The function owner is a dedicated NOLOGIN role. Grant only the columns the
|
|
-- function reads/inserts. CREATE is temporary and is revoked immediately after
|
|
-- ownership is assigned.
|
|
grant usage, create on schema kb_stage to leoclean_kb_stage_owner;
|
|
grant usage on schema public to leoclean_kb_stage_owner;
|
|
grant select (id, handle) on public.agents to leoclean_kb_stage_owner;
|
|
grant select on kb_stage.kb_proposals to leoclean_kb_stage_owner;
|
|
grant insert (
|
|
proposal_type,
|
|
status,
|
|
proposed_by_handle,
|
|
proposed_by_agent_id,
|
|
channel,
|
|
source_ref,
|
|
rationale,
|
|
payload
|
|
) on kb_stage.kb_proposals to leoclean_kb_stage_owner;
|
|
|
|
-- Drop both overloads so CREATE (not REPLACE) works regardless of prior
|
|
-- ownership. Cloud SQL's postgres cannot replace a function owned by another role.
|
|
drop function if exists kb_stage.stage_leoclean_proposal(text, text, text, text, text, jsonb);
|
|
drop function if exists kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb);
|
|
|
|
create function kb_stage.stage_leoclean_proposal(
|
|
p_proposal_type text,
|
|
p_channel text,
|
|
p_source_ref text,
|
|
p_rationale text,
|
|
p_payload jsonb
|
|
) returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = pg_catalog, pg_temp
|
|
as $function$
|
|
declare
|
|
staged kb_stage.kb_proposals%rowtype;
|
|
proposer_id uuid;
|
|
begin
|
|
if session_user <> 'leoclean_kb_runtime' then
|
|
raise exception 'stage_leoclean_proposal is restricted to leoclean_kb_runtime'
|
|
using errcode = '42501';
|
|
end if;
|
|
if p_proposal_type not in ('revise_claim', 'revise_strategy', 'add_edge') then
|
|
raise exception 'unsupported Leo proposal type: %', p_proposal_type using errcode = '22023';
|
|
end if;
|
|
if nullif(btrim(p_source_ref), '') is null then
|
|
raise exception 'source_ref is required' using errcode = '22023';
|
|
end if;
|
|
if nullif(btrim(p_rationale), '') is null then
|
|
raise exception 'rationale is required' using errcode = '22023';
|
|
end if;
|
|
if p_payload is null or jsonb_typeof(p_payload) <> 'object' then
|
|
raise exception 'proposal payload must be a JSON object' using errcode = '22023';
|
|
end if;
|
|
|
|
select agent.id
|
|
into proposer_id
|
|
from public.agents as agent
|
|
where agent.handle = 'leo';
|
|
|
|
if proposer_id is null then
|
|
raise exception 'canonical Leo agent row is required' using errcode = '23503';
|
|
end if;
|
|
|
|
insert into kb_stage.kb_proposals (
|
|
proposal_type,
|
|
status,
|
|
proposed_by_handle,
|
|
proposed_by_agent_id,
|
|
channel,
|
|
source_ref,
|
|
rationale,
|
|
payload
|
|
) values (
|
|
p_proposal_type,
|
|
'pending_review',
|
|
'leo',
|
|
proposer_id,
|
|
coalesce(nullif(p_channel, ''), 'telegram'),
|
|
p_source_ref,
|
|
p_rationale,
|
|
p_payload
|
|
)
|
|
returning * into staged;
|
|
|
|
return to_jsonb(staged);
|
|
end
|
|
$function$;
|
|
|
|
-- Revoke default PUBLIC execute, grant only to runtime, then transfer ownership.
|
|
revoke all on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb) from public;
|
|
grant execute on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
|
to leoclean_kb_runtime;
|
|
alter function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
|
owner to leoclean_kb_stage_owner;
|
|
revoke create on schema kb_stage from leoclean_kb_stage_owner;
|
|
|
|
select format('revoke leoclean_kb_stage_owner from %I', current_user)
|
|
where current_user <> 'leoclean_kb_stage_owner'
|
|
\gexec
|
|
|
|
do $verification$
|
|
declare
|
|
unexpected text;
|
|
runtime_oid oid := 'leoclean_kb_runtime'::pg_catalog.regrole::oid;
|
|
owner_oid oid := 'leoclean_kb_stage_owner'::pg_catalog.regrole::oid;
|
|
stage_function oid := 'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)'::pg_catalog.regprocedure::oid;
|
|
begin
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.pg_roles role_row
|
|
where role_row.rolname = 'leoclean_kb_runtime'
|
|
and (role_row.rolcanlogin
|
|
or role_row.rolsuper
|
|
or role_row.rolcreatedb
|
|
or role_row.rolcreaterole
|
|
or role_row.rolinherit
|
|
or role_row.rolreplication
|
|
or role_row.rolbypassrls
|
|
or role_row.rolconnlimit <> 8)
|
|
) then
|
|
raise exception 'leoclean_kb_runtime has unsafe provisioning role attributes';
|
|
end if;
|
|
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.pg_roles role_row
|
|
where role_row.rolname = 'leoclean_kb_stage_owner'
|
|
and (role_row.rolcanlogin
|
|
or role_row.rolsuper
|
|
or role_row.rolcreatedb
|
|
or role_row.rolcreaterole
|
|
or role_row.rolinherit
|
|
or role_row.rolreplication
|
|
or role_row.rolbypassrls
|
|
or role_row.rolconnlimit <> -1)
|
|
) then
|
|
raise exception 'leoclean_kb_stage_owner has unsafe role attributes';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I->%I', granted_role.rolname, member_role.rolname),
|
|
', '
|
|
order by granted_role.rolname, member_role.rolname
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
where membership.roleid in (runtime_oid, owner_oid)
|
|
or membership.member in (runtime_oid, owner_oid);
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles retain role memberships: %', unexpected;
|
|
end if;
|
|
|
|
-- Reject explicit default-ACL state that could expose future objects to a
|
|
-- scoped role. This is deliberately checked before later capability
|
|
-- validation, while the phase-1 NOLOGIN fence is still in force.
|
|
-- PostgreSQL hard-wired PUBLIC defaults for routines and types are not rows
|
|
-- in pg_default_acl and remain a separately bounded staging residual.
|
|
-- The closure follows member -> granted role conservatively; any membership
|
|
-- edge is independently forbidden for these roles.
|
|
with recursive scoped_role(oid) as (
|
|
values (runtime_oid), (owner_oid)
|
|
), reachable_role(oid) as (
|
|
select oid from scoped_role
|
|
union
|
|
select membership.roleid
|
|
from pg_catalog.pg_auth_members membership
|
|
join reachable_role on reachable_role.oid = membership.member
|
|
)
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'owner=%I schema=%s object_type=%s grantee=%s privilege=%s grantable=%s',
|
|
default_owner.rolname,
|
|
coalesce(default_namespace.nspname, '<global>'),
|
|
default_acl.defaclobjtype,
|
|
case when acl.grantee = 0 then 'PUBLIC' else acl_grantee.rolname end,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
),
|
|
', '
|
|
order by default_owner.rolname,
|
|
default_namespace.nspname nulls first,
|
|
default_acl.defaclobjtype,
|
|
acl.grantee,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_default_acl default_acl
|
|
join pg_catalog.pg_roles default_owner on default_owner.oid = default_acl.defaclrole
|
|
left join pg_catalog.pg_namespace default_namespace on default_namespace.oid = default_acl.defaclnamespace
|
|
left join lateral pg_catalog.aclexplode(default_acl.defaclacl) acl on true
|
|
left join pg_catalog.pg_roles acl_grantee on acl_grantee.oid = acl.grantee
|
|
where default_acl.defaclobjtype in ('r', 'S', 'f', 'T', 'n')
|
|
and (
|
|
default_acl.defaclrole in (select oid from reachable_role)
|
|
or acl.grantee = 0
|
|
or acl.grantee in (select oid from reachable_role)
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles have unsafe explicit default ACL entries: %', unexpected;
|
|
end if;
|
|
|
|
if not exists (
|
|
select 1
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
where namespace.nspname = 'kb_stage'
|
|
and relation.relname = 'kb_proposals'
|
|
and relation.relkind = 'r'
|
|
and relation.relpersistence = 'p'
|
|
and not relation.relrowsecurity
|
|
and not relation.relforcerowsecurity
|
|
) then
|
|
raise exception 'kb_stage.kb_proposals is not the reviewed permanent base table';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, relation.relkind),
|
|
', '
|
|
order by namespace.nspname, relation.relname
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'), ('public', 'claims'), ('public', 'claim_evidence'),
|
|
('public', 'claim_edges'), ('public', 'sources'), ('public', 'personas'),
|
|
('public', 'strategies'), ('public', 'beliefs'), ('public', 'blindspots'),
|
|
('public', 'agent_roles'), ('public', 'peer_models'), ('public', 'behavioral_rules'),
|
|
('public', 'contributor_rules'), ('public', 'reasoning_tools'),
|
|
('public', 'governance_gates'), ('kb_stage', 'kb_proposals')
|
|
)
|
|
and relation.relkind <> 'r';
|
|
if unexpected is not null then
|
|
raise exception 'Leo read allowlist contains a non-base relation: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%s->%s', inheritance.inhparent::pg_catalog.regclass, inheritance.inhrelid::pg_catalog.regclass),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_inherits inheritance
|
|
where inheritance.inhparent in (
|
|
'public.agents'::pg_catalog.regclass, 'public.claims'::pg_catalog.regclass,
|
|
'public.claim_evidence'::pg_catalog.regclass, 'public.claim_edges'::pg_catalog.regclass,
|
|
'public.sources'::pg_catalog.regclass, 'public.personas'::pg_catalog.regclass,
|
|
'public.strategies'::pg_catalog.regclass, 'public.beliefs'::pg_catalog.regclass,
|
|
'public.blindspots'::pg_catalog.regclass, 'public.agent_roles'::pg_catalog.regclass,
|
|
'public.peer_models'::pg_catalog.regclass, 'public.behavioral_rules'::pg_catalog.regclass,
|
|
'public.contributor_rules'::pg_catalog.regclass, 'public.reasoning_tools'::pg_catalog.regclass,
|
|
'public.governance_gates'::pg_catalog.regclass, 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
)
|
|
or inheritance.inhrelid in (
|
|
'public.agents'::pg_catalog.regclass, 'public.claims'::pg_catalog.regclass,
|
|
'public.claim_evidence'::pg_catalog.regclass, 'public.claim_edges'::pg_catalog.regclass,
|
|
'public.sources'::pg_catalog.regclass, 'public.personas'::pg_catalog.regclass,
|
|
'public.strategies'::pg_catalog.regclass, 'public.beliefs'::pg_catalog.regclass,
|
|
'public.blindspots'::pg_catalog.regclass, 'public.agent_roles'::pg_catalog.regclass,
|
|
'public.peer_models'::pg_catalog.regclass, 'public.behavioral_rules'::pg_catalog.regclass,
|
|
'public.contributor_rules'::pg_catalog.regclass, 'public.reasoning_tools'::pg_catalog.regclass,
|
|
'public.governance_gates'::pg_catalog.regclass, 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'Leo read allowlist has inheritance edges: %', unexpected;
|
|
end if;
|
|
|
|
with expected(ordinal, attname, typname, not_null, has_default, collated) as (
|
|
values
|
|
(1, 'id', 'uuid', true, true, false),
|
|
(2, 'proposal_type', 'text', true, false, true),
|
|
(3, 'status', 'text', true, true, true),
|
|
(4, 'proposed_by_handle', 'text', false, false, true),
|
|
(5, 'proposed_by_agent_id', 'uuid', false, false, false),
|
|
(6, 'channel', 'text', true, true, true),
|
|
(7, 'source_ref', 'text', false, false, true),
|
|
(8, 'rationale', 'text', true, false, true),
|
|
(9, 'payload', 'jsonb', true, false, false),
|
|
(10, 'reviewed_by_handle', 'text', false, false, true),
|
|
(11, 'reviewed_by_agent_id', 'uuid', false, false, false),
|
|
(12, 'reviewed_at', 'timestamptz', false, false, false),
|
|
(13, 'review_note', 'text', false, false, true),
|
|
(14, 'applied_by_handle', 'text', false, false, true),
|
|
(15, 'applied_by_agent_id', 'uuid', false, false, false),
|
|
(16, 'applied_at', 'timestamptz', false, false, false),
|
|
(17, 'created_at', 'timestamptz', true, true, false),
|
|
(18, 'updated_at', 'timestamptz', true, true, false)
|
|
), actual as (
|
|
select attribute.attnum::int as ordinal,
|
|
attribute.attname,
|
|
type_row.typname,
|
|
attribute.attnotnull as not_null,
|
|
attribute.atthasdef as has_default,
|
|
attribute.attidentity,
|
|
attribute.attgenerated,
|
|
attribute.atttypmod,
|
|
type_namespace.nspname as type_namespace,
|
|
attribute.attcollation = 'pg_catalog.default'::pg_catalog.regcollation as collated
|
|
from pg_catalog.pg_attribute attribute
|
|
join pg_catalog.pg_type type_row on type_row.oid = attribute.atttypid
|
|
join pg_catalog.pg_namespace type_namespace on type_namespace.oid = type_row.typnamespace
|
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
)
|
|
select pg_catalog.string_agg(coalesce(expected.attname, actual.attname), ', ' order by coalesce(expected.ordinal, actual.ordinal))
|
|
into unexpected
|
|
from expected
|
|
full join actual using (ordinal, attname)
|
|
where expected.ordinal is null
|
|
or actual.ordinal is null
|
|
or expected.typname is distinct from actual.typname
|
|
or expected.not_null is distinct from actual.not_null
|
|
or expected.has_default is distinct from actual.has_default
|
|
or expected.collated is distinct from actual.collated
|
|
or actual.attidentity <> ''
|
|
or actual.attgenerated <> ''
|
|
or actual.atttypmod <> -1
|
|
or actual.type_namespace <> 'pg_catalog';
|
|
if unexpected is not null then
|
|
raise exception 'kb_stage.kb_proposals column contract drifted: %', unexpected;
|
|
end if;
|
|
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.pg_trigger trigger_row
|
|
where trigger_row.tgrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and not trigger_row.tgisinternal
|
|
) or exists (
|
|
select 1
|
|
from pg_catalog.pg_rewrite rewrite_row
|
|
where rewrite_row.ev_class = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
) or exists (
|
|
select 1
|
|
from pg_catalog.pg_policy policy_row
|
|
where policy_row.polrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
) or exists (
|
|
select 1
|
|
from pg_catalog.pg_attribute attribute
|
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and attribute.attgenerated <> ''
|
|
) then
|
|
raise exception 'kb_stage.kb_proposals has unsafe trigger/rule/RLS/generated topology';
|
|
end if;
|
|
|
|
with expected(attname, expression) as (
|
|
values
|
|
('id', 'gen_random_uuid()'),
|
|
('status', '''pending_review''::text'),
|
|
('channel', '''cli''::text'),
|
|
('created_at', 'now()'),
|
|
('updated_at', 'now()')
|
|
), actual as (
|
|
select attribute.attname,
|
|
pg_catalog.pg_get_expr(default_row.adbin, default_row.adrelid) as expression
|
|
from pg_catalog.pg_attribute attribute
|
|
join pg_catalog.pg_attrdef default_row
|
|
on default_row.adrelid = attribute.attrelid
|
|
and default_row.adnum = attribute.attnum
|
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
)
|
|
select pg_catalog.string_agg(coalesce(expected.attname, actual.attname), ', ')
|
|
into unexpected
|
|
from expected
|
|
full join actual using (attname)
|
|
where expected.expression is distinct from actual.expression;
|
|
if unexpected is not null then
|
|
raise exception 'kb_stage.kb_proposals default topology drifted: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%s:%s', dependency.refclassid::pg_catalog.regclass, dependency.refobjid),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_attrdef default_row
|
|
join pg_catalog.pg_depend dependency
|
|
on dependency.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass
|
|
and dependency.objid = default_row.oid
|
|
where default_row.adrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and (
|
|
dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
|
and dependency.refobjid not in (
|
|
'pg_catalog.gen_random_uuid()'::pg_catalog.regprocedure,
|
|
'pg_catalog.now()'::pg_catalog.regprocedure
|
|
)
|
|
or dependency.refclassid = 'pg_catalog.pg_type'::pg_catalog.regclass
|
|
and not exists (
|
|
select 1
|
|
from pg_catalog.pg_type type_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = type_row.typnamespace
|
|
where type_row.oid = dependency.refobjid
|
|
and namespace.nspname = 'pg_catalog'
|
|
)
|
|
or dependency.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass
|
|
and not exists (
|
|
select 1
|
|
from pg_catalog.pg_operator operator_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = operator_row.oprnamespace
|
|
where operator_row.oid = dependency.refobjid
|
|
and namespace.nspname = 'pg_catalog'
|
|
)
|
|
or dependency.refclassid = 'pg_catalog.pg_collation'::pg_catalog.regclass
|
|
and not exists (
|
|
select 1
|
|
from pg_catalog.pg_collation collation_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = collation_row.collnamespace
|
|
where collation_row.oid = dependency.refobjid
|
|
and namespace.nspname = 'pg_catalog'
|
|
)
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'kb_stage.kb_proposals defaults depend on unreviewed objects: %', unexpected;
|
|
end if;
|
|
|
|
if (
|
|
select pg_catalog.count(*) <> 6
|
|
or pg_catalog.count(*) filter (
|
|
where (constraint_row.conname, constraint_row.contype) in (
|
|
('kb_proposals_pkey', 'p'),
|
|
('kb_proposals_proposal_type_check', 'c'),
|
|
('kb_proposals_status_check', 'c'),
|
|
('kb_proposals_applied_by_agent_id_fkey', 'f'),
|
|
('kb_proposals_proposed_by_agent_id_fkey', 'f'),
|
|
('kb_proposals_reviewed_by_agent_id_fkey', 'f')
|
|
)
|
|
) <> 6
|
|
from pg_catalog.pg_constraint constraint_row
|
|
where constraint_row.conrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
) or exists (
|
|
select 1
|
|
from pg_catalog.pg_constraint constraint_row
|
|
join pg_catalog.pg_depend dependency
|
|
on dependency.classid = 'pg_catalog.pg_constraint'::pg_catalog.regclass
|
|
and dependency.objid = constraint_row.oid
|
|
and dependency.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
|
join pg_catalog.pg_proc function_row on function_row.oid = dependency.refobjid
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where constraint_row.conrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and namespace.nspname <> 'pg_catalog'
|
|
) or exists (
|
|
select 1
|
|
from pg_catalog.pg_index index_row
|
|
where index_row.indrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and (
|
|
index_row.indexprs is not null
|
|
or (
|
|
index_row.indpred is not null
|
|
and pg_catalog.pg_get_expr(index_row.indpred, index_row.indrelid)
|
|
<> '(proposed_by_handle IS NOT NULL)'
|
|
)
|
|
)
|
|
) then
|
|
raise exception 'kb_stage.kb_proposals constraint/index topology drifted';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I:%s:%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
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(
|
|
database_row.datacl,
|
|
pg_catalog.acldefault('d', database_row.datdba)
|
|
)
|
|
) as acl
|
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
|
where grantee.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
and not (
|
|
database_row.datname = 'teleo_canonical'
|
|
and grantee.rolname = 'leoclean_kb_runtime'
|
|
and acl.privilege_type = 'CONNECT'
|
|
and not acl.is_grantable
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles retain unexpected direct database ACLs: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
|
into unexpected
|
|
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;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
|
into unexpected
|
|
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
|
|
)
|
|
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;
|
|
|
|
select pg_catalog.string_agg(database_row.datname, ', ' order by database_row.datname)
|
|
into unexpected
|
|
from pg_catalog.pg_database database_row
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(database_row.datacl, pg_catalog.acldefault('d', database_row.datdba))
|
|
) acl
|
|
where acl.grantee = 0
|
|
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 unexpected database CONNECT: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%s:%s', setting_row.setdatabase, setting_row.setconfig),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_db_role_setting setting_row
|
|
where setting_row.setrole in (runtime_oid, owner_oid)
|
|
and not (
|
|
setting_row.setrole = runtime_oid
|
|
and setting_row.setdatabase = (
|
|
select database_row.oid
|
|
from pg_catalog.pg_database database_row
|
|
where database_row.datname = 'teleo_canonical'
|
|
)
|
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
|
and setting_row.setconfig @> array[
|
|
'search_path=pg_catalog, public, kb_stage',
|
|
'statement_timeout=15s',
|
|
'lock_timeout=2s'
|
|
]::text[]
|
|
);
|
|
if unexpected is not null or not exists (
|
|
select 1
|
|
from pg_catalog.pg_db_role_setting setting_row
|
|
where setting_row.setrole = runtime_oid
|
|
and setting_row.setdatabase = (
|
|
select database_row.oid
|
|
from pg_catalog.pg_database database_row
|
|
where database_row.datname = 'teleo_canonical'
|
|
)
|
|
and pg_catalog.cardinality(setting_row.setconfig) = 3
|
|
and setting_row.setconfig @> array[
|
|
'search_path=pg_catalog, public, kb_stage',
|
|
'statement_timeout=15s',
|
|
'lock_timeout=2s'
|
|
]::text[]
|
|
) then
|
|
raise exception 'scoped Leo role settings differ from the reviewed contract';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I:%s', database_row.datname, scoped_role.name),
|
|
', '
|
|
order by database_row.datname, scoped_role.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_database database_row
|
|
cross join (values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')) scoped_role(name)
|
|
where has_database_privilege(scoped_role.name, database_row.oid, 'CREATE');
|
|
if unexpected is not null then
|
|
raise exception 'a scoped Leo role unexpectedly has database CREATE: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%s:%s:%s', dependency.classid::pg_catalog.regclass, dependency.objid, dependency.objsubid),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_shdepend dependency
|
|
where dependency.refclassid = 'pg_catalog.pg_authid'::pg_catalog.regclass
|
|
and dependency.refobjid = runtime_oid
|
|
and dependency.deptype = 'o';
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime unexpectedly owns database objects: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%s:%s:%s', dependency.classid::pg_catalog.regclass, dependency.objid, dependency.objsubid),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_shdepend dependency
|
|
where dependency.refclassid = 'pg_catalog.pg_authid'::pg_catalog.regclass
|
|
and dependency.refobjid = owner_oid
|
|
and dependency.deptype = 'o'
|
|
and not (
|
|
dependency.dbid = (select database_row.oid from pg_catalog.pg_database database_row where database_row.datname = current_database())
|
|
and
|
|
dependency.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass
|
|
and dependency.objid = stage_function
|
|
and dependency.objsubid = 0
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_stage_owner owns unexpected database objects: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(metadata.oid::text, ', ' order by metadata.oid)
|
|
into unexpected
|
|
from pg_catalog.pg_largeobject_metadata metadata
|
|
where metadata.lomowner in (runtime_oid, owner_oid)
|
|
or exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(metadata.lomacl, pg_catalog.acldefault('L', metadata.lomowner))
|
|
) acl
|
|
where acl.grantee in (0, runtime_oid, owner_oid)
|
|
and acl.privilege_type in ('SELECT', 'UPDATE')
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles retain large-object ownership or ACL reachability: %', unexpected;
|
|
end if;
|
|
|
|
with actual as (
|
|
select function_row.oid,
|
|
pg_catalog.format(
|
|
'%I(%s)',
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid)
|
|
) as signature,
|
|
exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(function_row.proacl, pg_catalog.acldefault('f', function_row.proowner))
|
|
) acl
|
|
where acl.grantee = 0
|
|
and acl.privilege_type = 'EXECUTE'
|
|
) as public_execute,
|
|
pg_catalog.has_function_privilege('leoclean_kb_runtime', function_row.oid, 'EXECUTE') as runtime_execute,
|
|
pg_catalog.has_function_privilege('leoclean_kb_stage_owner', function_row.oid, 'EXECUTE') as owner_execute
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where namespace.nspname = 'pg_catalog'
|
|
and function_row.proname in (
|
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
|
)
|
|
)
|
|
select pg_catalog.string_agg(
|
|
coalesce(expected.signature, actual.signature),
|
|
', '
|
|
order by coalesce(expected.signature, actual.signature)
|
|
)
|
|
into unexpected
|
|
from pg_temp.leoclean_expected_lo_routine_residual expected
|
|
full join actual using (signature)
|
|
where expected.signature is null
|
|
or actual.signature is null
|
|
or actual.public_execute is distinct from expected.public_execute
|
|
or actual.runtime_execute is distinct from expected.public_execute
|
|
or actual.owner_execute is distinct from expected.public_execute;
|
|
if unexpected is not null then
|
|
raise exception 'provider-owned large-object routine residual drifted: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I(%s):%s',
|
|
function_row.proname,
|
|
pg_catalog.pg_get_function_identity_arguments(function_row.oid),
|
|
role_row.rolname
|
|
),
|
|
', '
|
|
order by function_row.proname, role_row.rolname
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(function_row.proacl) acl
|
|
join pg_catalog.pg_roles role_row on role_row.oid = acl.grantee
|
|
where namespace.nspname = 'pg_catalog'
|
|
and function_row.proname in (
|
|
'lo_creat', 'lo_create', 'lo_export', 'lo_from_bytea', 'lo_import',
|
|
'lo_open', 'lo_put', 'lo_truncate', 'lo_truncate64', 'lo_unlink', 'lowrite'
|
|
)
|
|
and role_row.rolname in ('leoclean_kb_runtime', 'leoclean_kb_stage_owner')
|
|
and acl.privilege_type = 'EXECUTE';
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles have direct large-object routine ACLs: %', unexpected;
|
|
end if;
|
|
|
|
if pg_catalog.current_setting('lo_compat_privileges') <> 'off' then
|
|
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
|
|
where has_parameter_privilege('leoclean_kb_runtime', parameter_acl.parname, 'SET')
|
|
or has_parameter_privilege('leoclean_kb_runtime', parameter_acl.parname, 'ALTER SYSTEM')
|
|
or has_parameter_privilege('leoclean_kb_stage_owner', parameter_acl.parname, 'SET')
|
|
or has_parameter_privilege('leoclean_kb_stage_owner', parameter_acl.parname, 'ALTER SYSTEM');
|
|
if unexpected is not null then
|
|
raise exception 'scoped Leo roles retain parameter privileges: %', unexpected;
|
|
end if;
|
|
|
|
if has_schema_privilege('leoclean_kb_runtime', 'public', 'CREATE')
|
|
or has_schema_privilege('leoclean_kb_runtime', 'kb_stage', 'CREATE')
|
|
or has_schema_privilege('leoclean_kb_stage_owner', 'public', 'CREATE')
|
|
or has_schema_privilege('leoclean_kb_stage_owner', 'kb_stage', 'CREATE') then
|
|
raise exception 'a scoped Leo role unexpectedly has schema CREATE';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
', '
|
|
order by namespace.nspname, relation.relname, privilege.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join (values ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) privilege(name)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and has_table_privilege('leoclean_kb_runtime', relation.oid, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime unexpectedly has table-level DML: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
|
', '
|
|
order by namespace.nspname, relation.relname, attribute.attnum, privilege.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
cross join (values ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege('leoclean_kb_runtime', relation.oid, attribute.attnum, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime unexpectedly has column-level DML: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I', namespace.nspname, relation.relname),
|
|
', '
|
|
order by namespace.nspname, relation.relname
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and has_table_privilege('leoclean_kb_runtime', relation.oid, 'SELECT');
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime unexpectedly has table-level SELECT: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I', allowlist.schema_name, allowlist.relation_name, allowlist.column_name),
|
|
', '
|
|
order by allowlist.schema_name, allowlist.relation_name, allowlist.column_ordinal
|
|
)
|
|
into unexpected
|
|
from pg_temp.leoclean_runtime_read_column_allowlist allowlist
|
|
left join pg_catalog.pg_namespace namespace
|
|
on namespace.nspname = allowlist.schema_name
|
|
left join pg_catalog.pg_class relation
|
|
on relation.relnamespace = namespace.oid
|
|
and relation.relname = allowlist.relation_name
|
|
left join pg_catalog.pg_attribute attribute
|
|
on attribute.attrelid = relation.oid
|
|
and attribute.attname = allowlist.column_name
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
where attribute.attnum is null
|
|
or not coalesce(
|
|
has_column_privilege('leoclean_kb_runtime', relation.oid, attribute.attnum, 'SELECT'),
|
|
false
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime is missing required column SELECT: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
|
', '
|
|
order by namespace.nspname, relation.relname, attribute.attnum
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
left join pg_temp.leoclean_runtime_read_column_allowlist allowlist
|
|
on allowlist.schema_name = namespace.nspname
|
|
and allowlist.relation_name = relation.relname
|
|
and allowlist.column_name = attribute.attname
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege('leoclean_kb_runtime', relation.oid, attribute.attnum, 'SELECT')
|
|
and allowlist.column_name is null;
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime has column SELECT outside its allowlist: %', unexpected;
|
|
end if;
|
|
|
|
if has_table_privilege('leoclean_kb_stage_owner', 'public.agents', 'SELECT')
|
|
or not has_column_privilege('leoclean_kb_stage_owner', 'public.agents', 'id', 'SELECT')
|
|
or not has_column_privilege('leoclean_kb_stage_owner', 'public.agents', 'handle', 'SELECT')
|
|
or not has_table_privilege('leoclean_kb_stage_owner', 'kb_stage.kb_proposals', 'SELECT')
|
|
or has_table_privilege('leoclean_kb_stage_owner', 'kb_stage.kb_proposals', 'INSERT') then
|
|
raise exception 'leoclean_kb_stage_owner does not have the exact read/insert grant shape';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
|
', '
|
|
order by namespace.nspname, relation.relname, attribute.attnum
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, 'INSERT')
|
|
and not (
|
|
namespace.nspname = 'kb_stage'
|
|
and relation.relname = 'kb_proposals'
|
|
and attribute.attname in (
|
|
'proposal_type',
|
|
'status',
|
|
'proposed_by_handle',
|
|
'proposed_by_agent_id',
|
|
'channel',
|
|
'source_ref',
|
|
'rationale',
|
|
'payload'
|
|
)
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_stage_owner can INSERT unexpected columns: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
', '
|
|
order by namespace.nspname, relation.relname, privilege.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join (values ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) privilege(name)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and has_table_privilege('leoclean_kb_stage_owner', relation.oid, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_stage_owner unexpectedly has table-level DML: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I:%s', namespace.nspname, relation.relname, attribute.attname, privilege.name),
|
|
', '
|
|
order by namespace.nspname, relation.relname, attribute.attnum, privilege.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
cross join (values ('UPDATE'), ('REFERENCES')) privilege(name)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_stage_owner unexpectedly has column-level DML: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I.%I', namespace.nspname, relation.relname, attribute.attname),
|
|
', '
|
|
order by namespace.nspname, relation.relname, attribute.attnum
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege('leoclean_kb_stage_owner', relation.oid, attribute.attnum, 'SELECT')
|
|
and not (
|
|
(namespace.nspname = 'kb_stage' and relation.relname = 'kb_proposals')
|
|
or (
|
|
namespace.nspname = 'public'
|
|
and relation.relname = 'agents'
|
|
and attribute.attname in ('id', 'handle')
|
|
)
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_stage_owner can SELECT unexpected columns: %', unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s:%s', namespace.nspname, relation.relname, scoped_role.name, privilege.name),
|
|
', '
|
|
order by namespace.nspname, relation.relname, scoped_role.name, privilege.name
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join (values ('leoclean_kb_runtime'), ('leoclean_kb_stage_owner')) scoped_role(name)
|
|
cross join (values ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and relation.relkind = 'S'
|
|
and has_sequence_privilege(scoped_role.name, relation.oid, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception 'a scoped Leo role unexpectedly has sequence privileges: %', unexpected;
|
|
end if;
|
|
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.pg_attribute attribute
|
|
where attribute.attrelid = 'kb_stage.kb_proposals'::pg_catalog.regclass
|
|
and attribute.attname in (
|
|
'proposal_type',
|
|
'status',
|
|
'proposed_by_handle',
|
|
'proposed_by_agent_id',
|
|
'channel',
|
|
'source_ref',
|
|
'rationale',
|
|
'payload'
|
|
)
|
|
and not has_column_privilege(
|
|
'leoclean_kb_stage_owner',
|
|
attribute.attrelid,
|
|
attribute.attnum,
|
|
'INSERT'
|
|
)
|
|
) then
|
|
raise exception 'leoclean_kb_stage_owner is missing a required INSERT column grant';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I(%s)', namespace.nspname, function_row.proname, pg_catalog.pg_get_function_identity_arguments(function_row.oid)),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_proc function_row
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = function_row.pronamespace
|
|
where namespace.nspname in ('public', 'kb_stage')
|
|
and function_row.prosecdef
|
|
and function_row.oid <> stage_function
|
|
and has_function_privilege('leoclean_kb_runtime', function_row.oid, 'EXECUTE');
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime can execute unexpected SECURITY DEFINER functions: %', unexpected;
|
|
end if;
|
|
|
|
if not exists (
|
|
select 1
|
|
from pg_catalog.pg_proc function_row
|
|
where function_row.oid = stage_function
|
|
and function_row.prosecdef
|
|
and function_row.proowner = owner_oid
|
|
and function_row.proconfig = array['search_path=pg_catalog, pg_temp']::text[]
|
|
) then
|
|
raise exception 'stage_leoclean_proposal owner/security/search_path contract is invalid';
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'grantee=%s privilege=%s grantable=%s',
|
|
acl.grantee,
|
|
acl.privilege_type,
|
|
acl.is_grantable
|
|
),
|
|
', '
|
|
)
|
|
into unexpected
|
|
from pg_catalog.pg_proc function_row
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(
|
|
function_row.proacl,
|
|
pg_catalog.acldefault('f', function_row.proowner)
|
|
)
|
|
) as acl
|
|
where function_row.oid = stage_function
|
|
and not (
|
|
acl.grantee = runtime_oid
|
|
and acl.privilege_type = 'EXECUTE'
|
|
and not acl.is_grantable
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'stage_leoclean_proposal has unexpected ACL entries: %', unexpected;
|
|
end if;
|
|
|
|
if not has_function_privilege(
|
|
'leoclean_kb_runtime',
|
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
'EXECUTE'
|
|
) then
|
|
raise exception 'leoclean_kb_runtime cannot execute stage_leoclean_proposal';
|
|
end if;
|
|
|
|
if has_function_privilege(
|
|
'leoclean_kb_stage_owner',
|
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
'EXECUTE'
|
|
) then
|
|
raise exception 'leoclean_kb_stage_owner must not execute stage_leoclean_proposal';
|
|
end if;
|
|
end
|
|
$verification$;
|
|
|
|
-- LOGIN is the last state change in the same transaction as every database ACL,
|
|
-- canonical privilege, large-object ownership/residual, and topology
|
|
-- assertion. Any failure rolls back the complete privilege migration and
|
|
-- leaves the role NOLOGIN.
|
|
alter role leoclean_kb_runtime
|
|
with login nocreatedb nocreaterole noinherit connection limit 8;
|
|
|
|
do $login_verification$
|
|
begin
|
|
if exists (
|
|
select 1
|
|
from pg_catalog.pg_roles role_row
|
|
where role_row.rolname = 'leoclean_kb_runtime'
|
|
and (
|
|
not role_row.rolcanlogin
|
|
or role_row.rolsuper
|
|
or role_row.rolcreatedb
|
|
or role_row.rolcreaterole
|
|
or role_row.rolinherit
|
|
or role_row.rolreplication
|
|
or role_row.rolbypassrls
|
|
or role_row.rolconnlimit <> 8
|
|
)
|
|
) then
|
|
raise exception 'leoclean_kb_runtime final login attributes are unsafe';
|
|
end if;
|
|
end
|
|
$login_verification$;
|
|
|
|
commit;
|