\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 <> 'sa-observatory-read-adapter@teleo-501523.iam' then
    raise exception 'OBSERVATORY_DB_IAM_USER is not the exact dedicated Observatory 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;

  select pg_catalog.string_agg(protected_schema.schema_name, ', ' order by protected_schema.schema_name)
    into unexpected
    from (values ('public'), ('kb_stage')) protected_schema(schema_name)
   where pg_catalog.has_schema_privilege(principal, protected_schema.schema_name, 'CREATE');
  if unexpected is not null then
    raise exception 'Observatory principal can create objects in protected schemas: %', unexpected;
  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 unexpected
    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 pg_catalog.has_function_privilege(principal, procedure.oid, 'EXECUTE');
  if unexpected is not null then
    raise exception 'Observatory principal can execute protected proposal/apply gates: %', 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,
  'effective_schema_create_denied', true,
  'protected_apply_execute_denied', true,
  'default_transaction_read_only', true
)::text;
