Class RestSecurityConfig

java.lang.Object
ubic.gemma.rest.security.RestSecurityConfig

@Configuration @EnableWebSecurity public class RestSecurityConfig extends Object
Spring Security 6 configuration for the Gemma RESTful API (/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 token IS_AUTHENTICATED_ANONYMOUSLY which 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, authorizeHttpRequests uses AuthorizationManager rather than AccessDecisionManager; the legacy AccessDecisionManager bean is still defined here because it is consumed by code in gemma-rest (DatasetsWebService, PlatformsWebService, CacheControlHeaderDecorator) that calls accessDecisionManager.decide(...) directly. Once those call sites are migrated to AuthorizationManager, 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, no antMatchers, no authorizeRequests. Spring Security 6 deprecates these in favor of SecurityFilterChain + 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; the SecurityFilterChain bean with the @Order(1) annotation should be applied when both chains coexist).
  • Direct AccessDecisionManager.decide(...) call sites in DatasetsWebService.java:3082, PlatformsWebService.java:318, and CacheControlHeaderDecorator.java reference 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 to AuthorizationManager (Spring 6 idiom).
See Also:
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    org.springframework.security.access.AccessDecisionManager
    httpAccessDecisionManager(org.springframework.security.access.vote.RoleHierarchyVoter roleHierarchyVoter)
    Legacy AccessDecisionManager bean carried over from the gemma-web XML.
    org.springframework.security.web.SecurityFilterChain
    restSecurityFilterChain(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.

    Methods inherited from class Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • 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)
      Legacy AccessDecisionManager bean carried over from the gemma-web XML.

      This is the httpAccessDecisionManager bean defined in gemma-web/applicationContext-security.xml lines 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 ~3082
      • PlatformsWebService.java:87 → used at line ~318
      • CacheControlHeaderDecorator.java:31

      The composition matches the XML exactly: AffirmativeBased (allowIfAllAbstainDecisions=true) over WebExpressionVoter + RoleHierarchyVoter + AuthenticatedVoter. AuthenticatedVoter is what lets IS_AUTHENTICATED_ANONYMOUSLY resolve as an access expression token.

      TODO (post-cutover): migrate the three call sites to Spring Security 6's AuthorizationManager API and remove this bean. AccessDecisionManager is deprecated for removal in Spring Security 7.

      Parameters:
      roleHierarchyVoter - the roleHierarchyVoter bean from gsec (applicationContext-gsec.xml). The @Qualifier pins the lookup to the bean id rather than relying on type-only resolution — gsec defines several AccessDecisionVoter beans.