From 3fcff8e4f75b36568aa401c70835e0b0de07fe2c Mon Sep 17 00:00:00 2001 From: david kiania Date: Wed, 17 Jun 2026 11:04:41 +0300 Subject: [PATCH] feat(access): expose tickets + fuel schemas to analytics_ro (read-only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analytics_ro role only had USAGE/SELECT on reporting + tracksolid, so the tickets schema (INC/CRQ, 8 tables + 1 view + 7 fns) and fuel schema were invisible to the MCP server — queries failed with permission denied. - analytics_ro_role.sql: GRANT USAGE/SELECT/EXECUTE on tickets + fuel. Default privileges for these are keyed to postgres (their owner), not tracksolid_owner, so future objects auto-grant correctly. - analytics_mcp.py: READABLE_SCHEMAS now includes tickets + fuel and is overridable via MCP_READABLE_SCHEMAS, so the introspection helpers (list_tables/describe_table/sample_table) work for them too. - deploy.sh: reuse existing analyst tokens from the running container when MCP_AUTH_TOKENS is unset, so a code-only redeploy needs no secret. Co-Authored-By: Claude Opus 4.8 --- analytics_mcp.py | 10 +++++++++- deploy.sh | 9 +++++++++ scripts/analytics_ro_role.sql | 17 +++++++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/analytics_mcp.py b/analytics_mcp.py index 0b8170e..ef2fce7 100644 --- a/analytics_mcp.py +++ b/analytics_mcp.py @@ -64,7 +64,15 @@ log = _get_logger("server") DATABASE_URL = os.environ["DATABASE_URL"] # analytics_ro DSN (set by deploy) MAX_ROWS_CEIL = int(os.getenv("MCP_MAX_ROWS", "10000")) -READABLE_SCHEMAS = ("reporting", "tracksolid") +# Schemas the introspection helpers (list_tables/describe_table/sample_table) expose. +# Override with MCP_READABLE_SCHEMAS="reporting,tracksolid,tickets,fuel" — these must +# stay in sync with the GRANTs in scripts/analytics_ro_role.sql. The raw query() tool +# is bounded by the analytics_ro role's GRANTs, not by this list. +READABLE_SCHEMAS = tuple( + s.strip() for s in os.getenv( + "MCP_READABLE_SCHEMAS", "reporting,tracksolid,tickets,fuel" + ).split(",") if s.strip() +) # ── Read-only connection pool ──────────────────────────────────────────────── # Force read-only + a statement timeout at the connection level (belt + braces; diff --git a/deploy.sh b/deploy.sh index db75239..79ff6d1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -26,6 +26,15 @@ PORT=8892 HOST_DOMAIN="${HOST_DOMAIN:-fleetmcp.fivetitude.com}" # prod: fleetmcp.rahamafresh.com IMAGE="fleetanalytics-mcp:latest" ENV_FILE="$(pwd)/.deploy.env" + +# Per-analyst Bearer tokens. For a CODE-ONLY redeploy you can omit MCP_AUTH_TOKENS: +# we reuse the tokens from the currently running container so existing analysts keep +# working and no secret has to be re-typed or printed. Only set MCP_AUTH_TOKENS when +# you are adding/rotating/revoking a token. +if [ -z "${MCP_AUTH_TOKENS:-}" ]; then + MCP_AUTH_TOKENS="$(docker inspect "$NAME" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sed -n 's/^MCP_AUTH_TOKENS=//p' | head -1)" + [ -n "$MCP_AUTH_TOKENS" ] && echo "Reusing existing analyst tokens from running $NAME container." +fi : "${MCP_AUTH_TOKENS:?set MCP_AUTH_TOKENS=name:token[,name:token...] before running (per-analyst Bearer tokens)}" # Resolve the network + DB DSN from the running webhook_receiver (it sits on the diff --git a/scripts/analytics_ro_role.sql b/scripts/analytics_ro_role.sql index 35c0ba4..0692b59 100644 --- a/scripts/analytics_ro_role.sql +++ b/scripts/analytics_ro_role.sql @@ -33,19 +33,28 @@ END $role$; ALTER ROLE analytics_ro WITH LOGIN PASSWORD :'ro_pw'; GRANT CONNECT ON DATABASE tracksolid_db TO analytics_ro; -GRANT USAGE ON SCHEMA reporting, tracksolid TO analytics_ro; +GRANT USAGE ON SCHEMA reporting, tracksolid, tickets, fuel TO analytics_ro; GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO analytics_ro; -- tables + views GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid TO analytics_ro; -- tables + views +GRANT SELECT ON ALL TABLES IN SCHEMA tickets TO analytics_ro; -- INC/CRQ tickets: tables + views +GRANT SELECT ON ALL TABLES IN SCHEMA fuel TO analytics_ro; -- fuel: tables + views GRANT SELECT ON reporting.v_trips TO analytics_ro; -- MATERIALIZED VIEW (not in ALL TABLES) GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO analytics_ro; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA tickets TO analytics_ro; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA fuel TO analytics_ro; --- "dynamic": future objects created by the migration role (tracksolid_owner) --- are auto-granted. NOTE: matviews are still never covered — a new matview needs --- its own explicit GRANT SELECT (as above for v_trips). +-- "dynamic": future objects are auto-granted. reporting/tracksolid are created by the +-- migration role (tracksolid_owner); tickets/fuel are owned by postgres, so their +-- default-privilege grants must be keyed to postgres or new objects won't be readable. +-- NOTE: matviews are still never covered — a new matview needs its own explicit GRANT. ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA reporting GRANT SELECT ON TABLES TO analytics_ro; ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA tracksolid GRANT SELECT ON TABLES TO analytics_ro; ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA reporting GRANT EXECUTE ON FUNCTIONS TO analytics_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tickets GRANT SELECT ON TABLES TO analytics_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fuel GRANT SELECT ON TABLES TO analytics_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tickets GRANT EXECUTE ON FUNCTIONS TO analytics_ro; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fuel GRANT EXECUTE ON FUNCTIONS TO analytics_ro; -- Extra hardening over dashboard_ro: this role serves ad-hoc HUMAN queries via the -- MCP server, so pin read-only at the role level and cap runaway work. These are