Skip to content

Require authentication for Forge analytics report endpoint#15814

Open
jamesfredley wants to merge 2 commits into
8.0.xfrom
fix/forge-analytics-security
Open

Require authentication for Forge analytics report endpoint#15814
jamesfredley wants to merge 2 commits into
8.0.xfrom
fix/forge-analytics-security

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

The Forge analytics POST /analytics/report endpoint previously accepted unauthenticated writes, allowing anyone to inject fabricated project-generation statistics into the analytics database.

Changes

  • Authenticate POST /analytics/report with a Google-issued bearer JWT, validated by:
  • Public read endpoints (GET /top/*) remain explicitly anonymous (@secured(IS_ANONYMOUS)).
  • HSTS response filter added to the Forge web (Netty) app, enabled by default and configurable.
  • Deploy wiring: Cloud Run deploy workflows now set GRAILS_FORGE_ANALYTICS_AUDIENCE on both caller and receiver and compute GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT from the deployed service account; README documents the new env vars.

Tests

  • AnalyticsControllerSecuritySpec (no Docker): unauthenticated POST -> 401, wrong-audience token -> 401, wrong-caller-subject token -> 401, public GET not rejected as unauthenticated.
  • HstsHeaderFilterSpec: verifies the Strict-Transport-Security header is applied.
  • StoreGeneratedProjectStatsSpec updated to send an authorized bearer token.

Both focused specs pass locally.

The Forge analytics `POST /analytics/report` endpoint previously accepted
unauthenticated writes, letting anyone inject fabricated project-generation
statistics. Secure it with a Google-issued bearer JWT that is validated by
signature (Google JWKS), issuer, audience, and a caller-subject claim pinned
to the Forge web service account's unique id, so only the intended caller can
report analytics. Public read endpoints (`GET /top/*`) stay anonymous.

Also add an HSTS response filter to the Forge web (Netty) app and wire the
analytics audience and caller-subject env vars through the Cloud Run deploy
workflows and README.

Assisted-by: claude-code:claude-opus-4-8
Copilot AI review requested due to automatic review settings July 2, 2026 22:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the Grails Forge analytics ingestion path by requiring authenticated writes to POST /analytics/report (preventing unauthenticated injection of fabricated stats), while keeping the “top/*” read endpoints publicly accessible. It also adds an HSTS response filter to the Forge web Netty service and wires new env vars through Cloud Run deploy workflows and documentation.

Changes:

  • Require bearer JWT authentication for POST /analytics/report, and explicitly mark GET /analytics/top/* endpoints as anonymous.
  • Add an (enabled-by-default, configurable) HSTS response filter to the Forge web Netty app, with a new spec verifying the header.
  • Update deploy workflows + README to provide GRAILS_FORGE_ANALYTICS_AUDIENCE and GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
grails-forge/README.md Documents new analytics auth env vars for Forge + analytics services.
grails-forge/grails-forge-web-netty/src/test/groovy/org/grails/forge/netty/security/HstsHeaderFilterSpec.groovy Adds test coverage verifying the HSTS header is applied.
grails-forge/grails-forge-web-netty/src/main/resources/application.yml Introduces HSTS config defaults and passes analytics audience config to the GCP HTTP client auth config.
grails-forge/grails-forge-web-netty/src/main/java/org/grails/forge/netty/security/HstsHeaderFilter.java Adds a global response filter that sets Strict-Transport-Security when enabled.
grails-forge/grails-forge-web-netty/build.gradle Adds Groovy/Spock test dependencies needed for the new Spock spec.
grails-forge/grails-forge-analytics-postgres/src/test/groovy/org/grails/forge/analytics/postgres/StoreGeneratedProjectStatsSpec.groovy Updates integration-style test to send an authorized bearer token.
grails-forge/grails-forge-analytics-postgres/src/test/groovy/org/grails/forge/analytics/postgres/AnalyticsControllerSecuritySpec.groovy Adds security-focused tests for rejecting unauthenticated/invalid tokens and allowing anonymous reads.
grails-forge/grails-forge-analytics-postgres/src/main/resources/application.yml Enables bearer JWT auth + Google JWKS verification config and configures caller subject pinning.
grails-forge/grails-forge-analytics-postgres/src/main/java/org/grails/forge/analytics/postgres/AnalyticsController.java Secures the report endpoint and explicitly allows anonymous access to read endpoints.
grails-forge/grails-forge-analytics-postgres/src/main/java/org/grails/forge/analytics/postgres/AnalyticsCallerJwtClaimsValidator.java Adds a custom JWT claims validator to pin sub to the expected caller subject.
grails-forge/grails-forge-analytics-postgres/build.gradle Adds Micronaut JWT security dependency and Groovy/Spock test dependencies.
.github/workflows/forge-deploy-snapshot.yml Wires GRAILS_FORGE_ANALYTICS_AUDIENCE and sets GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT for analytics deployment.
.github/workflows/forge-deploy-release.yml Wires GRAILS_FORGE_ANALYTICS_AUDIENCE and sets GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT for analytics deployment (latest + versioned).
.github/workflows/forge-deploy-prev.yml Wires GRAILS_FORGE_ANALYTICS_AUDIENCE and sets GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT for analytics deployment.
.github/workflows/forge-deploy-prev-snapshot.yml Wires GRAILS_FORGE_ANALYTICS_AUDIENCE and sets GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT for analytics deployment.
.github/workflows/forge-deploy-next.yml Wires GRAILS_FORGE_ANALYTICS_AUDIENCE and sets GRAILS_FORGE_ANALYTICS_CALLER_SUBJECT for analytics deployment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +133 to +143
@MockBean(ApplicationRepository)
@NonNull
ApplicationRepository applicationRepository() {
Mock(ApplicationRepository)
}

@MockBean(JdbcOperations)
@NonNull
JdbcOperations jdbcOperations() {
Mock(JdbcOperations)
}
Comment on lines +81 to +89
void 'top analytics data is not rejected as unauthenticated'() {
when:
client.toBlocking().exchange(HttpRequest.GET('/top/features'), String)

then:
HttpClientResponseException e = thrown(HttpClientResponseException)
e.status == HttpStatus.INTERNAL_SERVER_ERROR
0 * applicationRepository._
}
}

@Get("/top/features")
@Secured(SecurityRule.IS_ANONYMOUS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused - these indicate they're anonymous but your'e requiring authentication?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesfredley isn't the point of this to ensure we're authenticated? Shouldn't all of these IS_ANONYMOUS be removed for the analytics endpoint?

Assisted-by: opencode:gpt-5.5
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 49.3657%. Comparing base (66cd24c) to head (fa5214f).
⚠️ Report is 5488 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15814        +/-   ##
==================================================
- Coverage     49.4842%   49.3657%   -0.1185%     
- Complexity      16697      16782        +85     
==================================================
  Files            1947       1986        +39     
  Lines           92474      93336       +862     
  Branches        16152      16337       +185     
==================================================
+ Hits            45760      46076       +316     
- Misses          39606      40135       +529     
- Partials         7108       7125        +17     

see 171 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Jul 9, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: fa5214f
▶️ Tests: 9499 executed
⚪️ Checks: 59/59 completed


Learn more about TestLens at testlens.app.

@jdaugherty

Copy link
Copy Markdown
Contributor

The tests are failing to authenticate:

AnalyticsControllerSecuritySpec > top analytics data is not rejected as unauthenticated FAILED
io.micronaut.http.client.exceptions.HttpClientResponseException at DefaultHttpClient.java:2177

StoreGeneratedProjectStatsSpec > test save generation data FAILED
io.micronaut.http.client.exceptions.HttpClientResponseException at DefaultHttpClient.java:2177

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants