-- ============================================================================
-- Database least-privilege provisioning — receipt scanner app
-- ============================================================================
-- Creates the runtime role `receipt_app` and grants it ONLY the privileges the
-- application needs at runtime:
--
--   * CONNECT on the database
--   * USAGE on the public schema — explicitly NO CREATE (no DDL of any kind)
--   * SELECT / INSERT / UPDATE / DELETE on all tables in public
--   * USAGE / SELECT on all sequences in public
--   * the same DML grants as DEFAULT PRIVILEGES, so tables/sequences created
--     by the owner during later migrations are covered automatically
--
-- The role is deliberately NOT a superuser and has no CREATEDB / CREATEROLE /
-- CREATE-on-schema rights: if the application is compromised, the attacker's
-- database blast radius is limited to reading and modifying rows. They cannot
-- create/drop tables, alter the schema, or grant themselves more rights.
--
-- ----------------------------------------------------------------------------
-- Who must run this?  The database owner or a superuser.
--   * `receipt_user` (docker-compose's POSTGRES_USER / the owner URL) KEEPS all
--     of its current rights — including DDL for migrations and schema init —
--     and is the only role this script ever grants privileges to (besides the
--     new runtime role). Nothing here weakens the owner.
--   * REVOKE CREATE ON SCHEMA public FROM PUBLIC requires the schema owner or
--     a superuser. The official postgres image's /docker-entrypoint-initdb.d
--     scripts run as POSTGRES_USER, so that path satisfies this.
--
-- How is it executed?
--   * Fresh database / volume: docker-compose mounts this file into
--     /docker-entrypoint-initdb.d/10-db-permissions.sql; the postgres image
--     runs it once, before the app starts, as POSTGRES_USER. (Init scripts do
--     NOT run on an existing volume.)
--   * Existing database (e.g. the local dev DB): `node scripts/apply-db-permissions.mjs`
--
-- Idempotent: safe to run any number of times. CREATE ROLE is guarded by an
-- existence check; every GRANT / REVOKE / ALTER DEFAULT PRIVILEGES is a no-op
-- when already applied.
--
-- Runtime role password:
--   The default below (receipt_app_secure_password) matches the docker-compose
--   default APP_DATABASE_PASSWORD and is for LOCAL DEVELOPMENT only. For
--   production, choose a strong password:
--     * docker init path: edit the CREATE ROLE statement below before first
--       volume creation (this file runs as-is under psql), or
--     * apply-script path: set APP_DATABASE_PASSWORD when running
--       scripts/apply-db-permissions.mjs — the script substitutes that value
--       and rotates the password of an already-existing role (ALTER ROLE).
-- ============================================================================

-- 1. Create the least-privilege runtime role (guarded -> re-runnable).
DO $dbpermissions$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'receipt_app') THEN
    CREATE ROLE receipt_app
      LOGIN
      PASSWORD 'receipt_app_secure_password'   -- documented default, see header
      NOSUPERUSER
      NOCREATEDB
      NOCREATEROLE;
  END IF;
END
$dbpermissions$;

-- 2. Allow the app to connect. The database name matches POSTGRES_DB /
--    the DATABASE_URL database (receipt_scanner); adjust if you use a
--    different database name.
GRANT CONNECT ON DATABASE receipt_scanner TO receipt_app;

-- 3. Schema access: strip anything pre-existing, then grant USAGE only.
--    NOTE: CREATE is deliberately NOT granted — receipt_app can never create,
--    alter or drop schema objects (no DDL).
REVOKE ALL ON SCHEMA public FROM receipt_app;
GRANT USAGE ON SCHEMA public TO receipt_app;

-- 4. DML on the tables / sequences that exist right now. (On a fresh database
--    this grants nothing yet — step 5 covers the objects the owner creates
--    during migrations.)
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO receipt_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO receipt_app;

-- 5. Future objects: when the owner (receipt_user) creates tables / sequences
--    during later migrations, the runtime role gets the same DML automatically.
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO receipt_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO receipt_app;

-- 6. Harden the schema: take CREATE away from the PUBLIC pseudo-role so no
--    other database user can create objects in public either. Must be run by
--    the schema owner / a superuser (see header). The owner (receipt_user) is
--    unaffected — it keeps full rights through pg_database_owner — so
--    migrations and schema init keep working.
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
