From 8ba8b288c6d60f5b3d31bad5677a875fed67c120 Mon Sep 17 00:00:00 2001 From: collinschreyer-dev Date: Mon, 31 Aug 2026 12:16:14 -0500 Subject: [PATCH 1/2] Require authentication on all rag-analytics routes 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 --- server/app.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/server/app.js b/server/app.js index 1a532c9..4ce2331 100644 --- a/server/app.js +++ b/server/app.js @@ -361,30 +361,30 @@ module.exports = { // Advanced RAG Analytics Routes for Dashboarding const ragAnalyticsRoutes = ragAnalyticsRoutesFactory(pgPool) - app.get('/api/rag-analytics/tri-state', ragAnalyticsRoutes.getTriState) - app.get('/api/rag-analytics/posture', ragAnalyticsRoutes.getPosture) - app.get('/api/rag-analytics/ict-taxonomy', ragAnalyticsRoutes.getIctTaxonomy) - app.get('/api/rag-analytics/document-intelligence', ragAnalyticsRoutes.getDocumentIntelligence) - app.get('/api/rag-analytics/vector-violations', ragAnalyticsRoutes.getVectorViolations) - app.get('/api/rag-analytics/agency-leaderboard', ragAnalyticsRoutes.getAgencyLeaderboard) - - app.get('/api/rag-analytics/playground/status', ragAnalyticsRoutes.getPlaygroundStatus) - app.get('/api/rag-analytics/adhoc-usage', ragAnalyticsRoutes.getAdhocUsage) - app.get('/api/rag-analytics/stages', ragAnalyticsRoutes.listStages) - app.post('/api/rag-analytics/stages', ragAnalyticsRoutes.saveStage) - app.delete('/api/rag-analytics/stages/:stageId', ragAnalyticsRoutes.deleteStage) - app.post('/api/rag-analytics/stages/generate-examples', ragAnalyticsRoutes.generateExamples) - app.get('/api/rag-analytics/pipelines', ragAnalyticsRoutes.listPipelines) - app.post('/api/rag-analytics/pipelines', ragAnalyticsRoutes.savePipeline) - app.delete('/api/rag-analytics/pipelines/:templateId', ragAnalyticsRoutes.deletePipeline) - app.get('/api/rag-analytics/playground/list-models', ragAnalyticsRoutes.listPlaygroundModels) - app.post('/api/rag-analytics/playground/test-completion', ragAnalyticsRoutes.testPlaygroundCompletion) - app.post('/api/rag-analytics/playground/test-embeddings', ragAnalyticsRoutes.testPlaygroundEmbeddings) - app.post('/api/rag-analytics/playground/package-synthesis', ragAnalyticsRoutes.packageSynthesis) - app.post('/api/rag-analytics/playground/execute-pipeline', ragAnalyticsRoutes.executePipeline) - app.post('/api/rag-analytics/playground/execute-stage', ragAnalyticsRoutes.executeStage) - app.post('/api/rag-analytics/playground/generate-prompt', ragAnalyticsRoutes.generatePrompt) - app.post('/api/rag-analytics/playground/analyze', ragAnalyticsRoutes.playgroundAnalyze) + app.get('/api/rag-analytics/tri-state', token(), admin_only(), ragAnalyticsRoutes.getTriState) + app.get('/api/rag-analytics/posture', token(), admin_only(), ragAnalyticsRoutes.getPosture) + app.get('/api/rag-analytics/ict-taxonomy', token(), admin_only(), ragAnalyticsRoutes.getIctTaxonomy) + app.get('/api/rag-analytics/document-intelligence', token(), admin_only(), ragAnalyticsRoutes.getDocumentIntelligence) + app.get('/api/rag-analytics/vector-violations', token(), admin_only(), ragAnalyticsRoutes.getVectorViolations) + app.get('/api/rag-analytics/agency-leaderboard', token(), admin_only(), ragAnalyticsRoutes.getAgencyLeaderboard) + + app.get('/api/rag-analytics/playground/status', token(), admin_only(), ragAnalyticsRoutes.getPlaygroundStatus) + app.get('/api/rag-analytics/adhoc-usage', token(), admin_only(), ragAnalyticsRoutes.getAdhocUsage) + app.get('/api/rag-analytics/stages', token(), admin_only(), ragAnalyticsRoutes.listStages) + app.post('/api/rag-analytics/stages', token(), admin_only(), ragAnalyticsRoutes.saveStage) + app.delete('/api/rag-analytics/stages/:stageId', token(), admin_only(), ragAnalyticsRoutes.deleteStage) + app.post('/api/rag-analytics/stages/generate-examples', token(), admin_only(), ragAnalyticsRoutes.generateExamples) + app.get('/api/rag-analytics/pipelines', token(), admin_only(), ragAnalyticsRoutes.listPipelines) + app.post('/api/rag-analytics/pipelines', token(), admin_only(), ragAnalyticsRoutes.savePipeline) + app.delete('/api/rag-analytics/pipelines/:templateId', token(), admin_only(), ragAnalyticsRoutes.deletePipeline) + app.get('/api/rag-analytics/playground/list-models', token(), admin_only(), ragAnalyticsRoutes.listPlaygroundModels) + app.post('/api/rag-analytics/playground/test-completion', token(), admin_only(), ragAnalyticsRoutes.testPlaygroundCompletion) + app.post('/api/rag-analytics/playground/test-embeddings', token(), admin_only(), ragAnalyticsRoutes.testPlaygroundEmbeddings) + app.post('/api/rag-analytics/playground/package-synthesis', token(), ragAnalyticsRoutes.packageSynthesis) + app.post('/api/rag-analytics/playground/execute-pipeline', token(), admin_only(), ragAnalyticsRoutes.executePipeline) + app.post('/api/rag-analytics/playground/execute-stage', token(), admin_only(), ragAnalyticsRoutes.executeStage) + app.post('/api/rag-analytics/playground/generate-prompt', token(), admin_only(), ragAnalyticsRoutes.generatePrompt) + app.post('/api/rag-analytics/playground/analyze', token(), ragAnalyticsRoutes.playgroundAnalyze) app.post('/api/rag-analytics/art-lookup', token(), ragAnalyticsRoutes.artLookup) app.use(expressWinston.errorLogger({ From acd5cca05a3273be2e5d761fa7ad3d2af01628ec Mon Sep 17 00:00:00 2001 From: collinschreyer-dev Date: Tue, 1 Sep 2026 12:07:42 -0500 Subject: [PATCH 2/2] Correct disk_quota in the staging and prod manifests 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 --- cf/manifest.prod.yml | 10 +++++++++- cf/manifest.staging.yml | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cf/manifest.prod.yml b/cf/manifest.prod.yml index 0835375..4d69863 100644 --- a/cf/manifest.prod.yml +++ b/cf/manifest.prod.yml @@ -2,7 +2,15 @@ applications: - name: srt-api-prod memory: 1024M - disk_quota: 2048M + # The image is ~3.3G, mostly the Python venv, so 2048M is not enough to + + # unpack it. Pushing with a smaller quota than the running app fails with + + # "uncompressed layer size exceeds quota" and crash-loops the app, which is + + # how production went down on 2026-09-01. Keep this in step with reality. + + disk_quota: 4096M # health-check-type: process # don't re-enable....move to port 8080 if you have problems instances: 1 env: diff --git a/cf/manifest.staging.yml b/cf/manifest.staging.yml index 009e858..abfa5ef 100644 --- a/cf/manifest.staging.yml +++ b/cf/manifest.staging.yml @@ -2,7 +2,15 @@ applications: - name: srt-api-staging memory: 1024M - disk_quota: 2048M + # The image is ~3.3G, mostly the Python venv, so 2048M is not enough to + + # unpack it. Pushing with a smaller quota than the running app fails with + + # "uncompressed layer size exceeds quota" and crash-loops the app, which is + + # how production went down on 2026-09-01. Keep this in step with reality. + + disk_quota: 4096M # health-check-type: process # don't re-enable....move to port 8080 if you have problems instances: 1 env: