Skip to content

Wire HMAC bearer auth + JIT tenant provisioning save flow into dotCMS analytics proxy #35775

@swicken

Description

@swicken

Description

Harden the dotCMS-side analytics proxy and credential flow so the system can talk to the dot-ca-event-manager microservice with per-tenant HMAC bearer tokens instead of plain Basic auth, and so backend authorization is consistent with the rest of dotCMS.

Parent epic: #35048 — Phase 3 (Security) work, with some Phase 4 (Reliability) overlap (proxy/health timeouts, structured failure modes).

What changes

Auth model

  • Replace Basic auth on the proxy with bearer tokens; remove DOT_ANALYTICS_PASSWORD entirely.
  • New ContentAnalyticsAppListener: when an admin saves the Content Analytics App with adminPassword populated, the listener exchanges the password for a bearer token via POST {DOT_ANALYTICS_BASE_URL}/v1/admin/token, persists the returned token as a hidden secret on the app config, and atomically clears the user-entered password. The password is never persisted.
  • App YAML grows new fields (adminPassword, bearerToken) with hints explaining the save-flow exchange.

Proxy hardening

  • Path allowlist: only event/* may be proxied. Traversal segments, encoded ?/#, and non-event prefixes are rejected with 400.
  • Hard timeouts: 10s proxy / 2s health probe — prevents a slow upstream from exhausting the Jersey thread pool.
  • Blank-body / null JSON guard on the ingest POST.
  • project= query param deduped: caller-supplied wins over DOT_ANALYTICS_PROJECT.
  • RFC-6750-compliant: when no token is available, the Authorization header is omitted entirely instead of emitting a malformed Bearer (no token).
  • Empty-body POSTs drop Content-Type (per RFC 9110 §8.3).

Per-site permission gating on the GET catch-all

  • Site is resolved as the logged-in user via HostAPI.find(siteId, user, …) and HostWebAPI.getCurrentHost(request, user), so PermissionAPI gates access.
  • A backend user without READ on the requested site gets 403 SITE_ACCESS_DENIED even when crafting ?siteId= by hand to bypass the (already-permissioned) site picker.
  • SecurityLogger.logInfo is called on the 403 path to mirror what the global DotForbiddenExceptionMapper would have logged — keeps denied-site accesses in the audit trail.

Health monitor

  • Drops DOT_ANALYTICS_PASSWORD from the configuration check.
  • Null-safe site probe (monitor pings landing on System Host no longer NPE).
  • Tighter /v1/health timeout so monitor probes fail fast.

Tests

  • ContentAnalyticsAppListenerTest: failure paths (missing tenant / missing base URL clear the password and notify the user) plus a direct happy-path test of persistTokenAndClearCredentials that asserts the atomic invariant — adminPassword absent, bearerToken replaced with the new value, unrelated secrets (siteAuth, etc.) preserved verbatim.
  • EventAnalyticsProxyHelperTest: path allowlist (accept/reject matrix including traversal, smuggled ?/#, non-event prefixes), buildUpstreamUrl (trailing-slash strip, project dedup, query forwarding), and buildAuthHeader covering the four security-critical branches (per-site wins, env fallback with trim, null host, both absent → empty Optional).

Out of scope on this PR

These are intentionally deferred and tracked in .scratch/reviews/feat-analytics-hmac-auth.md:

  • EventAnalyticsProxyResourceTest — resource-level test for the auth-decision branches (admin bypass, READ-permitted 200, READ-denied 403, session fallback). Auth model was in flux during the branch; folded into a follow-up once it settled.
  • Promoting DOT_ANALYTICS_TENANT to a per-site App field — multi-tenant-per-JVM is a real product question, not a refactor.
  • Standardizing on siteId vs host_id query param across analytics endpoints — cross-cutting product decision.
  • wrapUpstreamResponse branch test coverage — correctness branches, not security; lower priority than buildAuthHeader.

Acceptance Criteria

  • HMAC bearer auth wired into the proxy; DOT_ANALYTICS_PASSWORD no longer read anywhere
  • Save flow on Content Analytics App exchanges admin password for bearer token, clears password atomically
  • Proxy path allowlist + hard timeouts + blank-body guard + project= dedup
  • GET catch-all enforces per-site READ via PermissionAPI; non-permitted requests return 403 SITE_ACCESS_DENIED
  • 403 path logs to SecurityLogger for audit-trail parity with the global exception mapper
  • Health monitor no longer requires DOT_ANALYTICS_PASSWORD; null-safe site probe
  • Tests: listener failure paths + persist-token invariant; helper allowlist, URL building, buildAuthHeader security branches
  • Generated openapi.yaml reflects the new endpoint shapes (rebuild on merge via swagger-maven-plugin)

Priority

Medium

Additional Context

  • Branch: feat/analytics-hmac-auth (~16 commits, ~1016 LOC across 10 files)

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

Status

In Review

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions