-- Hammy backend bootstrap. Run ONCE, as a superuser, before any migration: -- -- psql -U postgres -f scripts/bootstrap.sql -- -- Deliberately NOT in migrations/. Three reasons: -- -- * CREATE DATABASE cannot run inside a transaction block, and migration -- tools wrap each file in one. -- * \c is a psql meta-command, not SQL. A migration tool has no idea what it -- means. -- * Roles are cluster-wide, not per-database, so they are not part of any one -- database's schema history. -- -- Order matters: the schemas and ALTER DEFAULT PRIVILEGES must exist BEFORE any -- table is created, because default privileges only apply to objects created -- afterwards. Get this right here and no migration ever needs a GRANT. -- --------------------------------------------------------------------------- -- Roles (cluster-wide) -- --------------------------------------------------------------------------- -- Owns every object and runs migrations. Never connects at runtime. CREATE ROLE hammy_owner NOLOGIN; -- What the backend connects as. DML only: it can read and write rows but not -- create, alter or drop anything, so a compromised backend cannot drop the -- logbook. CREATE ROLE hammy_app LOGIN PASSWORD 'CHANGE_ME'; -- Bulk loaders for FCC ULS, 44net allocations and the like. CREATE ROLE hammy_ingest LOGIN PASSWORD 'CHANGE_ME'; -- Analytics and debugging. Deliberately no access to the api schema - a -- reporting query has no business reading key material. CREATE ROLE hammy_readonly LOGIN PASSWORD 'CHANGE_ME'; -- --------------------------------------------------------------------------- -- Database -- --------------------------------------------------------------------------- CREATE DATABASE hammy OWNER hammy_owner ENCODING 'UTF8'; \c hammy -- --------------------------------------------------------------------------- -- Schemas -- --------------------------------------------------------------------------- -- Before PostgreSQL 15, PUBLIC held CREATE on the public schema, so any role -- could add objects to it. Revoke it and do not use public for anything. REVOKE ALL ON SCHEMA public FROM PUBLIC; CREATE SCHEMA core AUTHORIZATION hammy_owner; CREATE SCHEMA api AUTHORIZATION hammy_owner; CREATE SCHEMA logbook AUTHORIZATION hammy_owner; CREATE SCHEMA ingest AUTHORIZATION hammy_owner; COMMENT ON SCHEMA core IS 'Discord-linked user identity and callsign verification.'; COMMENT ON SCHEMA api IS 'API tenancy: owners, keys, usage. Most sensitive schema.'; COMMENT ON SCHEMA logbook IS 'Per-user QSO logs and import batches.'; COMMENT ON SCHEMA ingest IS 'Bulk-loaded third-party reference data.'; GRANT USAGE ON SCHEMA core, api, logbook, ingest TO hammy_app; GRANT USAGE ON SCHEMA ingest TO hammy_ingest; GRANT USAGE ON SCHEMA core, logbook TO hammy_readonly; -- --------------------------------------------------------------------------- -- Default privileges -- -- Apply to objects hammy_owner creates FROM NOW ON. Every migration runs as -- hammy_owner, so tables pick these up automatically. -- --------------------------------------------------------------------------- ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA core, api, logbook GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hammy_app; ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA core, api, logbook GRANT USAGE ON SEQUENCES TO hammy_app; ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA ingest GRANT SELECT ON TABLES TO hammy_app; ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA ingest GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hammy_ingest; ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA core, logbook GRANT SELECT ON TABLES TO hammy_readonly; -- Postgres grants EXECUTE on new functions to PUBLIC by default, which would -- make forget_user() callable by anyone who can connect. Revoke it; 005 grants -- it back to hammy_app explicitly. ALTER DEFAULT PRIVILEGES FOR ROLE hammy_owner IN SCHEMA core, api REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; -- --------------------------------------------------------------------------- -- search_path per role, so queries need not schema-qualify everything. -- --------------------------------------------------------------------------- ALTER ROLE hammy_owner SET search_path = core, api, logbook, ingest; ALTER ROLE hammy_app SET search_path = core, api, logbook, ingest; ALTER ROLE hammy_ingest SET search_path = ingest; ALTER ROLE hammy_readonly SET search_path = core, logbook;