-- ============================================================================= -- 003_roles.sql — least privilege inside pg-ai. -- -- psql -h pg-ai -U postgres -d plant -f 003_roles.sql -- -- Passwords are NOT in this file. Set them from the 0600 env files: -- \set agent_pw `echo "$AGENT_DB_PASSWORD"` -- or ALTER ROLE ... PASSWORD after creation, from a shell that reads ~/ai/*.env. -- -- Two roles, deliberately different: -- agent_ro the API. SELECT only, everywhere. It must not be able to write. -- cube_rw Cube. SELECT on reference data, full rights on cube_preagg only, -- because pre-aggregation refresh creates and drops tables there. -- ============================================================================= -- --- agent_ro — the application role. SELECT and nothing else. --------------- DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'agent_ro') THEN CREATE ROLE agent_ro LOGIN; END IF; END $$; REVOKE ALL ON DATABASE plant FROM agent_ro; GRANT CONNECT ON DATABASE plant TO agent_ro; REVOKE ALL ON SCHEMA public FROM agent_ro; GRANT USAGE ON SCHEMA public TO agent_ro; REVOKE ALL ON ALL TABLES IN SCHEMA public FROM agent_ro; GRANT SELECT ON ALL TABLES IN SCHEMA public TO agent_ro; -- Applies to tables created later, including the fixture tables. ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO agent_ro; -- No sequences, no functions, no temp tables, no schema creation. REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM agent_ro; REVOKE TEMPORARY ON DATABASE plant FROM agent_ro; REVOKE CREATE ON SCHEMA public FROM agent_ro; -- --- cube_rw — Cube. Read reference data, own cube_preagg. ------------------- DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'cube_rw') THEN CREATE ROLE cube_rw LOGIN; END IF; END $$; GRANT CONNECT ON DATABASE plant TO cube_rw; GRANT USAGE ON SCHEMA public TO cube_rw; GRANT SELECT ON ALL TABLES IN SCHEMA public TO cube_rw; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO cube_rw; GRANT ALL ON SCHEMA cube_preagg TO cube_rw; ALTER SCHEMA cube_preagg OWNER TO cube_rw; -- agent_ro reads pre-aggregations but never writes them. GRANT USAGE ON SCHEMA cube_preagg TO agent_ro; ALTER DEFAULT PRIVILEGES FOR ROLE cube_rw IN SCHEMA cube_preagg GRANT SELECT ON TABLES TO agent_ro; -- ============================================================================= -- Phase 1 gate — verify, do not assume. As agent_ro: -- -- SELECT count(*) FROM equipment; -- must work -- INSERT INTO equipment VALUES ('X'); -- must be REJECTED -- CREATE TABLE t (i int); -- must be REJECTED -- -- An INSERT that succeeds here is a Phase 1 failure, not a detail to fix later. -- The API's SQL allow-list in guardrails.py is the second line of defence, not -- the first; this role is the first. -- =============================================================================