Files
gongxue-base/supabase/migrations/202607120013_backend_runtime_roles.sql
2026-07-12 19:26:57 +08:00

278 lines
9.0 KiB
SQL

do $$
declare
runtime_role name;
role_state record;
begin
foreach runtime_role in array array['tiku_api'::name, 'tiku_worker'::name]
loop
select role_row.*,
exists (
select 1 from pg_auth_members membership
where membership.member = role_row.oid
) as has_parent_roles
into role_state
from pg_roles role_row
where role_row.rolname = runtime_role;
if not found then
raise exception
'Runtime role % is missing. A PostgreSQL superuser must run scripts/bootstrap-backend-runtime-roles.js before migrations.',
runtime_role;
end if;
if role_state.rolsuper
or role_state.rolinherit
or role_state.rolcreatedb
or role_state.rolcreaterole
or not role_state.rolcanlogin
or role_state.rolreplication
or not role_state.rolbypassrls
or role_state.has_parent_roles
or not coalesce(role_state.rolconfig, '{}'::text[])
@> array['search_path=pg_catalog, public, extensions']::text[] then
raise exception
'Runtime role % attributes are unsafe or incomplete. Re-run the privileged backend runtime role bootstrap.',
runtime_role;
end if;
end loop;
end
$$;
-- Persistent DDL stays with the migration owner. Revoke PUBLIC first so an
-- effective CREATE privilege cannot leak back through the implicit role.
revoke create on schema public, app, app_private, extensions from public;
revoke all privileges on schema public, app, app_private, extensions from tiku_api, tiku_worker;
grant usage on schema public, app_private, extensions to tiku_api, tiku_worker;
grant usage on schema app to tiku_api;
revoke all privileges
on all tables in schema public, app_private
from tiku_api, tiku_worker;
grant select, insert, update, delete
on all tables in schema public
to tiku_api, tiku_worker;
grant select
on all tables in schema app_private
to tiku_api, tiku_worker;
grant insert, update
on app_private.auth_sessions,
app_private.tenant_secrets,
app_private.platform_secrets
to tiku_api;
grant insert, update, delete
on app_private.sms_send_rate_limits
to tiku_api;
revoke all privileges
on all sequences in schema public, app_private
from tiku_api, tiku_worker;
grant usage, select
on all sequences in schema public
to tiku_api, tiku_worker;
-- Normalize UUID defaults to the PostgreSQL 13+ core function. Supabase may
-- install pgcrypto in extensions while older self-hosted databases may have a
-- public wrapper, so runtime correctness must not depend on either layout.
do $$
declare
default_column record;
begin
if to_regprocedure('pg_catalog.gen_random_uuid()') is null then
raise exception 'PostgreSQL 13 or newer is required: pg_catalog.gen_random_uuid() is unavailable';
end if;
for default_column in
select namespace.nspname as schema_name,
relation.relname as table_name,
attribute.attname as column_name
from pg_attrdef default_value
join pg_class relation on relation.oid = default_value.adrelid
join pg_namespace namespace on namespace.oid = relation.relnamespace
join pg_attribute attribute
on attribute.attrelid = relation.oid
and attribute.attnum = default_value.adnum
where namespace.nspname in ('public', 'app_private')
and relation.relkind in ('r', 'p')
and attribute.atttypid = 'uuid'::regtype
and pg_get_expr(default_value.adbin, default_value.adrelid)
~ '(^|[.])gen_random_uuid[(][)]$'
loop
execute format(
'alter table %I.%I alter column %I set default pg_catalog.gen_random_uuid()',
default_column.schema_name,
default_column.table_name,
default_column.column_name
);
end loop;
end
$$;
-- Function execution is reviewed explicitly. The API directly calls only the
-- public-bank helpers; pg_catalog functions retain PostgreSQL's built-in ACL.
revoke execute on all functions in schema app from public;
revoke all privileges
on all functions in schema public, app, app_private
from tiku_api, tiku_worker;
-- RLS policies resolve these functions by OID. Client roles still need
-- EXECUTE even though the app schema is not exposed for direct RPC access.
grant execute on function app.jwt_text(text)
to anon, authenticated, service_role;
grant execute on function app.current_tenant_id()
to anon, authenticated, service_role;
grant execute on function app.current_role()
to anon, authenticated, service_role;
grant execute on function app.is_platform_admin()
to anon, authenticated, service_role;
grant execute on function app.uuid_array_from_jsonb(jsonb)
to anon, authenticated, service_role;
grant execute on function app.public_question_bank_grant_allows(uuid[], uuid[], uuid, uuid[])
to anon, authenticated, service_role;
grant execute on function app.public_question_bank_subscription_allows(jsonb, jsonb, uuid, uuid, uuid[])
to anon, authenticated, service_role;
grant execute on function app.uuid_array_from_jsonb(jsonb)
to tiku_api;
grant execute on function app.public_question_bank_grant_allows(uuid[], uuid[], uuid, uuid[])
to tiku_api;
grant execute on function app.public_question_bank_subscription_allows(jsonb, jsonb, uuid, uuid, uuid[])
to tiku_api;
-- Keep future migration-owned objects on the same privilege matrix. New
-- functions intentionally receive no runtime grant until reviewed.
do $$
declare
schema_name text;
owner_name name;
begin
foreach schema_name in array array['public', 'app_private']
loop
for owner_name in
select distinct owner_role.rolname
from (
select c.relowner as owner_oid
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = schema_name
union
select p.proowner as owner_oid
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = schema_name
) owners
join pg_roles owner_role on owner_role.oid = owners.owner_oid
where owner_role.rolname = current_user
or pg_has_role(current_user, owner_role.oid, 'MEMBER')
loop
execute format(
'alter default privileges for role %I in schema %I revoke all privileges on tables from tiku_api, tiku_worker',
owner_name,
schema_name
);
execute format(
'alter default privileges for role %I in schema %I revoke all privileges on sequences from tiku_api, tiku_worker',
owner_name,
schema_name
);
if schema_name = 'public' then
execute format(
'alter default privileges for role %I in schema public grant select, insert, update, delete on tables to tiku_api, tiku_worker',
owner_name
);
execute format(
'alter default privileges for role %I in schema public grant usage, select on sequences to tiku_api, tiku_worker',
owner_name
);
else
execute format(
'alter default privileges for role %I in schema app_private grant select on tables to tiku_api, tiku_worker',
owner_name
);
end if;
end loop;
end loop;
end
$$;
-- PostgreSQL's built-in function default is PUBLIC EXECUTE. Revoke it at the
-- owner-global level because a schema-local default ACL cannot subtract that
-- global default for future app functions.
do $$
declare
owner_name name;
begin
for owner_name in
select distinct owner_role.rolname
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
join pg_roles owner_role on owner_role.oid = p.proowner
where n.nspname = 'app'
and (
owner_role.rolname = current_user
or pg_has_role(current_user, owner_role.oid, 'MEMBER')
)
loop
execute format(
'alter default privileges for role %I revoke execute on functions from public',
owner_name
);
end loop;
end
$$;
-- Fail closed if an earlier manual setup made a runtime role an owner. Owners
-- can always ALTER/DROP their objects regardless of grants.
do $$
declare
owned_object_count integer;
begin
select count(*)
into owned_object_count
from (
select c.relowner as owner_oid
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname in ('public', 'app', 'app_private')
union all
select p.proowner
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname in ('public', 'app', 'app_private')
union all
select t.typowner
from pg_type t
join pg_namespace n on n.oid = t.typnamespace
where n.nspname in ('public', 'app', 'app_private')
union all
select n.nspowner
from pg_namespace n
where n.nspname in ('public', 'app', 'app_private')
union all
select d.datdba
from pg_database d
where d.datname = current_database()
) owners
join pg_roles owner_role on owner_role.oid = owners.owner_oid
where owner_role.rolname in ('tiku_api', 'tiku_worker');
if owned_object_count > 0 then
raise exception
'tiku_api/tiku_worker must not own database objects; reassign ownership to the migration role before applying this migration';
end if;
end
$$;