Skip to content

fix(server): server-side feature-flag enforcement on dashboard endpoints #1277

@FelixTJDietrich

Description

@FelixTJDietrich

Part of #1206.

What ships

Server-side enforcement of feature flags on every dashboard endpoint, fixing the existing data-leak bug: today WorkspaceFeatures.leaderboardEnabled is consulted only by the webapp; the API endpoints (/api/workspaces/{slug}/leaderboard, the user-activity counters) return data regardless of the flag, so a scraper can pull the full ranking for any workspace by hitting the API directly.

The fix:

  • Retire WorkspaceFeatures.leaderboardEnabled. The flag's scope was overloaded (it nominally gated the leaderboard surface but in practice controlled four unrelated UI fragments); splitting the responsibilities is the correct fix.
  • Introduce two replacement flags:
  • The profile's self + instructor tiers default-on and platform-fixed (not flag-gated); the role gate is the access control. These are documented as such.
  • Every dashboard endpoint enforces its flag with a 404 response when the flag is off (not 403, not empty list — 404 because the endpoint conceptually does not exist for a workspace that has not enabled the feature).

Why

The current behavior is a privacy bug: the webapp hides the leaderboard when leaderboardEnabled=false, but GET /api/workspaces/{slug}/leaderboard returns the full ranking regardless. Anyone aware of the API surface can scrape it. Fixing this is non-negotiable; doing it as part of the leaderboard transformation lets us split the overloaded flag into the two scopes the post-state actually needs.

The 404 (not 403, not empty) response is the right shape because feature-flagging is an existence question, not an authorization question — for a workspace that has not enabled publicAttributionEnabled, the peer-attribution endpoint does not exist; saying "you can't access this" leaks that the feature is available somewhere, which it is not.

Acceptance criteria

  • WorkspaceFeatures.leaderboardEnabled is removed from the codebase; the build succeeds; no remaining import references the deleted flag
  • WorkspaceFeatures.publicAttributionEnabled exists (workspace-toggleable, default OFF); the workspace admin UI surfaces a toggle; an integration test exercises the toggle + asserts the profile API filters peer + public tier content when off
  • WorkspaceFeatures.instructorCohortEnabled exists (platform-default-on, role-gated); the cohort endpoint from feat(webapp,server): instructor cohort dashboard with distributions and flags #1274 returns 404 when the flag is off (operator-level kill switch, not workspace-level toggle in v1)
  • Every dashboard endpoint returns 404 (not empty list, not 403) when its enabling flag is off; an integration test scrapes the previously-leaky /api/workspaces/{slug}/leaderboard endpoint with the flag off and asserts the 404 response
  • The profile API from feat(webapp,server): practice-centric profile with audience-aware rendering #1273 consults publicAttributionEnabled AND the per-user preference from feat(server,webapp): per-user per-workspace public-attribution opt-in #1275 for peer + public tiers; an integration test asserts both gates are required (turning either off hides the tier)
  • An ArchUnit test asserts every controller method exposed under /api/workspaces/{slug}/cohort and the peer / public profile fields is gated by an explicit @RequireWorkspaceFeature(...) annotation or equivalent; bypassing the gate fails the build
  • A regression test for the original data-leak bug exercises a known-pre-leak API path with the new flag off and asserts no leak; the test is named such that an operator searching for the bug class finds it
  • CHANGELOG entry under "Security" calls out the fix + the upgrade path for workspaces that previously relied on leaderboardEnabled (no migration is required because the existing flag is being retired in concert with the deletion of its underlying machinery)

Tests to write

Implementation notes

  • The @RequireWorkspaceFeature(WorkspaceFeatures.X) annotation is a new Spring HandlerMethodArgumentResolver / interceptor that resolves the workspace from the path variable + checks the flag pre-handler-invocation. A method missing the annotation that should have it is caught by the ArchUnit rule.
  • The 404 response shape matches Spring's default 404 (no body, plain RFC 7807 problem detail); intentionally indistinguishable from an unknown route, by design.
  • publicAttributionEnabled defaults OFF for every existing workspace at migration time; chore(server): drop leaderboard columns and backfill profile preferences #1278's migration writes the default explicitly. Workspaces that previously had leaderboardEnabled=true do not automatically get publicAttributionEnabled=true — the existing flag's scope did not imply consent for the peer + public tier rendering this epic introduces.
  • instructorCohortEnabled's default-on posture is platform-policy, not workspace-toggle. A workspace admin cannot disable the cohort dashboard for their workspace in v1; the operator can disable it globally via a Spring property hephaestus.profile.instructor-cohort.enabled (the WorkspaceFeatures enum reads from this property at evaluation time).
  • The retired leaderboardEnabled flag's column in Workspace / workspace_features is dropped by chore(server): drop leaderboard columns and backfill profile preferences #1278's migration; this sub-issue removes the Java reader so the column is orphaned before drop.

Dependencies

Depends on #1273. Depends on #1274. Depends on #1275. Blocks #1278.

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databasebreakingIntroduces breaking API or behavior changesbugSomething isn't workingpriority:criticalDrop everything - Loss of functionality or datasecurityAuthentication, authorization, vulnerability fixes

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions