teleo-infrastructure/ops/observatory_read_role.sql
twentyOne2x 92da54d804
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add authenticated GCP Observatory read adapter (#154)
2026-07-15 03:43:05 +02:00

183 lines
7.3 KiB
SQL

\set ON_ERROR_STOP on
\getenv observatory_iam_user OBSERVATORY_DB_IAM_USER
-- Provision only after Cloud SQL has created the dedicated IAM database user.
-- The service-account username omits the .gserviceaccount.com suffix.
select set_config('observatory.iam_user', :'observatory_iam_user', false);
do $preflight$
declare
principal text := current_setting('observatory.iam_user');
begin
if current_database() <> 'teleo_canonical' then
raise exception 'observatory_read_role must run only in teleo_canonical';
end if;
if principal !~ '^[a-z0-9-]+@teleo-501523[.]iam$' then
raise exception 'OBSERVATORY_DB_IAM_USER is not the scoped GCP service-account database user';
end if;
if not exists (select 1 from pg_catalog.pg_roles where rolname = principal) then
raise exception 'Cloud SQL IAM database user does not exist: %', principal;
end if;
end
$preflight$;
select 'create role kb_observatory_read nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1'
where not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_observatory_read')
\gexec
alter role kb_observatory_read
with nologin nosuperuser nocreatedb nocreaterole noinherit noreplication nobypassrls connection limit -1
password null;
alter role kb_observatory_read reset all;
revoke all privileges on database teleo_canonical from kb_observatory_read;
grant connect on database teleo_canonical to kb_observatory_read;
revoke all on schema public, kb_stage from kb_observatory_read;
revoke all privileges on all tables in schema public, kb_stage from kb_observatory_read;
revoke all privileges on all sequences in schema public, kb_stage from kb_observatory_read;
revoke all privileges on all functions in schema public, kb_stage from kb_observatory_read;
revoke all privileges on all procedures in schema public, kb_stage from kb_observatory_read;
select format(
'revoke all privileges (%s) on table %I.%I from kb_observatory_read',
pg_catalog.string_agg(pg_catalog.quote_ident(attribute.attname), ', ' order by attribute.attnum),
namespace.nspname,
relation.relname
)
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
group by namespace.nspname, relation.relname
\gexec
grant usage on schema public, kb_stage to kb_observatory_read;
grant select on
public.claims,
public.sources,
public.claim_evidence,
public.claim_edges,
kb_stage.kb_proposals
to kb_observatory_read;
do $membership$
declare
principal text := current_setting('observatory.iam_user');
unexpected text;
begin
select pg_catalog.string_agg(granted.rolname, ', ' order by granted.rolname)
into unexpected
from pg_catalog.pg_auth_members membership
join pg_catalog.pg_roles granted on granted.oid = membership.roleid
join pg_catalog.pg_roles member on member.oid = membership.member
where member.rolname = principal
and granted.rolname <> 'kb_observatory_read';
if unexpected is not null then
raise exception 'dedicated Observatory principal has unexpected role memberships: %', unexpected;
end if;
select pg_catalog.string_agg(member.rolname, ', ' order by member.rolname)
into unexpected
from pg_catalog.pg_auth_members membership
join pg_catalog.pg_roles granted on granted.oid = membership.roleid
join pg_catalog.pg_roles member on member.oid = membership.member
where granted.rolname = 'kb_observatory_read'
and member.rolname <> principal;
if unexpected is not null then
raise exception 'kb_observatory_read has unexpected members: %', unexpected;
end if;
execute pg_catalog.format('grant kb_observatory_read to %I', principal);
execute pg_catalog.format(
'alter role %I in database teleo_canonical set default_transaction_read_only = on',
principal
);
execute pg_catalog.format(
'alter role %I in database teleo_canonical set statement_timeout = %L',
principal,
'5s'
);
execute pg_catalog.format(
'alter role %I in database teleo_canonical set lock_timeout = %L',
principal,
'1s'
);
end
$membership$;
do $verify$
declare
principal text := current_setting('observatory.iam_user');
unexpected text;
begin
if not pg_catalog.pg_has_role(principal, 'kb_observatory_read', 'member') then
raise exception 'Observatory principal is not a member of kb_observatory_read';
end if;
if not exists (
select 1 from pg_catalog.pg_roles
where rolname = principal and rolinherit and not rolsuper and not rolcreatedb
and not rolcreaterole and not rolreplication and not rolbypassrls
) then
raise exception 'Observatory principal has unsafe role attributes or cannot inherit read grants';
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 pg_catalog.has_table_privilege(principal, relation.oid, privilege.name);
if unexpected is not null then
raise exception 'Observatory principal has effective table writes: %', 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 pg_catalog.has_table_privilege(principal, relation.oid, 'SELECT')
and (namespace.nspname, relation.relname) not in (
('public', 'claims'),
('public', 'sources'),
('public', 'claim_evidence'),
('public', 'claim_edges'),
('kb_stage', 'kb_proposals')
);
if unexpected is not null then
raise exception 'Observatory principal can SELECT outside its allowlist: %', unexpected;
end if;
if not (
pg_catalog.has_table_privilege(principal, 'public.claims', 'SELECT')
and pg_catalog.has_table_privilege(principal, 'public.sources', 'SELECT')
and pg_catalog.has_table_privilege(principal, 'public.claim_evidence', 'SELECT')
and pg_catalog.has_table_privilege(principal, 'public.claim_edges', 'SELECT')
and pg_catalog.has_table_privilege(principal, 'kb_stage.kb_proposals', 'SELECT')
) then
raise exception 'Observatory principal is missing a required read grant';
end if;
end
$verify$;
select jsonb_build_object(
'database', current_database(),
'authorization_role', 'kb_observatory_read',
'database_principal', current_setting('observatory.iam_user'),
'required_reads', true,
'effective_table_writes_denied', true,
'default_transaction_read_only', true
)::text;