You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
WorkspaceFeatures.publicAttributionEnabled — workspace-toggleable, default OFF. Gates the profile's peer + public tiers from feat(webapp,server): practice-centric profile with audience-aware rendering #1273. When OFF, no workspace_peer or public viewer of any contributor's profile sees attribution content; the API endpoint filters server-side.
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
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
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
DashboardEndpointFlagEnforcementIT — every endpoint, every flag state, 404-when-off.
LeaderboardEndpointDataLeakRegressionTest — explicit reproduction of the pre-fix bug, asserting it no longer reproduces.
An ArchUnit test asserting the @RequireWorkspaceFeature annotation is mandatory on the controller methods.
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).
Part of #1206.
What ships
Server-side enforcement of feature flags on every dashboard endpoint, fixing the existing data-leak bug: today
WorkspaceFeatures.leaderboardEnabledis 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:
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.WorkspaceFeatures.publicAttributionEnabled— workspace-toggleable, default OFF. Gates the profile's peer + public tiers from feat(webapp,server): practice-centric profile with audience-aware rendering #1273. When OFF, noworkspace_peerorpublicviewer of any contributor's profile sees attribution content; the API endpoint filters server-side.WorkspaceFeatures.instructorCohortEnabled— platform-default-on for workspace admin / maintainer role. No workspace toggle in v1; the role gate is the access control. The cohort endpoint from feat(webapp,server): instructor cohort dashboard with distributions and flags #1274 is gated by this flag.Why
The current behavior is a privacy bug: the webapp hides the leaderboard when
leaderboardEnabled=false, butGET /api/workspaces/{slug}/leaderboardreturns 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.leaderboardEnabledis removed from the codebase; the build succeeds; no remaining import references the deleted flagWorkspaceFeatures.publicAttributionEnabledexists (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 offWorkspaceFeatures.instructorCohortEnabledexists (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)/api/workspaces/{slug}/leaderboardendpoint with the flag off and asserts the 404 responsepublicAttributionEnabledAND 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)/api/workspaces/{slug}/cohortand the peer / public profile fields is gated by an explicit@RequireWorkspaceFeature(...)annotation or equivalent; bypassing the gate fails the buildleaderboardEnabled(no migration is required because the existing flag is being retired in concert with the deletion of its underlying machinery)Tests to write
DashboardEndpointFlagEnforcementIT— every endpoint, every flag state, 404-when-off.ProfileApiPeerPublicTierFilteringIT— both-gates-required test composing with feat(webapp,server): practice-centric profile with audience-aware rendering #1273 + feat(server,webapp): per-user per-workspace public-attribution opt-in #1275.LeaderboardEndpointDataLeakRegressionTest— explicit reproduction of the pre-fix bug, asserting it no longer reproduces.@RequireWorkspaceFeatureannotation is mandatory on the controller methods.Implementation notes
@RequireWorkspaceFeature(WorkspaceFeatures.X)annotation is a new SpringHandlerMethodArgumentResolver/ 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.RFC 7807problem detail); intentionally indistinguishable from an unknown route, by design.publicAttributionEnableddefaults 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 hadleaderboardEnabled=truedo not automatically getpublicAttributionEnabled=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 propertyhephaestus.profile.instructor-cohort.enabled(theWorkspaceFeaturesenum reads from this property at evaluation time).leaderboardEnabledflag's column inWorkspace/workspace_featuresis 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.