183 lines
7.3 KiB
SQL
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;
|