Skip to content

Require authentication on all rag-analytics routes - #208

Open
collinschreyer-dev wants to merge 2 commits into
devfrom
fix/rag-analytics-auth
Open

Require authentication on all rag-analytics routes#208
collinschreyer-dev wants to merge 2 commits into
devfrom
fix/rag-analytics-auth

Conversation

@collinschreyer-dev

@collinschreyer-dev collinschreyer-dev commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

What

23 of the 24 /api/rag-analytics/* routes were registered with no middleware. SRT authenticates per route rather than through a blanket middleware, so these were reachable unauthenticated. The handlers do no authorization of their own.

Must merge together with GSA/srt-ui#191. On its own, this change breaks the analytics pages.

Why it matters beyond analytics reads

Endpoint Unauthenticated capability
POST /stages, DELETE /stages/:id write and delete pipeline stage config
POST /pipelines, DELETE /pipelines/:id write and delete pipeline templates
POST /playground/execute-pipeline, /execute-stage run LLM pipelines
POST /playground/test-completion, /test-embeddings invoke model and embedding backends
GET /agency-leaderboard, /tri-state, /posture, ... read cross-agency analytics

So this was an anonymous config-write and model-spend surface, not only a data read.

Guards chosen

Matched to what the UI already enforces client-side in app.routing.ts:

  • token(), admin_only() for 21 routes. Every analytics/* page is behind AdminGuardFn.
  • token() only for playground/analyze and playground/package-synthesis. Both are called by the regular home page, which is behind AuthGuardFn. Requiring admin would break the normal user workflow.
  • art-lookup already had token() and is unchanged.

Root cause

art-lookup was the only guarded route and also the only one called via Angular HttpClient, which attaches a bearer token through TokenInterceptor. Every unguarded route was called with raw fetch(), which bypasses the interceptor. Adding a guard would have 401'd them, so none was added.

Verification

  • All 24 routes now carry a guard; zero unguarded remain.
  • node --check passes.
  • No tests reference these endpoints.
  • No other service calls them. manual_upload_pipeline matches are an April reference bundle, not a deployed service.

🤖 Generated with Claude Code

23 of the 24 /api/rag-analytics/* routes were registered with no middleware at
all. SRT applies authentication per route rather than through a blanket
middleware, and these were never given any, so they were reachable
unauthenticated from the internet. The handlers perform no authorization of
their own.

This exposed more than analytics reads. saveStage, deleteStage, savePipeline and
deletePipeline are unauthenticated writes to pipeline configuration, and
execute-pipeline, execute-stage, test-completion, test-embeddings,
generate-prompt and package-synthesis all invoke LLM and embedding backends,
so an anonymous caller could both alter pipeline config and spend model budget.

Guards follow what the UI already enforces on the client side. Every analytics
page is behind AdminGuardFn in app.routing.ts, so those routes get
token(), admin_only(). The two endpoints the regular home page calls,
playground/analyze and playground/package-synthesis, get token() only, since
requiring admin there would break the normal user workflow.

The cause of the gap is visible in how the calls are written. art-lookup was the
one guarded route and the one called through Angular HttpClient, which attaches
a bearer token via TokenInterceptor. Every unguarded route was called with raw
fetch(), which bypasses the interceptor. The companion srt-ui change attaches
the header explicitly at those call sites.

Verified no tests and no other services call these endpoints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both declared 2048M while the running apps were on 4G, so the file had drifted
from reality. The image is around 3.3G, mostly the Python virtualenv, and does
not unpack inside 2048M.

Pushing production with its own manifest therefore lowered the quota below what
the image needs and crash-looped the app:

  uncompressed layer size exceeds quota
  disk limit is smaller than volume size

That took the production API down for roughly two minutes on 2026-09-01 until
the quota was restored with cf scale. Staging carried the same fault and was
only spared because it had been deployed with cf restage, which does not apply
the manifest.

Both now declare 4096M, matching dev and matching what the apps actually run on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant