Enforce authorization on every endpoint (default-deny + complete @PreAuthorize coverage)#2521
Merged
Conversation
…allow-list Switch the catch-all SecurityFilterChain from permitAll to default-deny. Only /info, /auth/providers and /i18n/** remain anonymous (login page needs them). Token-less requests are now rejected with 401.
Class-level @PreAuthorize broke Spring Batch user-import tasklets, which call UserImportService interface methods as SYSTEM_USER. Annotate only the HTTP handler methods so background jobs are unaffected.
Add @PreAuthorize("isPermitted('admin:cache')") to CDMResultsService#clearCache (POST /cdmresults/clearCache) and remove the satisfied TODO comment. Add postAsLimitedUser helper and limitedUserDeniedGlobalCdmResultsCacheClear test to SourceAccessIT to assert 403 for non-admin users.
…ethods AchillesCacheTasklet (batch job, no security context) now calls getRawTreeMap instead of the @PreAuthorize-gated getTreemap. ConceptSetService now calls the new executeIdentifierLookupInternal instead of the gated overload.
@PreAuthorize SpEL reads public fields on WebApiSecurityExpressionRoot (READ/WRITE/SOURCE/...) and the AccessType/EntityType enums reflectively. Register them via RuntimeHintsRegistrar so AOT-processed runtimes keep the types reflection-reachable and authorization evaluation works. No-op on a standard JVM.
…n auth provider Default-deny rejects token-less requests with 401, which makes WebAPI unusable when no auth provider is configured. Add security.anonymousAccess.enabled (default true): when on, token-less requests are served as the built-in anonymous user instead of 401; set false for strict default-deny. Only the web-layer catch-all changes (permitAll vs authenticated). Per-endpoint @PreAuthorize is unchanged, so authorization is still evaluated against the anonymous user's roles plus defaultGlobalReadPermissions — anonymous gets reads, admin/write stay gated. Not a blanket bypass. AnonymousAccessIT (flag on): unprivileged anonymous reaches ungated endpoints (200) but is denied gated endpoints (403). SecurityIT pins the flag off and still proves token-less requests get 401.
Closes the defense-in-depth gap where 116 HTTP handlers had no method-level
authorization, relying solely on the filter chain. Gates 105 of them with
existing 3.0 permissions; 11 auth/bootstrap endpoints (login flows, /info,
/auth/providers, /i18n) intentionally stay open.
Mapping (existing permissions only, matching each controller's gated siblings
and 2.15 intent):
- source data (vocabulary default-source overloads, evidence, source reads,
ddl, sqlrender) -> isAnyPermitted(anyOf('read:source','write:source'))
(+ hasSourceAccess(#sourceKey, READ) where a sourceKey is present)
- per-domain list/check/byTags/print -> read:<domain> (cohort-definition,
conceptset, cohort-characterization, pathway, feature-analysis, reusable)
- entity read-by-id -> isOwner/isAnyPermitted(read|write)/hasEntityAccess
- user/role/permission reads -> admin:security; source refresh -> admin:source
- tag/tool/job reads -> read (no domain-specific permission exists in 3.0)
- self-scoped (/user/me, notifications) -> isAuthenticated()
Only @PreAuthorize annotations (and the PreAuthorize import) were added; no
method bodies changed. Updates AnonymousAccessIT, whose /cohortdefinition
example is now gated, to assert anonymous reaches method security (403) with
the flag on versus 401 at the filter under default-deny.
The generation endpoints (CC and pathway) were gated with an expression that passed the path's #generationId to isOwner/hasEntityAccess as if it were the parent entity id — but a generation id and an entity id are different id spaces, making those terms semantically wrong (and able to block a legitimate entity-owner who lacks the global read permission). Each of these endpoints already enforces the correct authorization in-body via checkGenerationReadAccess/checkGenerationWriteAccess, which resolves the generation to its real parent entity and checks owner/permission/entity-access plus source. Replace the coarse gate with isAuthenticated() so it only blocks anonymous and defers the precise per-generation decision to the authoritative in-body check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens WebAPI authorization so no endpoint is reachable without an explicit check. Before this PR, the API filter chain was effectively permit-all and several handlers were without per-operation authorization.
Changes
security.anonymousAccess.enabled(defaulttrue) decides how token-less requests are handled: admitted as the anonymous user, then denied by per-endpoint@PreAuthorizeon protected endpoints@PreAuthorizeacross the API, using existing 3.0 permissions:admin:cache,admin:tags,admin:tools,admin:security(user/role/permission + user-import),admin:source, admin for statisticsread:source/hasSourceAccesson cdmresults, cohort-sample, vocabulary, evidence, ddl, sqlrenderread:<x>, entity owner/access expressions for cohort, concept-set, characterization, pathway, feature-analysis, reusablereadfor tag/tool/job reads;isAuthenticated()for self-scoped endpoints (/user/me, notifications)