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>
95 lines
4.8 KiB
SQL
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;
|