847 lines
34 KiB
PL/PgSQL
847 lines
34 KiB
PL/PgSQL
\set ON_ERROR_STOP on
|
|
\getenv runtime_password TELEO_LEOCLEAN_DB_PASSWORD
|
|
|
|
-- One-time GCP staging provisioning for Leo's Cloud SQL identity. The password
|
|
-- is supplied only through the process environment and is never stored here.
|
|
--
|
|
-- 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.
|
|
select 'create role leoclean_kb_runtime login 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
|
|
|
|
select format('alter role leoclean_kb_runtime password %L', :'runtime_password')
|
|
\gexec
|
|
|
|
alter role leoclean_kb_runtime
|
|
with login nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit 8;
|
|
alter role leoclean_kb_stage_owner
|
|
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
|
|
password null;
|
|
|
|
alter role leoclean_kb_runtime reset all;
|
|
alter role leoclean_kb_stage_owner reset all;
|
|
|
|
-- NOINHERIT does not prevent SET ROLE. Remove every membership edge in either
|
|
-- direction so neither scoped principal can reach, or be reached through, a
|
|
-- broader role left behind by an earlier deployment.
|
|
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
|
|
|
|
revoke all privileges on database teleo_canonical from leoclean_kb_runtime;
|
|
revoke all privileges on database teleo_canonical from leoclean_kb_stage_owner;
|
|
revoke all privileges on database teleo_kb from leoclean_kb_runtime;
|
|
revoke all privileges on database teleo_kb from leoclean_kb_stage_owner;
|
|
grant connect on database teleo_canonical to leoclean_kb_runtime;
|
|
|
|
-- 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;
|
|
|
|
-- 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;
|
|
|
|
-- 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;
|
|
|
|
grant select on
|
|
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
|
|
to leoclean_kb_runtime;
|
|
|
|
-- 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;
|
|
|
|
-- Remove the prior caller-supplied-identity overload before installing the
|
|
-- session-bound form. A runtime caller can only stage as canonical agent Leo.
|
|
drop function if exists kb_stage.stage_leoclean_proposal(text, text, text, text, text, jsonb);
|
|
|
|
create or replace 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$;
|
|
|
|
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;
|
|
|
|
-- Normalize explicit ACL entries left by CREATE OR REPLACE or an earlier grant.
|
|
revoke all on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
|
from public;
|
|
select format(
|
|
'revoke all on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb) from %I',
|
|
grantee.rolname
|
|
)
|
|
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
|
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
|
where function_row.oid = 'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)'::pg_catalog.regprocedure
|
|
and grantee.rolname <> 'leoclean_kb_stage_owner'
|
|
\gexec
|
|
|
|
grant execute on function kb_stage.stage_leoclean_proposal(text, text, text, text, jsonb)
|
|
to leoclean_kb_runtime;
|
|
|
|
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 (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 has unsafe 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;
|
|
|
|
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 database_row.datname in ('teleo_canonical', 'teleo_kb')
|
|
and 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;
|
|
|
|
if has_database_privilege('leoclean_kb_runtime', 'teleo_canonical', 'CREATE')
|
|
or has_database_privilege('leoclean_kb_runtime', 'teleo_kb', 'CREATE')
|
|
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_canonical', 'CREATE')
|
|
or has_database_privilege('leoclean_kb_stage_owner', 'teleo_kb', 'CREATE') then
|
|
raise exception 'a scoped Leo role unexpectedly has database CREATE';
|
|
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;
|
|
|
|
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')
|
|
and (namespace.nspname, relation.relname) not 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')
|
|
);
|
|
if unexpected is not null then
|
|
raise exception 'leoclean_kb_runtime can SELECT outside its table allowlist: %', 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_runtime', relation.oid, attribute.attnum, 'SELECT')
|
|
and (namespace.nspname, relation.relname) not 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')
|
|
);
|
|
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 in (runtime_oid, owner_oid)
|
|
and acl.privilege_type = 'EXECUTE'
|
|
and (acl.grantee = owner_oid or 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;
|
|
end
|
|
$verification$;
|
|
|
|
commit;
|
|
|
|
-- The legacy restore database is not an audit fallback for the scoped runtime.
|
|
-- Strip direct ACLs from both scoped roles and fail if PUBLIC/group grants still
|
|
-- make any teleo_restore relation reachable.
|
|
\connect teleo_kb
|
|
|
|
begin;
|
|
|
|
select format('revoke all on schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
from pg_catalog.pg_namespace namespace
|
|
where namespace.nspname = 'teleo_restore'
|
|
\gexec
|
|
|
|
select format('revoke all privileges on all tables in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
from pg_catalog.pg_namespace namespace
|
|
where namespace.nspname = 'teleo_restore'
|
|
\gexec
|
|
|
|
select format('revoke all privileges on all sequences in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
from pg_catalog.pg_namespace namespace
|
|
where namespace.nspname = 'teleo_restore'
|
|
\gexec
|
|
|
|
select format('revoke all privileges on all functions in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
from pg_catalog.pg_namespace namespace
|
|
where namespace.nspname = 'teleo_restore'
|
|
\gexec
|
|
|
|
select format('revoke all privileges on all procedures in schema %I from leoclean_kb_runtime, leoclean_kb_stage_owner', namespace.nspname)
|
|
from pg_catalog.pg_namespace namespace
|
|
where namespace.nspname = 'teleo_restore'
|
|
\gexec
|
|
|
|
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 = 'teleo_restore'
|
|
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
|
|
|
|
do $audit_verification$
|
|
declare
|
|
unexpected text;
|
|
scoped_role text;
|
|
begin
|
|
foreach scoped_role in array array['leoclean_kb_runtime', 'leoclean_kb_stage_owner'] loop
|
|
if to_regnamespace('teleo_restore') is not null
|
|
and (has_schema_privilege(scoped_role, 'teleo_restore', 'USAGE')
|
|
or has_schema_privilege(scoped_role, 'teleo_restore', 'CREATE')) then
|
|
raise exception '% unexpectedly has teleo_restore schema access', scoped_role;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
', '
|
|
order by 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 ('SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')
|
|
) privilege(name)
|
|
where namespace.nspname = 'teleo_restore'
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and has_table_privilege(scoped_role, relation.oid, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception '% unexpectedly has teleo_restore table access: %', scoped_role, 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 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 ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) privilege(name)
|
|
where namespace.nspname = 'teleo_restore'
|
|
and relation.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and has_column_privilege(scoped_role, relation.oid, attribute.attnum, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception '% unexpectedly has teleo_restore column access: %', scoped_role, unexpected;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I:%s', namespace.nspname, relation.relname, privilege.name),
|
|
', '
|
|
order by 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 ('USAGE'), ('SELECT'), ('UPDATE')) privilege(name)
|
|
where namespace.nspname = 'teleo_restore'
|
|
and relation.relkind = 'S'
|
|
and has_sequence_privilege(scoped_role, relation.oid, privilege.name);
|
|
if unexpected is not null then
|
|
raise exception '% unexpectedly has teleo_restore sequence access: %', scoped_role, unexpected;
|
|
end if;
|
|
end loop;
|
|
end
|
|
$audit_verification$;
|
|
|
|
commit;
|
|
|
|
\connect teleo_canonical
|
|
|
|
select jsonb_build_object(
|
|
'role', rolname,
|
|
'superuser', rolsuper,
|
|
'create_db', rolcreatedb,
|
|
'create_role', rolcreaterole,
|
|
'inherit', rolinherit,
|
|
'replication', rolreplication,
|
|
'bypass_rls', rolbypassrls,
|
|
'has_role_memberships', exists (
|
|
select 1
|
|
from pg_catalog.pg_auth_members membership
|
|
where membership.roleid = pg_catalog.pg_roles.oid
|
|
or membership.member = pg_catalog.pg_roles.oid
|
|
),
|
|
'can_select_claims', has_table_privilege(rolname, 'public.claims', 'SELECT'),
|
|
'can_select_proposals', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'SELECT'),
|
|
'can_insert_proposals_directly', has_table_privilege(rolname, 'kb_stage.kb_proposals', 'INSERT'),
|
|
'can_create_database_objects', has_database_privilege(rolname, 'teleo_canonical', 'CREATE'),
|
|
'can_create_temp_objects', has_database_privilege(rolname, 'teleo_canonical', 'TEMP'),
|
|
'can_stage_proposal', has_function_privilege(
|
|
rolname,
|
|
'kb_stage.stage_leoclean_proposal(text,text,text,text,jsonb)',
|
|
'EXECUTE'
|
|
)
|
|
)::text
|
|
from pg_catalog.pg_roles
|
|
where rolname = 'leoclean_kb_runtime';
|