\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;