- Normalize rich proposals into atomic reviewed canonical graph bundles with exact row verification. - Harden reviewer/apply roles, upgrade ACLs, and prove generic plus Helmer lifecycles in isolated PostgreSQL. - Publish repo-native VPS/GCP/Cory skills and live-readonly GCP inventory without changing the live VPS runtime. `.agents/skills/crabbox/SKILL.md` `.agents/skills/teleo-gcp-parity-ops/SKILL.md` `.agents/skills/teleo-kb-db-change-workflow/SKILL.md` `.agents/skills/teleo-leo-onboarding/SKILL.md` `.agents/skills/teleo-vps-runtime-ops/SKILL.md` `.agents/skills/working-leo-cory-outcomes/SKILL.md` `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json` `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md` `docs/reports/leo-working-state-20260709/current-truth-index.md` `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json` `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md` `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json` `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md` `docs/reports/leo-working-state-20260709/gcp-parallel-delegation-current.json` `docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json` `docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json` `docs/reports/leo-working-state-20260709/skill-pack-manifest.json` `docs/reports/leo-working-state-20260709/skill-pack-readme.md` `docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md` `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md` `docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md` `scripts/apply_proposal.py` `scripts/apply_worker.py` `scripts/approve_proposal.py` `scripts/kb_apply_prereqs.sql` `scripts/kb_proposal_normalize.py` `scripts/probe_gcp_db_parity.py` `scripts/run_approve_claim_clone_canary.py` `scripts/run_approve_claim_isolated_container_canary.sh` `tests/test_apply_proposal.py` `tests/test_apply_worker.py` `tests/test_approve_proposal.py` `tests/test_kb_apply_prereqs.py` `tests/test_kb_proposal_normalize.py` `tests/test_kb_proposal_routes.py` `tests/test_probe_gcp_db_parity.py` `tests/test_repo_skill_pack.py`
1073 lines
42 KiB
PL/PgSQL
1073 lines
42 KiB
PL/PgSQL
-- Guarded KB review/apply prerequisites. Run as postgres at deploy time.
|
|
--
|
|
-- Authority is deliberately split:
|
|
-- * kb_review can read proposals and execute one constrained approval function;
|
|
-- * kb_apply can read approved proposals, perform narrow canonical writes, and
|
|
-- execute payload-bound assert/finish functions;
|
|
-- * kb_gate_owner is a NOLOGIN owner for gate tables/functions;
|
|
-- * neither login role can directly UPDATE kb_stage.kb_proposals or mutate the
|
|
-- immutable approval snapshot.
|
|
--
|
|
-- The deployer provisions passwords separately in kb-review.env/kb-apply.env.
|
|
-- This file contains no credentials and is safe to reuse in disposable clones.
|
|
|
|
begin;
|
|
|
|
do $deployer$
|
|
begin
|
|
if not exists (
|
|
select 1
|
|
from pg_catalog.pg_roles
|
|
where rolname = current_user
|
|
and rolsuper
|
|
) then
|
|
raise exception 'kb_apply_prereqs: current role % must be a superuser', current_user;
|
|
end if;
|
|
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_apply') then
|
|
raise exception 'kb_apply_prereqs: required runtime role kb_apply does not exist';
|
|
end if;
|
|
end
|
|
$deployer$;
|
|
|
|
do $roles$
|
|
begin
|
|
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_gate_owner') then
|
|
create role kb_gate_owner nologin;
|
|
end if;
|
|
if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_review') then
|
|
create role kb_review login;
|
|
end if;
|
|
end
|
|
$roles$;
|
|
|
|
alter role kb_gate_owner nologin noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;
|
|
alter role kb_review login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;
|
|
alter role kb_apply login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls;
|
|
|
|
-- Protected roles are standalone principals. Any membership involving one of
|
|
-- them creates an inherited or SET ROLE path that this migration cannot safely
|
|
-- classify, so fail instead of silently revoking an operator-managed grant.
|
|
do $memberships$
|
|
declare
|
|
v_memberships text;
|
|
begin
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname),
|
|
', ' order by member_role.rolname, granted_role.rolname
|
|
)
|
|
into v_memberships
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply');
|
|
|
|
if v_memberships is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected role membership: %', v_memberships;
|
|
end if;
|
|
end
|
|
$memberships$;
|
|
|
|
-- Service identity used for the applied_by_agent_id foreign key. kb_apply can
|
|
-- read this row but can never create or modify agent rows.
|
|
insert into public.agents (id, handle, kind)
|
|
values ('44444444-4444-4444-4444-444444444444', 'kb-apply', 'service')
|
|
on conflict (handle) do nothing;
|
|
|
|
-- DB-login to human-reviewer mapping. Only the gate owner/admin can change it;
|
|
-- the approval function verifies --reviewed-by against this mapping rather than
|
|
-- trusting caller-supplied identity. The current dedicated reviewer credential
|
|
-- maps to the existing canonical m3ta human agent when that row is present.
|
|
create table if not exists kb_stage.kb_review_principals (
|
|
db_role name primary key,
|
|
reviewed_by_handle text not null,
|
|
reviewed_by_agent_id uuid not null references public.agents(id),
|
|
active boolean not null default true,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- One immutable snapshot per reviewed proposal. Apply is bound to this row as
|
|
-- well as the current proposal ledger, so an approved payload cannot drift.
|
|
create table if not exists kb_stage.kb_proposal_approvals (
|
|
proposal_id uuid primary key references kb_stage.kb_proposals(id),
|
|
proposal_type text not null,
|
|
payload jsonb not null,
|
|
reviewed_by_handle text not null,
|
|
reviewed_by_agent_id uuid not null references public.agents(id),
|
|
reviewed_by_db_role name not null,
|
|
reviewed_at timestamptz not null,
|
|
review_note text not null
|
|
);
|
|
|
|
-- Existing canonical/proposal tables must remain owned by the deployer. Gate
|
|
-- tables may be owned by the deployer during a legacy upgrade or kb_gate_owner
|
|
-- after an earlier guarded run. Anything else is ownership drift.
|
|
do $gate_preflight$
|
|
declare
|
|
v_drift text;
|
|
begin
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname),
|
|
', ' order by namespace.nspname, relation.relname
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'),
|
|
('public', 'strategies'),
|
|
('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'),
|
|
('public', 'claim_edges'),
|
|
('public', 'claims'),
|
|
('public', 'sources'),
|
|
('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'),
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and relation.relkind in ('r', 'p')
|
|
and not (
|
|
(namespace.nspname = 'kb_stage'
|
|
and relation.relname in ('kb_review_principals', 'kb_proposal_approvals')
|
|
and owner_role.rolname in (current_user, 'kb_gate_owner'))
|
|
or (not (
|
|
namespace.nspname = 'kb_stage'
|
|
and relation.relname in ('kb_review_principals', 'kb_proposal_approvals')
|
|
)
|
|
and owner_role.rolname = current_user)
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected table owner: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I(%s) owned by %I',
|
|
namespace.nspname,
|
|
procedure.proname,
|
|
pg_catalog.oidvectortypes(procedure.proargtypes),
|
|
owner_role.rolname
|
|
),
|
|
', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes)
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
and owner_role.rolname not in (current_user, 'kb_gate_owner');
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected function owner: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I(%s)',
|
|
namespace.nspname,
|
|
procedure.proname,
|
|
pg_catalog.oidvectortypes(procedure.proargtypes)
|
|
),
|
|
', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes)
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
and not (
|
|
(procedure.proname = 'approve_strict_proposal'
|
|
and pg_catalog.oidvectortypes(procedure.proargtypes) in (
|
|
'uuid, text, text',
|
|
'uuid, text, jsonb, text, text'
|
|
))
|
|
or (procedure.proname = 'assert_approved_proposal'
|
|
and pg_catalog.oidvectortypes(procedure.proargtypes) =
|
|
'uuid, text, jsonb, text, uuid, timestamp with time zone, text')
|
|
or (procedure.proname = 'finish_approved_proposal'
|
|
and pg_catalog.oidvectortypes(procedure.proargtypes) =
|
|
'uuid, text, jsonb, text, uuid, timestamp with time zone, text, text')
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected function overload: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I -> %s',
|
|
namespace.nspname,
|
|
relation.relname,
|
|
case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else pg_catalog.pg_get_userbyid(acl.grantee)
|
|
end
|
|
),
|
|
', ' order by namespace.nspname, relation.relname, acl.grantee
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
|
|
) acl
|
|
where (namespace.nspname, relation.relname) in (
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and acl.grantee <> 0
|
|
and acl.grantee <> relation.relowner
|
|
and pg_catalog.pg_get_userbyid(acl.grantee) not in (
|
|
'kb_gate_owner', 'kb_review', 'kb_apply'
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I.%I -> %s',
|
|
namespace.nspname,
|
|
relation.relname,
|
|
attribute.attname,
|
|
case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else pg_catalog.pg_get_userbyid(acl.grantee)
|
|
end
|
|
),
|
|
', ' order by namespace.nspname, relation.relname, attribute.attnum, acl.grantee
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_attribute attribute
|
|
join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
|
|
where (namespace.nspname, relation.relname) in (
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and not attribute.attisdropped
|
|
and acl.grantee <> 0
|
|
and acl.grantee <> relation.relowner
|
|
and pg_catalog.pg_get_userbyid(acl.grantee) not in (
|
|
'kb_gate_owner', 'kb_review', 'kb_apply'
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I(%s) -> %s',
|
|
namespace.nspname,
|
|
procedure.proname,
|
|
pg_catalog.oidvectortypes(procedure.proargtypes),
|
|
case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else pg_catalog.pg_get_userbyid(acl.grantee)
|
|
end
|
|
),
|
|
', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), acl.grantee
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
and acl.grantee <> 0
|
|
and acl.grantee <> procedure.proowner
|
|
and pg_catalog.pg_get_userbyid(acl.grantee) not in (
|
|
'kb_gate_owner', 'kb_review', 'kb_apply'
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: unexpected protected function grantee: %', v_drift;
|
|
end if;
|
|
end
|
|
$gate_preflight$;
|
|
|
|
-- This legacy three-argument SECURITY DEFINER path trusted caller-supplied
|
|
-- reviewer text. Its exact signature is the only obsolete overload auto-removed.
|
|
drop function if exists kb_stage.approve_strict_proposal(uuid, text, text);
|
|
|
|
alter table kb_stage.kb_review_principals owner to kb_gate_owner;
|
|
alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner;
|
|
|
|
insert into kb_stage.kb_review_principals
|
|
(db_role, reviewed_by_handle, reviewed_by_agent_id, active)
|
|
select 'kb_review'::name, a.handle, a.id, true
|
|
from public.agents a
|
|
where a.handle = 'm3ta'
|
|
on conflict (db_role) do nothing;
|
|
|
|
-- Reset every explicit privilege held by the protected roles before granting
|
|
-- the documented matrix. This removes stale DELETE/TRUNCATE/REFERENCES/TRIGGER
|
|
-- rights from upgrades instead of layering narrow grants on top of them.
|
|
revoke all privileges on schema public, kb_stage
|
|
from kb_gate_owner, kb_apply, kb_review;
|
|
grant usage on schema public, kb_stage to kb_gate_owner;
|
|
grant usage on schema public, kb_stage to kb_apply;
|
|
grant usage on schema public, kb_stage to kb_review;
|
|
|
|
revoke all privileges on table
|
|
public.agents,
|
|
public.strategies,
|
|
public.strategy_nodes,
|
|
public.claim_evidence,
|
|
public.claim_edges,
|
|
public.claims,
|
|
public.sources,
|
|
public.reasoning_tools,
|
|
kb_stage.kb_proposals,
|
|
kb_stage.kb_review_principals,
|
|
kb_stage.kb_proposal_approvals
|
|
from kb_gate_owner, kb_apply, kb_review;
|
|
revoke all privileges on table
|
|
kb_stage.kb_proposals,
|
|
kb_stage.kb_review_principals,
|
|
kb_stage.kb_proposal_approvals
|
|
from public;
|
|
|
|
-- Column grants are stored separately from table ACLs and survive a table-level
|
|
-- REVOKE. Clear them explicitly so stale per-column writes cannot bypass the
|
|
-- normalized table matrix.
|
|
do $column_acl_reset$
|
|
declare
|
|
protected_table record;
|
|
begin
|
|
for protected_table in
|
|
select namespace.nspname as schema_name,
|
|
relation.relname as table_name,
|
|
pg_catalog.string_agg(
|
|
pg_catalog.format('%I', attribute.attname),
|
|
', ' order by attribute.attnum
|
|
) as column_list
|
|
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, relation.relname) in (
|
|
('public', 'agents'),
|
|
('public', 'strategies'),
|
|
('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'),
|
|
('public', 'claim_edges'),
|
|
('public', 'claims'),
|
|
('public', 'sources'),
|
|
('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'),
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
group by namespace.nspname, relation.relname
|
|
loop
|
|
execute pg_catalog.format(
|
|
'revoke all privileges (%s) on table %I.%I from kb_gate_owner, kb_apply, kb_review',
|
|
protected_table.column_list,
|
|
protected_table.schema_name,
|
|
protected_table.table_name
|
|
);
|
|
if protected_table.schema_name = 'kb_stage'
|
|
and protected_table.table_name in (
|
|
'kb_proposals', 'kb_review_principals', 'kb_proposal_approvals'
|
|
) then
|
|
execute pg_catalog.format(
|
|
'revoke all privileges (%s) on table %I.%I from public',
|
|
protected_table.column_list,
|
|
protected_table.schema_name,
|
|
protected_table.table_name
|
|
);
|
|
end if;
|
|
end loop;
|
|
end
|
|
$column_acl_reset$;
|
|
|
|
grant select on public.agents to kb_gate_owner;
|
|
grant select on public.agents to kb_apply;
|
|
grant select on public.agents to kb_review;
|
|
|
|
grant select, insert, update on public.strategies to kb_apply;
|
|
grant select, insert, update on public.strategy_nodes to kb_apply;
|
|
grant select, insert on public.claim_evidence to kb_apply;
|
|
grant select, insert on public.claim_edges to kb_apply;
|
|
grant select, insert on public.claims to kb_apply;
|
|
grant select, insert on public.sources to kb_apply;
|
|
grant select, insert on public.reasoning_tools to kb_apply;
|
|
|
|
-- Keep the primary ledger denial explicit in addition to the full reset above.
|
|
revoke update on kb_stage.kb_proposals from kb_apply;
|
|
grant select on kb_stage.kb_proposals to kb_apply;
|
|
grant select on kb_stage.kb_proposals to kb_review;
|
|
|
|
grant select, update on kb_stage.kb_proposals to kb_gate_owner;
|
|
grant select on kb_stage.kb_review_principals to kb_gate_owner;
|
|
grant select, insert on kb_stage.kb_proposal_approvals to kb_gate_owner;
|
|
|
|
-- Required conflict arbiters. Existing production constraints satisfy these
|
|
-- checks; fresh clones receive named indexes only when no equivalent unique
|
|
-- index exists. Migration failure on pre-existing duplicates is intentional.
|
|
do $indexes$
|
|
begin
|
|
if not exists (
|
|
select 1
|
|
from pg_catalog.pg_index i
|
|
join pg_catalog.pg_class t on t.oid = i.indrelid
|
|
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
|
|
where n.nspname = 'public'
|
|
and t.relname = 'sources'
|
|
and i.indisunique
|
|
and i.indpred is null
|
|
and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '') like '%(hash)%'
|
|
) then
|
|
execute 'create unique index kb_apply_sources_hash_unique on public.sources (hash)';
|
|
end if;
|
|
|
|
if not exists (
|
|
select 1
|
|
from pg_catalog.pg_index i
|
|
join pg_catalog.pg_class t on t.oid = i.indrelid
|
|
join pg_catalog.pg_namespace n on n.oid = t.relnamespace
|
|
where n.nspname = 'public'
|
|
and t.relname = 'claim_evidence'
|
|
and i.indisunique
|
|
and i.indpred is null
|
|
and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '')
|
|
like '%(claim_id,source_id,role)%'
|
|
) then
|
|
execute 'create unique index kb_apply_claim_evidence_semantic_unique '
|
|
'on public.claim_evidence (claim_id, source_id, role)';
|
|
end if;
|
|
end
|
|
$indexes$;
|
|
|
|
create unique index if not exists one_active_strategy_per_agent
|
|
on public.strategies (agent_id) where active;
|
|
|
|
-- Reviewer-only state transition. The expected type/payload are the exact
|
|
-- Python-validated snapshot; a concurrent pending-row mutation makes the
|
|
-- locked WHERE clause fail. Reviewer identity comes from session_user mapping.
|
|
create or replace function kb_stage.approve_strict_proposal(
|
|
p_proposal_id uuid,
|
|
p_expected_proposal_type text,
|
|
p_expected_payload jsonb,
|
|
p_expected_reviewed_by_handle text,
|
|
p_review_note text
|
|
) returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = pg_catalog, pg_temp
|
|
as $function$
|
|
declare
|
|
v_row kb_stage.kb_proposals%rowtype;
|
|
v_principal kb_stage.kb_review_principals%rowtype;
|
|
v_reviewed_at timestamptz;
|
|
begin
|
|
if nullif(pg_catalog.btrim(p_expected_reviewed_by_handle), '') is null then
|
|
raise exception 'approve_strict_proposal: expected reviewer handle is required';
|
|
end if;
|
|
if nullif(pg_catalog.btrim(p_review_note), '') is null then
|
|
raise exception 'approve_strict_proposal: review note is required';
|
|
end if;
|
|
|
|
select * into v_principal
|
|
from kb_stage.kb_review_principals
|
|
where db_role = session_user::name
|
|
and active;
|
|
if not found then
|
|
raise exception 'approve_strict_proposal: session role % has no active reviewer principal',
|
|
session_user;
|
|
end if;
|
|
if v_principal.reviewed_by_handle <> pg_catalog.btrim(p_expected_reviewed_by_handle) then
|
|
raise exception 'approve_strict_proposal: session role % is mapped to reviewer %, not %',
|
|
session_user, v_principal.reviewed_by_handle, p_expected_reviewed_by_handle;
|
|
end if;
|
|
|
|
select * into v_row
|
|
from kb_stage.kb_proposals
|
|
where id = p_proposal_id
|
|
and status = 'pending_review'
|
|
and proposal_type = p_expected_proposal_type
|
|
and payload = p_expected_payload
|
|
for update;
|
|
if not found then
|
|
raise exception 'approve_strict_proposal: proposal % is not the exact pending snapshot',
|
|
p_proposal_id;
|
|
end if;
|
|
if v_row.proposal_type not in ('revise_strategy', 'add_edge', 'attach_evidence', 'approve_claim') then
|
|
raise exception 'approve_strict_proposal: unsupported proposal_type %', v_row.proposal_type;
|
|
end if;
|
|
if pg_catalog.jsonb_typeof(v_row.payload) <> 'object'
|
|
or not (v_row.payload ? 'apply_payload')
|
|
or pg_catalog.jsonb_typeof(v_row.payload->'apply_payload') <> 'object' then
|
|
raise exception 'approve_strict_proposal: proposal % lacks object payload.apply_payload',
|
|
p_proposal_id;
|
|
end if;
|
|
|
|
v_reviewed_at := pg_catalog.clock_timestamp();
|
|
insert into kb_stage.kb_proposal_approvals
|
|
(proposal_id, proposal_type, payload, reviewed_by_handle,
|
|
reviewed_by_agent_id, reviewed_by_db_role, reviewed_at, review_note)
|
|
values
|
|
(v_row.id, v_row.proposal_type, v_row.payload, v_principal.reviewed_by_handle,
|
|
v_principal.reviewed_by_agent_id, session_user::name, v_reviewed_at,
|
|
pg_catalog.btrim(p_review_note));
|
|
|
|
update kb_stage.kb_proposals
|
|
set status = 'approved',
|
|
reviewed_by_handle = v_principal.reviewed_by_handle,
|
|
reviewed_by_agent_id = v_principal.reviewed_by_agent_id,
|
|
reviewed_at = v_reviewed_at,
|
|
review_note = pg_catalog.btrim(p_review_note),
|
|
updated_at = pg_catalog.clock_timestamp()
|
|
where id = p_proposal_id
|
|
and status = 'pending_review'
|
|
and proposal_type = p_expected_proposal_type
|
|
and payload = p_expected_payload
|
|
returning * into v_row;
|
|
if not found then
|
|
raise exception 'approve_strict_proposal: proposal % changed while approval was recorded',
|
|
p_proposal_id;
|
|
end if;
|
|
|
|
return pg_catalog.jsonb_build_object(
|
|
'id', v_row.id::text,
|
|
'proposal_type', v_row.proposal_type,
|
|
'status', v_row.status,
|
|
'reviewed_by_handle', v_row.reviewed_by_handle,
|
|
'reviewed_by_agent_id', v_row.reviewed_by_agent_id::text,
|
|
'reviewed_by_db_role', session_user::text,
|
|
'reviewed_at', v_row.reviewed_at::text,
|
|
'review_note', v_row.review_note
|
|
);
|
|
end
|
|
$function$;
|
|
|
|
-- Apply-side precondition. It locks the proposal for the surrounding transaction
|
|
-- and compares both the mutable ledger and immutable approval snapshot.
|
|
create or replace function kb_stage.assert_approved_proposal(
|
|
p_proposal_id uuid,
|
|
p_proposal_type text,
|
|
p_payload jsonb,
|
|
p_reviewed_by_handle text,
|
|
p_reviewed_by_agent_id uuid,
|
|
p_reviewed_at timestamptz,
|
|
p_review_note text
|
|
) returns void
|
|
language plpgsql
|
|
security definer
|
|
set search_path = pg_catalog, pg_temp
|
|
as $function$
|
|
begin
|
|
perform 1
|
|
from kb_stage.kb_proposals p
|
|
join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id
|
|
where p.id = p_proposal_id
|
|
and p.status = 'approved'
|
|
and p.proposal_type = p_proposal_type
|
|
and p.payload = p_payload
|
|
and p.reviewed_by_handle = p_reviewed_by_handle
|
|
and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
|
|
and p.reviewed_at = p_reviewed_at
|
|
and p.review_note = p_review_note
|
|
and a.proposal_type = p_proposal_type
|
|
and a.payload = p_payload
|
|
and a.reviewed_by_handle = p_reviewed_by_handle
|
|
and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
|
|
and a.reviewed_at = p_reviewed_at
|
|
and a.review_note = p_review_note
|
|
for update of p;
|
|
if not found then
|
|
raise exception 'assert_approved_proposal: proposal % is not the exact immutable approved snapshot',
|
|
p_proposal_id;
|
|
end if;
|
|
end
|
|
$function$;
|
|
|
|
-- Apply-side terminal transition. It repeats every snapshot comparison while
|
|
-- the proposal row remains locked, resolves a real service-agent FK, and changes
|
|
-- ledger fields only. kb_apply remains a trusted narrow canonical writer; the
|
|
-- Python transaction performs and independently verifies typed writes first.
|
|
create or replace function kb_stage.finish_approved_proposal(
|
|
p_proposal_id uuid,
|
|
p_proposal_type text,
|
|
p_payload jsonb,
|
|
p_reviewed_by_handle text,
|
|
p_reviewed_by_agent_id uuid,
|
|
p_reviewed_at timestamptz,
|
|
p_review_note text,
|
|
p_applied_by_handle text
|
|
) returns jsonb
|
|
language plpgsql
|
|
security definer
|
|
set search_path = pg_catalog, pg_temp
|
|
as $function$
|
|
declare
|
|
v_row kb_stage.kb_proposals%rowtype;
|
|
v_applied_by_agent_id uuid;
|
|
begin
|
|
if nullif(pg_catalog.btrim(p_applied_by_handle), '') is null then
|
|
raise exception 'finish_approved_proposal: applied-by handle is required';
|
|
end if;
|
|
|
|
perform 1
|
|
from kb_stage.kb_proposals p
|
|
join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id
|
|
where p.id = p_proposal_id
|
|
and p.status = 'approved'
|
|
and p.proposal_type = p_proposal_type
|
|
and p.payload = p_payload
|
|
and p.reviewed_by_handle = p_reviewed_by_handle
|
|
and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
|
|
and p.reviewed_at = p_reviewed_at
|
|
and p.review_note = p_review_note
|
|
and a.proposal_type = p_proposal_type
|
|
and a.payload = p_payload
|
|
and a.reviewed_by_handle = p_reviewed_by_handle
|
|
and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
|
|
and a.reviewed_at = p_reviewed_at
|
|
and a.review_note = p_review_note
|
|
for update of p;
|
|
if not found then
|
|
raise exception 'finish_approved_proposal: proposal % is not the locked immutable approved snapshot',
|
|
p_proposal_id;
|
|
end if;
|
|
|
|
select id into v_applied_by_agent_id
|
|
from public.agents
|
|
where handle = pg_catalog.btrim(p_applied_by_handle)
|
|
limit 1;
|
|
if v_applied_by_agent_id is null then
|
|
raise exception 'finish_approved_proposal: applied-by handle % has no public.agents row',
|
|
p_applied_by_handle;
|
|
end if;
|
|
|
|
update kb_stage.kb_proposals
|
|
set status = 'applied',
|
|
applied_by_handle = pg_catalog.btrim(p_applied_by_handle),
|
|
applied_by_agent_id = v_applied_by_agent_id,
|
|
applied_at = pg_catalog.clock_timestamp(),
|
|
updated_at = pg_catalog.clock_timestamp()
|
|
where id = p_proposal_id
|
|
and status = 'approved'
|
|
and proposal_type = p_proposal_type
|
|
and payload = p_payload
|
|
and reviewed_by_handle = p_reviewed_by_handle
|
|
and reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id
|
|
and reviewed_at = p_reviewed_at
|
|
and review_note = p_review_note
|
|
returning * into v_row;
|
|
if not found then
|
|
raise exception 'finish_approved_proposal: proposal % changed during apply', p_proposal_id;
|
|
end if;
|
|
|
|
return pg_catalog.jsonb_build_object(
|
|
'id', v_row.id::text,
|
|
'proposal_type', v_row.proposal_type,
|
|
'status', v_row.status,
|
|
'applied_by_handle', v_row.applied_by_handle,
|
|
'applied_by_agent_id', v_row.applied_by_agent_id::text,
|
|
'applied_at', v_row.applied_at::text
|
|
);
|
|
end
|
|
$function$;
|
|
|
|
alter function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
|
owner to kb_gate_owner;
|
|
alter function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
|
|
owner to kb_gate_owner;
|
|
alter function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
|
|
owner to kb_gate_owner;
|
|
|
|
revoke all on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
|
from public, kb_gate_owner, kb_apply, kb_review;
|
|
revoke all on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
|
|
from public, kb_gate_owner, kb_apply, kb_review;
|
|
revoke all on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
|
|
from public, kb_gate_owner, kb_apply, kb_review;
|
|
|
|
grant execute on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
|
to kb_review;
|
|
grant execute on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text)
|
|
to kb_apply;
|
|
grant execute on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text)
|
|
to kb_apply;
|
|
|
|
-- Assert the complete protected catalog contract before commit. These checks
|
|
-- make reruns an upgrade proof rather than a best-effort sequence of grants.
|
|
do $final_invariants$
|
|
declare
|
|
v_drift text;
|
|
begin
|
|
select pg_catalog.string_agg(role.rolname, ', ' order by role.rolname)
|
|
into v_drift
|
|
from pg_catalog.pg_roles role
|
|
where role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
and (
|
|
role.rolsuper
|
|
or role.rolinherit
|
|
or role.rolcreatedb
|
|
or role.rolcreaterole
|
|
or role.rolreplication
|
|
or role.rolbypassrls
|
|
or (role.rolname = 'kb_gate_owner' and role.rolcanlogin)
|
|
or (role.rolname in ('kb_review', 'kb_apply') and not role.rolcanlogin)
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected role attributes did not normalize: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname),
|
|
', ' order by member_role.rolname, granted_role.rolname
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
|
or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply');
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected role membership appeared during migration: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I on %I usage=%s create=%s',
|
|
protected_role.rolname,
|
|
protected_schema.nspname,
|
|
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'USAGE'),
|
|
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'CREATE')
|
|
),
|
|
', ' order by protected_role.rolname, protected_schema.nspname
|
|
)
|
|
into v_drift
|
|
from (values ('kb_gate_owner'), ('kb_review'), ('kb_apply')) protected_role(rolname)
|
|
cross join (values ('public'), ('kb_stage')) protected_schema(nspname)
|
|
where not pg_catalog.has_schema_privilege(
|
|
protected_role.rolname, protected_schema.nspname, 'USAGE'
|
|
)
|
|
or pg_catalog.has_schema_privilege(
|
|
protected_role.rolname, protected_schema.nspname, 'CREATE'
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected schema ACL mismatch: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname),
|
|
', ' order by namespace.nspname, relation.relname
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_class relation
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
|
|
where (namespace.nspname, relation.relname) in (
|
|
('public', 'agents'),
|
|
('public', 'strategies'),
|
|
('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'),
|
|
('public', 'claim_edges'),
|
|
('public', 'claims'),
|
|
('public', 'sources'),
|
|
('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'),
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
and not (
|
|
(namespace.nspname = 'kb_stage'
|
|
and relation.relname in ('kb_review_principals', 'kb_proposal_approvals')
|
|
and owner_role.rolname = 'kb_gate_owner')
|
|
or (not (
|
|
namespace.nspname = 'kb_stage'
|
|
and relation.relname in ('kb_review_principals', 'kb_proposal_approvals')
|
|
)
|
|
and owner_role.rolname = current_user)
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected table ownership mismatch: %', v_drift;
|
|
end if;
|
|
|
|
with expected(signature) as (
|
|
values
|
|
('approve_strict_proposal(uuid, text, jsonb, text, text)'),
|
|
('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)'),
|
|
('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)')
|
|
), actual(signature) as (
|
|
select procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')'
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
), delta(kind, signature) as (
|
|
select 'unexpected', actual.signature
|
|
from actual
|
|
where not exists (select 1 from expected where expected.signature = actual.signature)
|
|
union all
|
|
select 'missing', expected.signature
|
|
from expected
|
|
where not exists (select 1 from actual where actual.signature = expected.signature)
|
|
)
|
|
select pg_catalog.string_agg(kind || ' ' || signature, ', ' order by kind, signature)
|
|
into v_drift
|
|
from delta;
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected function signature mismatch: %', v_drift;
|
|
end if;
|
|
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I(%s) owner=%I security_definer=%s config=%s',
|
|
namespace.nspname,
|
|
procedure.proname,
|
|
pg_catalog.oidvectortypes(procedure.proargtypes),
|
|
owner_role.rolname,
|
|
procedure.prosecdef,
|
|
procedure.proconfig
|
|
),
|
|
', ' order by procedure.proname
|
|
)
|
|
into v_drift
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
and (
|
|
owner_role.rolname <> 'kb_gate_owner'
|
|
or not procedure.prosecdef
|
|
or pg_catalog.array_to_string(procedure.proconfig, ',') is distinct from
|
|
'search_path=pg_catalog, pg_temp'
|
|
);
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected function attribute mismatch: %', v_drift;
|
|
end if;
|
|
|
|
with tracked(schema_name, object_name) as (
|
|
values
|
|
('public', 'agents'),
|
|
('public', 'strategies'),
|
|
('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'),
|
|
('public', 'claim_edges'),
|
|
('public', 'claims'),
|
|
('public', 'sources'),
|
|
('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'),
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
)
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%I.%I.%I -> %s %s grantable=%s',
|
|
namespace.nspname,
|
|
relation.relname,
|
|
attribute.attname,
|
|
case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else pg_catalog.pg_get_userbyid(acl.grantee)
|
|
end,
|
|
pg_catalog.upper(acl.privilege_type),
|
|
acl.is_grantable
|
|
),
|
|
', ' order by namespace.nspname, relation.relname, attribute.attnum, acl.grantee
|
|
)
|
|
into v_drift
|
|
from tracked
|
|
join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name
|
|
join pg_catalog.pg_class relation
|
|
on relation.relnamespace = namespace.oid
|
|
and relation.relname = tracked.object_name
|
|
join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid
|
|
cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where attribute.attnum > 0
|
|
and not attribute.attisdropped
|
|
and (acl.grantee = 0
|
|
or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'));
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected column ACL mismatch: %', v_drift;
|
|
end if;
|
|
|
|
with expected(schema_name, object_name, grantee, privilege_type, is_grantable) as (
|
|
values
|
|
('public', 'agents', 'kb_gate_owner', 'SELECT', false),
|
|
('public', 'agents', 'kb_apply', 'SELECT', false),
|
|
('public', 'agents', 'kb_review', 'SELECT', false),
|
|
('public', 'strategies', 'kb_apply', 'SELECT', false),
|
|
('public', 'strategies', 'kb_apply', 'INSERT', false),
|
|
('public', 'strategies', 'kb_apply', 'UPDATE', false),
|
|
('public', 'strategy_nodes', 'kb_apply', 'SELECT', false),
|
|
('public', 'strategy_nodes', 'kb_apply', 'INSERT', false),
|
|
('public', 'strategy_nodes', 'kb_apply', 'UPDATE', false),
|
|
('public', 'claim_evidence', 'kb_apply', 'SELECT', false),
|
|
('public', 'claim_evidence', 'kb_apply', 'INSERT', false),
|
|
('public', 'claim_edges', 'kb_apply', 'SELECT', false),
|
|
('public', 'claim_edges', 'kb_apply', 'INSERT', false),
|
|
('public', 'claims', 'kb_apply', 'SELECT', false),
|
|
('public', 'claims', 'kb_apply', 'INSERT', false),
|
|
('public', 'sources', 'kb_apply', 'SELECT', false),
|
|
('public', 'sources', 'kb_apply', 'INSERT', false),
|
|
('public', 'reasoning_tools', 'kb_apply', 'SELECT', false),
|
|
('public', 'reasoning_tools', 'kb_apply', 'INSERT', false),
|
|
('kb_stage', 'kb_proposals', 'kb_gate_owner', 'SELECT', false),
|
|
('kb_stage', 'kb_proposals', 'kb_gate_owner', 'UPDATE', false),
|
|
('kb_stage', 'kb_proposals', 'kb_apply', 'SELECT', false),
|
|
('kb_stage', 'kb_proposals', 'kb_review', 'SELECT', false)
|
|
), tracked(schema_name, object_name) as (
|
|
values
|
|
('public', 'agents'),
|
|
('public', 'strategies'),
|
|
('public', 'strategy_nodes'),
|
|
('public', 'claim_evidence'),
|
|
('public', 'claim_edges'),
|
|
('public', 'claims'),
|
|
('public', 'sources'),
|
|
('public', 'reasoning_tools'),
|
|
('kb_stage', 'kb_proposals'),
|
|
('kb_stage', 'kb_review_principals'),
|
|
('kb_stage', 'kb_proposal_approvals')
|
|
), actual(schema_name, object_name, grantee, privilege_type, is_grantable) as (
|
|
select distinct
|
|
namespace.nspname::text,
|
|
relation.relname::text,
|
|
(case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else grantee_role.rolname
|
|
end)::text,
|
|
pg_catalog.upper(acl.privilege_type),
|
|
acl.is_grantable
|
|
from tracked
|
|
join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name
|
|
join pg_catalog.pg_class relation
|
|
on relation.relnamespace = namespace.oid
|
|
and relation.relname = tracked.object_name
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
|
|
) acl
|
|
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
|
where acl.grantee <> relation.relowner
|
|
and (acl.grantee = 0
|
|
or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'))
|
|
), delta(kind, schema_name, object_name, grantee, privilege_type, is_grantable) as (
|
|
select 'unexpected', actual.*
|
|
from actual
|
|
where not exists (
|
|
select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual)
|
|
)
|
|
union all
|
|
select 'missing', expected.*
|
|
from expected
|
|
where not exists (
|
|
select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected)
|
|
)
|
|
)
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%s %I.%I -> %I %s grantable=%s',
|
|
kind, schema_name, object_name, grantee, privilege_type, is_grantable
|
|
),
|
|
', ' order by kind, schema_name, object_name, grantee, privilege_type
|
|
)
|
|
into v_drift
|
|
from delta;
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected table ACL mismatch: %', v_drift;
|
|
end if;
|
|
|
|
with expected(signature, grantee, privilege_type, is_grantable) as (
|
|
values
|
|
('approve_strict_proposal(uuid, text, jsonb, text, text)', 'kb_review', 'EXECUTE', false),
|
|
('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)',
|
|
'kb_apply', 'EXECUTE', false),
|
|
('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)',
|
|
'kb_apply', 'EXECUTE', false)
|
|
), actual(signature, grantee, privilege_type, is_grantable) as (
|
|
select distinct
|
|
procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')',
|
|
(case
|
|
when acl.grantee = 0 then 'PUBLIC'
|
|
else pg_catalog.pg_get_userbyid(acl.grantee)
|
|
end)::text,
|
|
pg_catalog.upper(acl.privilege_type),
|
|
acl.is_grantable
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
cross join lateral pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname in (
|
|
'approve_strict_proposal',
|
|
'assert_approved_proposal',
|
|
'finish_approved_proposal'
|
|
)
|
|
and acl.grantee <> procedure.proowner
|
|
), delta(kind, signature, grantee, privilege_type, is_grantable) as (
|
|
select 'unexpected', actual.*
|
|
from actual
|
|
where not exists (
|
|
select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual)
|
|
)
|
|
union all
|
|
select 'missing', expected.*
|
|
from expected
|
|
where not exists (
|
|
select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected)
|
|
)
|
|
)
|
|
select pg_catalog.string_agg(
|
|
pg_catalog.format(
|
|
'%s %s -> %I %s grantable=%s',
|
|
kind, signature, grantee, privilege_type, is_grantable
|
|
),
|
|
', ' order by kind, signature, grantee, privilege_type
|
|
)
|
|
into v_drift
|
|
from delta;
|
|
|
|
if v_drift is not null then
|
|
raise exception 'kb_apply_prereqs: protected function ACL mismatch: %', v_drift;
|
|
end if;
|
|
end
|
|
$final_invariants$;
|
|
|
|
commit;
|