Class RestSecurityConfig
/rest/v2/**).
Wiring status (Phase 1 of GEMMA_REST_STANDALONE_ROADMAP.md): this
@Configuration class is picked up by gemma-rest's existing
<context:component-scan base-package="ubic.gemma.rest"/>
(gemma-rest/src/main/resources/ubic/gemma/applicationContext-component-scan.xml).
It is therefore active in any Spring root context that loads gemma-rest's
classpath XML โ which today means both gemma-web's WAR boot AND the
gemma-rest standalone WAR boot (the latter activated via
mvn -pl gemma-rest -P gemma-rest-war package; see
gemma-rest/src/main/webapp/WEB-INF/web.xml).
The legacy <s:http pattern="/rest/v2/**"> block in
gemma-web/src/main/resources/ubic/gemma/applicationContext-security.xml
(lines 41-47) coexists with this Java config for now. Removing the XML block
is roadmap ยง8 row 2; until that lands, both definitions register a
SecurityFilterChain for /rest/v2/** and Spring Security
applies them in registration order. The gemma-rest standalone WAR
deliberately omits gemma-web's applicationContext-security.xml from
its classpath, so there only this @Bean contributes the REST chain.
Filter chain summary (translated from the legacy XML)
The legacy XML block was:<s:http access-decision-manager-ref="httpAccessDecisionManager" pattern="/rest/v2/**"
entry-point-ref="restAuthEntryPoint" realm="Gemma RESTful API">
<s:anonymous granted-authority="IS_AUTHENTICATED_ANONYMOUSLY"/>
<s:http-basic entry-point-ref="restAuthEntryPoint"/>
<s:intercept-url pattern="/rest/v2/users/**" access="GROUP_USER"/>
</s:http>
Mapping to Spring Security 6 idioms:
pattern="/rest/v2/**"→HttpSecurity.securityMatcher(String...).<s:intercept-url pattern="/rest/v2/users/**" access="GROUP_USER"/>→.authorizeHttpRequests(auth -> auth.requestMatchers("/rest/v2/users/**").hasAuthority("GROUP_USER").anyRequest().permitAll()). The default rule (anyRequest().permitAll()) corresponds to the legacy "no intercept-url match" behavior — the XML had no fallback<s:intercept-url>inside this chain, and the outer chain in gemma-web had<s:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>which is functionally equivalent to permitAll for our anonymous-authenticated users.<s:http-basic entry-point-ref="restAuthEntryPoint"/>→.httpBasic(basic -> basic.authenticationEntryPoint(restAuthEntryPoint)).entry-point-ref="restAuthEntryPoint"(chain-level entry point) →.exceptionHandling(eh -> eh.authenticationEntryPoint(restAuthEntryPoint)).<s:anonymous granted-authority="IS_AUTHENTICATED_ANONYMOUSLY"/>→.anonymous(anon -> anon.authorities("IS_AUTHENTICATED_ANONYMOUSLY")). Spring Security 6's default anonymous principal is "anonymousUser" with role ROLE_ANONYMOUS; the legacy XML overrode the authority to the marker tokenIS_AUTHENTICATED_ANONYMOUSLYwhich is referenced by access expressions elsewhere in the codebase. We preserve that override here.realm="Gemma RESTful API"→.httpBasic(basic -> basic.realmName("Gemma RESTful API")).access-decision-manager-ref="httpAccessDecisionManager"→ see httpAccessDecisionManager bean below. In Spring Security 6,authorizeHttpRequestsusesAuthorizationManagerrather thanAccessDecisionManager; the legacyAccessDecisionManagerbean is still defined here because it is consumed by code ingemma-rest(DatasetsWebService,PlatformsWebService,CacheControlHeaderDecorator) that callsaccessDecisionManager.decide(...)directly. Once those call sites are migrated toAuthorizationManager, the bean can be removed.
Spring Security 6 idiom choices
- CSRF disabled. This is a stateless REST API authenticated via HTTP Basic. The legacy XML did not configure CSRF explicitly; Spring Security's default (CSRF enabled) was effectively bypassed for the REST chain because no form-bound state existed. Explicitly disable to match the de-facto behavior and conform to REST-API conventions.
- Session creation policy: STATELESS. REST clients pass HTTP Basic
credentials on every request; no server-side session is needed. The legacy
XML did not set
create-session, so the namespace default (ifRequired) applied — this was an oversight, and the conventional REST-API choice (stateless) is the right one. Note this differs from the gemma-web chain (which uses sessions for the form-login flow). - No
WebSecurityConfigurerAdapter, noantMatchers, noauthorizeRequests. Spring Security 6 deprecates these in favor ofSecurityFilterChain+requestMatchers+authorizeHttpRequests.
Open items at cutover time
- The gemma-web outer chain (
<s:http pattern="/**">, lines 51-85) is NOT migrated by this class. It remains in XML for the form-login web UI. Care must be taken at cutover so the/rest/v2/**chain is registered before the catch-all/**chain (Spring Security evaluates filter chains in registration order; theSecurityFilterChainbean with the@Order(1)annotation should be applied when both chains coexist). - Direct
AccessDecisionManager.decide(...)call sites inDatasetsWebService.java:3082,PlatformsWebService.java:318, andCacheControlHeaderDecorator.javareference the bean by type (@Autowired AccessDecisionManager). Once this config is wired and the legacy XML bean is removed, those injections will resolve to the bean defined here. TODO: migrate those sites toAuthorizationManager(Spring 6 idiom).
- See Also:
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionorg.springframework.security.access.AccessDecisionManagerhttpAccessDecisionManager(org.springframework.security.access.vote.RoleHierarchyVoter roleHierarchyVoter) LegacyAccessDecisionManagerbean carried over from the gemma-web XML.org.springframework.security.web.SecurityFilterChainrestSecurityFilterChain(org.springframework.security.config.annotation.web.builders.HttpSecurity http, org.springframework.security.web.AuthenticationEntryPoint restAuthEntryPoint, org.springframework.security.authentication.AuthenticationManager authenticationManager, TokenStore tokenStore) Registered with @Order(1) so this chain is consulted BEFORE the gemma-web XML<s:http pattern="/**">chain.
-
Constructor Details
-
RestSecurityConfig
public RestSecurityConfig()
-
-
Method Details
-
restSecurityFilterChain
@Bean @Order(1) public org.springframework.security.web.SecurityFilterChain restSecurityFilterChain(org.springframework.security.config.annotation.web.builders.HttpSecurity http, @Qualifier("restAuthEntryPoint") org.springframework.security.web.AuthenticationEntryPoint restAuthEntryPoint, @Qualifier("authenticationManager") org.springframework.security.authentication.AuthenticationManager authenticationManager, TokenStore tokenStore) throws Exception Registered with @Order(1) so this chain is consulted BEFORE the gemma-web XML<s:http pattern="/**">chain. Without an explicit order, FilterChainProxy uses bean-registration order โ which is not source-file order โ and the XML `/**` chain was winning for /rest/v2/login. The XML chain has CSRF enabled, so the SPA's POST /rest/v2/login was getting 403s before this @Order(1) was set.- Throws:
Exception
-
httpAccessDecisionManager
@Bean(name="httpAccessDecisionManager") public org.springframework.security.access.AccessDecisionManager httpAccessDecisionManager(@Qualifier("roleHierarchyVoter") org.springframework.security.access.vote.RoleHierarchyVoter roleHierarchyVoter) LegacyAccessDecisionManagerbean carried over from the gemma-web XML.This is the
httpAccessDecisionManagerbean defined ingemma-web/applicationContext-security.xmllines 9-18. It is still needed because three call sites in gemma-rest inject it directly to call.decide(...):DatasetsWebService.java:222→ used at line ~3082PlatformsWebService.java:87→ used at line ~318CacheControlHeaderDecorator.java:31
The composition matches the XML exactly:
AffirmativeBased(allowIfAllAbstainDecisions=true) overWebExpressionVoter+RoleHierarchyVoter+AuthenticatedVoter.AuthenticatedVoteris what letsIS_AUTHENTICATED_ANONYMOUSLYresolve as an access expression token.TODO (post-cutover): migrate the three call sites to Spring Security 6's
AuthorizationManagerAPI and remove this bean.AccessDecisionManageris deprecated for removal in Spring Security 7.- Parameters:
roleHierarchyVoter- theroleHierarchyVoterbean from gsec (applicationContext-gsec.xml). The@Qualifierpins the lookup to the bean id rather than relying on type-only resolution — gsec defines severalAccessDecisionVoterbeans.
-