-- =============================================================================
-- 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.
--
-- Three 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.
--   ingest_rw ingestion. The ONLY role that writes doc_chunks.
--
-- ingest_rw is created here, at Phase 1, and not with the Phase 9 upload tables
-- that also use it. Ingestion has needed a writing role since Phase 3; it just
-- never had one. ai-ingest took PGUSER from api.env, which is agent_ro, so
-- `ai-ingest --all` connected as a SELECT-only role and could not insert a
-- single chunk. Phase 3 was unrunnable as documented.
-- =============================================================================

-- --- 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;
-- Revoking TEMPORARY from the role alone is not enough: Postgres grants TEMP on
-- every database to PUBLIC by default, and agent_ro inherits it. Phase 1 gate
-- caught agent_ro creating a temp table. Revoke it from PUBLIC as well.
-- Affects every non-superuser on plant (agent_ro, cube_rw). postgres is a
-- superuser and is unaffected, so the seed load still works. If Cube ever needs
-- temp tables, grant TEMPORARY back to cube_rw explicitly - never to PUBLIC.
REVOKE TEMPORARY ON DATABASE plant FROM PUBLIC;
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;

-- --- ingest_rw — ingestion. The only role that writes doc_chunks. -----------
-- Used by `ai-ingest` from the command line and, from Phase 9, by
-- ai-docs-worker. Never used by ai-api: the component reachable from the
-- internet is not the component that can put a document chunk in front of an
-- operator.
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'ingest_rw') THEN
        CREATE ROLE ingest_rw LOGIN;
    END IF;
END
$$;

GRANT CONNECT ON DATABASE plant TO ingest_rw;
GRANT USAGE ON SCHEMA public TO ingest_rw;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ingest_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ingest_rw;

-- doc_chunks only. DELETE is required: re-ingesting a file deletes its chunks
-- and reinserts them in one transaction, which is what stops re-runs
-- duplicating. It writes no other table in this schema.
GRANT INSERT, UPDATE, DELETE ON doc_chunks TO ingest_rw;
GRANT USAGE, SELECT ON SEQUENCE doc_chunks_id_seq TO ingest_rw;

REVOKE CREATE ON SCHEMA public FROM ingest_rw;
REVOKE TEMPORARY ON DATABASE plant FROM ingest_rw;

-- =============================================================================
-- 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
--
-- And as ingest_rw, because Phase 3 depends on it:
--
--   INSERT INTO doc_chunks (source_file, doc_type, chunk_text)
--        VALUES ('gate','manual','gate');       -- must SUCCEED
--   DELETE FROM doc_chunks WHERE source_file = 'gate';  -- must SUCCEED
--   INSERT INTO equipment VALUES ('X');         -- must be REJECTED
--
-- An INSERT that succeeds as agent_ro is a Phase 1 failure, not a detail to fix
-- later. An INSERT that FAILS as ingest_rw is the same failure in reverse: it
-- means Phase 3 will not run.
-- The API's SQL allow-list in guardrails.py is the second line of defence, not
-- the first; this role is the first.
-- =============================================================================
