• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 7366

15 Dec 2025 09:24AM UTC coverage: 36.322% (+0.1%) from 36.219%
7366

push

github

web-flow
Merge pull request #1376 from SpiNNakerManchester/dependabot/maven/spring.boot.version-4.0.0

Bump spring.boot.version from 3.5.7 to 4.0.0

1915 of 5902 branches covered (32.45%)

Branch coverage included in aggregate %.

8 of 9 new or added lines in 1 file covered. (88.89%)

4 existing lines in 1 file now uncovered.

8985 of 24107 relevant lines covered (37.27%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.65
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/security/SecurityConfig.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package uk.ac.manchester.spinnaker.alloc.security;
17

18
import static org.slf4j.LoggerFactory.getLogger;
19
import static org.springframework.beans.factory.config.BeanDefinition.ROLE_APPLICATION;
20
import static org.springframework.beans.factory.config.BeanDefinition.ROLE_SUPPORT;
21
import static org.springframework.http.HttpMethod.GET;
22
import static org.springframework.http.MediaType.APPLICATION_JSON;
23
import static uk.ac.manchester.spinnaker.alloc.security.AppAuthTransformationFilter.clearToken;
24
import static uk.ac.manchester.spinnaker.alloc.security.Utils.installInjectableTrustStoreAsDefault;
25
import static uk.ac.manchester.spinnaker.alloc.security.Utils.loadTrustStore;
26
import static uk.ac.manchester.spinnaker.alloc.security.Utils.trustManager;
27
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.pathPattern;
28

29
import java.io.IOException;
30
import java.net.URI;
31
import java.security.GeneralSecurityException;
32
import java.time.Instant;
33
import java.util.Collection;
34
import java.util.LinkedHashSet;
35
import java.util.List;
36
import java.util.Map;
37

38
import javax.net.ssl.X509TrustManager;
39

40
import org.slf4j.Logger;
41
import org.springframework.beans.factory.annotation.Autowired;
42
import org.springframework.boot.restclient.RestTemplateBuilder;
43
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Role;
45
import org.springframework.core.ParameterizedTypeReference;
46
import org.springframework.http.HttpHeaders;
47
import org.springframework.http.RequestEntity;
48
import org.springframework.security.access.prepost.PreAuthorize;
49
import org.springframework.security.config.Customizer;
50
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
51
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
52
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
53
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
54
import org.springframework.security.core.GrantedAuthority;
55
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
56
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
57
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
58
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
59
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
60
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
61
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
62
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
63
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
64
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
65
import org.springframework.security.web.SecurityFilterChain;
66
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
67
import org.springframework.security.web.authentication.logout.LogoutHandler;
68
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
69
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
70
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
71

72
import jakarta.servlet.DispatcherType;
73
import uk.ac.manchester.spinnaker.alloc.ServiceConfig.URLPathMaker;
74
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AuthProperties;
75
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
76

77
/**
78
 * The security and administration configuration of the service.
79
 * <p>
80
 * <strong>Note:</strong> role expressions ({@link #IS_USER} and
81
 * {@link #IS_ADMIN}) must be applied (with {@code @}{@link PreAuthorize}) to
82
 * <em>interfaces</em> of classes (or methods of those interfaces) that are
83
 * Spring Beans in order for the security interception to be applied correctly.
84
 * This is the <em>only</em> combination that is known to work reliably.
85
 *
86
 * @author Donal Fellows
87
 */
88
@EnableWebSecurity
89
@Role(ROLE_APPLICATION)
90
@EnableMethodSecurity(prePostEnabled = true)
91
@UsedInJavadocOnly(PreAuthorize.class)
92
public class SecurityConfig {
2✔
93
        private static final Logger log = getLogger(SecurityConfig.class);
2✔
94

95
        /** How to assert that a user must be an admin. */
96
        public static final String IS_ADMIN = "hasRole('ADMIN')";
97

98
        /** How to assert that a user must be an admin. */
99
        public static final String IS_NMPI_EXEC = "hasRole('NMPI_EXEC')";
100

101
        /** How to assert that a user must be able to read summaries. */
102
        public static final String IS_READER = "hasRole('READER')";
103

104
        /** How to filter out job details that a given user may see (or not). */
105
        public static final String MAY_SEE_JOB_DETAILS = "#permit.admin or "
106
                        + " #permit.nmpiexec or "
107
                        + " #permit.name == filterObject.owner.orElse(null)";
108

109
        private static final ParameterizedTypeReference<
110
                        Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
2✔
111
                                        new ParameterizedTypeReference<>() {
2✔
112
                                        };
113

114
        /**
115
         * How to assert that a user must be able to make jobs and read job details
116
         * in depth.
117
         */
118
        public static final String IS_USER = "hasRole('USER')";
119

120
        private static final String SESSION_COOKIE = "JSESSIONID";
121

122
        // ------------------------------------------------------------------------
123
        // What follows is UGLY stuff to make Java open HTTPS right
124
        private static X509TrustManager customTm;
125

126
        // Static because it has to be done very early.
127
        static {
128
                try {
129
                        installInjectableTrustStoreAsDefault(() -> customTm);
2✔
130
                        log.info("custom SSL trust injection point installed");
2✔
131
                } catch (Exception e) {
×
132
                        throw new RuntimeException("failed to set up SSL trust", e);
×
133
                }
2✔
134
        }
2✔
135

136
        /**
137
         * Builds a custom trust manager to plug into the Java runtime. This is so
138
         * that we can access resources managed by Keycloak, which is necessary
139
         * because Java doesn't trust its certificate by default (for messy
140
         * reasons).
141
         *
142
         * @param props
143
         *            Configuration properties
144
         * @return the custom trust manager, <em>already injected</em>
145
         * @throws IOException
146
         *             If the trust store can't be loaded because of I/O
147
         * @throws GeneralSecurityException
148
         *             If there is a security problem with the trust store
149
         * @see <a href="https://stackoverflow.com/a/24561444/301832">Stack
150
         *      Overflow</a>
151
         */
152
        @Bean
153
        @Role(ROLE_SUPPORT)
154
        static X509TrustManager customTrustManager(AuthProperties props)
155
                        throws IOException, GeneralSecurityException {
156
                var p = props.getOpenid();
2✔
157
                var tm = trustManager(loadTrustStore(p));
2✔
158
                customTm = tm;
2✔
159
                log.info("set trust store from {}", p.getTruststorePath().getURI());
2✔
160
                return tm;
2✔
161
        }
162

163
        // ------------------------------------------------------------------------
164

165
        @Autowired
166
        private BasicAuthEntryPoint authenticationEntryPoint;
167

168
        @Autowired
169
        private LocalAuthenticationProvider<?> localAuthProvider;
170

171
        @Autowired
172
        private AppAuthTransformationFilter authApplicationFilter;
173

174
        @Autowired
175
        private AuthenticationFailureHandler authenticationFailureHandler;
176

177
        @Autowired
178
        private AuthProperties properties;
179

180
        @Autowired
181
        private URLPathMaker urlMaker;
182

183
        /**
184
         * Configure things we plug into.
185
         *
186
         * @param auth
187
         *            The authentication manager builder to configure.
188
         */
189
        @Autowired
190
        public void configureGlobal(AuthenticationManagerBuilder auth) {
191
                auth.authenticationProvider(localAuthProvider);
2✔
192
        }
2✔
193

194
        private String oidcPath(String suffix) {
195
                return urlMaker.systemUrl("perform_oidc/" + suffix);
2✔
196
        }
197

198
        /**
199
         * Set up access control policies where they're not done by method security.
200
         * The {@code /info} part reveals admin details; you need {@code ROLE_ADMIN}
201
         * to view it. Everything to do with logging in <strong>must not</strong>
202
         * require being logged in. For anything else, as long as you are
203
         * authenticated we're happy. <em>Some</em> calls have additional
204
         * requirements; those are annotated with {@link PreAuthorize @PreAuthorize}
205
         * and a suitable auth expression.
206
         *
207
         * @param http
208
         *            Where the configuration is applied to.
209
         * @param introspector
210
         *           The introspector used to build request matchers.
211
         * @throws Exception
212
         *             If anything goes wrong with setting up.
213
         */
214
        private void defineAccessPolicy(HttpSecurity http,
215
                        HandlerMappingIntrospector introspector) throws Exception {
216
                http.authorizeHttpRequests((authorize) -> authorize
2✔
217
                                // Allow forwarded requests
218
                                .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
2✔
219
                                // Login process and static resources are available to all
220
                                .requestMatchers(pathPattern(urlMaker.systemUrl("login*")),
2✔
221
                                                pathPattern(urlMaker.systemUrl("perform_*")),
2✔
222
                                                pathPattern(oidcPath("**")),
2✔
223
                                                pathPattern(urlMaker.systemUrl("error")),
2✔
224
                                                pathPattern(urlMaker.systemUrl("resources/*")),
2✔
225
                                                pathPattern(urlMaker.serviceUrl("openapi.json")),
2✔
226
                                                pathPattern(urlMaker.serviceUrl("swagger*")),
2✔
227
                                                pathPattern(urlMaker.serviceUrl("index.css")))
2✔
228
                                .permitAll()
2✔
229
                                // Everything else requires post-login
230
                                .anyRequest().authenticated());
2✔
231
        }
2✔
232

233
        /**
234
         * How we handle the mechanics of login with the REST API.
235
         *
236
         * @param http
237
         *            Where the configuration is applied to.
238
         * @throws Exception
239
         *             If anything goes wrong with setting up. Not expected.
240
         */
241
        private void defineAPILoginRules(HttpSecurity http) throws Exception {
242
                if (properties.isBasic()) {
2!
243
                        http.httpBasic((authorize) -> authorize
2✔
244
                                        .authenticationEntryPoint(authenticationEntryPoint));
2✔
245
                }
246
                if (properties.getOpenid().isEnable()) {
2!
247
                        http.oauth2ResourceServer((authorize) -> authorize
×
248
                                        .authenticationEntryPoint(authenticationEntryPoint)
×
249
                                        .opaqueToken(oauth2 -> oauth2
×
250
                                                .introspector(new UserInfoOpaqueTokenIntrospector())));
×
251
                }
252
        }
2✔
253

254
        /**
255
         * How we handle the mechanics of login within the web UI.
256
         *
257
         * @param http
258
         *            Where the configuration is applied to.
259
         * @throws Exception
260
         *             If anything goes wrong with setting up. Not expected.
261
         */
262
        private void defineWebUILoginRules(HttpSecurity http) throws Exception {
263
                var loginUrl = urlMaker.systemUrl("login.html");
2✔
264
                var rootPage = urlMaker.systemUrl("");
2✔
265
                if (properties.getOpenid().isEnable()) {
2!
266
                        /*
267
                         * We're both, so we can have logins AND tokens. The logins are for
268
                         * using the HTML UI, and the tokens are for using from SpiNNaker
269
                         * tools (especially within the collabratory and the Jupyter
270
                         * notebook).
271
                         */
272
                        http.oauth2Login(oauth2 -> oauth2.loginPage(loginUrl)
×
273
                                        .loginProcessingUrl(oidcPath("login/code/*"))
×
274
                                        .authorizationEndpoint(
×
275
                                                        auth -> auth.baseUri(oidcPath("auth")))
×
276
                                        .defaultSuccessUrl(rootPage, true)
×
277
                                        .failureUrl(loginUrl + "?error=true")
×
278
                                        .userInfoEndpoint(auth -> auth
×
279
                                                .userAuthoritiesMapper(userAuthoritiesMapper()))
×
280
                                        .permitAll());
×
281
                        http.oauth2Client(Customizer.withDefaults());
×
282
                }
283
                if (properties.isLocalForm()) {
2!
284
                        http.formLogin(auth -> auth.loginPage(loginUrl)
2✔
285
                                        .loginProcessingUrl(urlMaker.systemUrl("perform_login"))
2✔
286
                                        .defaultSuccessUrl(rootPage, true)
2✔
287
                                        .failureUrl(loginUrl + "?error=true")
2✔
288
                                        .failureHandler(authenticationFailureHandler)
2✔
289
                                        .permitAll());
2✔
290
                }
291
        }
2✔
292

293
        /**
294
         * Logging out is common code between the UI and the API, but pretty
295
         * pointless for Basic Auth as browsers will just log straight back in
296
         * again. Still, it is meaningful (it invalidates the session).
297
         *
298
         * @param http
299
         *            Where the configuration is applied to.
300
         * @throws Exception
301
         *             If anything goes wrong with setting up. Not expected.
302
         */
303
        private void defineLogoutRules(HttpSecurity http) throws Exception {
304
                var loginUrl = urlMaker.systemUrl("login.html");
2✔
305
                http.logout(cust -> cust.logoutUrl(urlMaker.systemUrl("perform_logout"))
2✔
306
                                .addLogoutHandler((req, resp, auth) -> clearToken(req))
2✔
307
                                .deleteCookies(SESSION_COOKIE).invalidateHttpSession(true)
2✔
308
                                .logoutSuccessUrl(loginUrl));
2✔
309
        }
2✔
310

311
        /**
312
         * Define our main security controls.
313
         *
314
         * @param http
315
         *            Used to build the filter chain.
316
         * @param introspector
317
         *            The introspector used to build request matchers.
318
         * @return The filter chain that implements the controls.
319
         * @throws Exception
320
         *             If anything goes wrong with setting up. Not expected.
321
         */
322
        @Bean
323
        @Role(ROLE_SUPPORT)
324
        public SecurityFilterChain securityFilter(HttpSecurity http,
325
                        HandlerMappingIntrospector introspector)
326
                        throws Exception {
327
                defineAccessPolicy(http, introspector);
2✔
328
                defineAPILoginRules(http);
2✔
329
                defineWebUILoginRules(http);
2✔
330
                defineLogoutRules(http);
2✔
331
                http.addFilterAfter(authApplicationFilter,
2✔
332
                                BasicAuthenticationFilter.class);
333
                http.securityContext((securityContext) ->
2✔
334
                                securityContext.requireExplicitSave(false));
2✔
335
                return http.build();
2✔
336
        }
337

338
        private final class UserInfoOpaqueTokenIntrospector
339
                        implements OpaqueTokenIntrospector {
340
                private final OpaqueTokenIntrospector delegate;
341

342
                private final String userInfoUri;
343

344
                private UserInfoOpaqueTokenIntrospector() {
×
345
                        var p = properties.getOpenid();
×
346

347
                        delegate = SpringOpaqueTokenIntrospector
×
348
                                        .withIntrospectionUri(p.getIntrospection())
×
349
                                        .clientId(p.getId()).clientSecret(p.getSecret()).build();
×
350
                        userInfoUri = p.getUserinfo();
×
351
                }
×
352

353
                @Override
354
                public OAuth2AuthenticatedPrincipal introspect(String token) {
355
                        var authorized = delegate.introspect(token);
×
356
                        Instant issuedAt = authorized.getAttribute("issued-at");
×
357
                        Instant expiresAt = authorized.getAttribute("expires-at");
×
358

359
                        var userAttributes = userinfo(token);
×
360
                        var authorities = new LinkedHashSet<GrantedAuthority>();
×
361
                        var auth = new OidcUserAuthority(
×
362
                                        new OidcIdToken(token, issuedAt, expiresAt, userAttributes),
363
                                        new OidcUserInfo(userAttributes));
364
                        localAuthProvider.mapAuthorities(auth, authorities);
×
365
                        return new DefaultOAuth2User(authorities, userAttributes,
×
366
                                        "preferred_username");
367
                }
368

369
                private Map<String, Object> userinfo(String token) {
370
                        log.debug("Fetching user info from {}", userInfoUri);
×
371
                        var headers = new HttpHeaders();
×
372
                        headers.setAccept(List.of(APPLICATION_JSON));
×
373
                        headers.setBearerAuth(token);
×
374
                        var request = new RequestEntity<>(headers, GET,
×
375
                                        URI.create(userInfoUri));
×
376

NEW
377
                        var restTemplate = new RestTemplateBuilder().build();
×
378
                        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
×
379
                        var response =
×
380
                                        restTemplate.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
×
381

382
                        return response.getBody();
×
383
                }
384
        }
385

386
        /**
387
         * @return A converter that handles the initial extraction of collabratories
388
         *         and organisations from the info we have available when a user
389
         *         logs in explicitly in the web UI.
390
         * @see LocalAuthProviderImpl#mapAuthorities(OidcUserAuthority, Collection)
391
         */
392
        @Bean("hbp.collab-and-org.user-converter.shim")
393
        @Role(ROLE_SUPPORT)
394
        GrantedAuthoritiesMapper userAuthoritiesMapper() {
395
                var baseMapper = new SimpleAuthorityMapper();
2✔
396
                return authorities -> {
2✔
397
                        var mappedAuthorities = baseMapper.mapAuthorities(authorities);
×
398
                        authorities.forEach(authority -> {
×
399
                                /*
400
                                 * Check for OidcUserAuthority because Spring Security 5.2
401
                                 * returns each scope as a GrantedAuthority, which we don't care
402
                                 * about.
403
                                 */
404
                                if (authority instanceof OidcUserAuthority) {
×
405
                                        localAuthProvider.mapAuthorities(
×
406
                                                        (OidcUserAuthority) authority, mappedAuthorities);
407
                                }
408
                                mappedAuthorities.add(authority);
×
409
                        });
×
410
                        return mappedAuthorities;
×
411
                };
412
        }
413

414
        @Bean
415
        @Role(ROLE_SUPPORT)
416
        LogoutHandler logoutHandler() {
417
                var handler = new SecurityContextLogoutHandler();
2✔
418
                handler.setClearAuthentication(true);
2✔
419
                handler.setInvalidateHttpSession(true);
2✔
420
                return handler;
2✔
421
        }
422
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc