Files
scan-receipts/scripts/db-permissions.sql
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

95 lines
4.8 KiB
SQL

-- ============================================================================
-- 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;