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

SpiNNakerManchester / JavaSpiNNaker / 6826

17 Jul 2025 10:30AM UTC coverage: 36.203% (+0.01%) from 36.193%
6826

push

github

web-flow
Merge pull request #1257 from SpiNNakerManchester/fix_migration_issues

Fix migration issues

1895 of 5876 branches covered (32.25%)

Branch coverage included in aggregate %.

13 of 16 new or added lines in 2 files covered. (81.25%)

43 existing lines in 2 files now uncovered.

8929 of 24022 relevant lines covered (37.17%)

0.74 hits per line

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

52.34
/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.POST;
22
import static org.springframework.http.MediaType.APPLICATION_JSON;
23
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN;
24
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
25
import static org.springframework.util.StreamUtils.copyToByteArray;
26
import static uk.ac.manchester.spinnaker.alloc.security.AppAuthTransformationFilter.clearToken;
27
import static uk.ac.manchester.spinnaker.alloc.security.Utils.installInjectableTrustStoreAsDefault;
28
import static uk.ac.manchester.spinnaker.alloc.security.Utils.loadTrustStore;
29
import static uk.ac.manchester.spinnaker.alloc.security.Utils.trustManager;
30

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

40
import javax.net.ssl.X509TrustManager;
41

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

80
import jakarta.servlet.DispatcherType;
81
import uk.ac.manchester.spinnaker.alloc.ServiceConfig.URLPathMaker;
82
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AuthProperties;
83
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
84

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

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

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

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

112
        /** How to filter out job details that a given user may see (or not). */
113
        public static final String MAY_SEE_JOB_DETAILS = "#permit.admin or "
114
                        + " #permit.nmpiexec or "
115
                        + " #permit.name == filterObject.owner.orElse(null)";
116

117
        private static final ParameterizedTypeReference<
118
                        Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
2✔
119
                                        new ParameterizedTypeReference<>() {
2✔
120
                                        };
121

122
        /**
123
         * How to assert that a user must be able to make jobs and read job details
124
         * in depth.
125
         */
126
        public static final String IS_USER = "hasRole('USER')";
127

128
        private static final String SESSION_COOKIE = "JSESSIONID";
129

130
        // ------------------------------------------------------------------------
131
        // What follows is UGLY stuff to make Java open HTTPS right
132
        private static X509TrustManager customTm;
133

134
        // Static because it has to be done very early.
135
        static {
136
                try {
137
                        installInjectableTrustStoreAsDefault(() -> customTm);
2✔
138
                        log.info("custom SSL trust injection point installed");
2✔
UNCOV
139
                } catch (Exception e) {
×
UNCOV
140
                        throw new RuntimeException("failed to set up SSL trust", e);
×
141
                }
2✔
142
        }
2✔
143

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

171
        // ------------------------------------------------------------------------
172

173
        @Autowired
174
        private BasicAuthEntryPoint authenticationEntryPoint;
175

176
        @Autowired
177
        private LocalAuthenticationProvider<?> localAuthProvider;
178

179
        @Autowired
180
        private AppAuthTransformationFilter authApplicationFilter;
181

182
        @Autowired
183
        private AuthenticationFailureHandler authenticationFailureHandler;
184

185
        @Autowired
186
        private AuthProperties properties;
187

188
        @Autowired
189
        private URLPathMaker urlMaker;
190

191
        /**
192
         * Configure things we plug into.
193
         *
194
         * @param auth
195
         *            The authentication manager builder to configure.
196
         */
197
        @Autowired
198
        public void configureGlobal(AuthenticationManagerBuilder auth) {
199
                auth.authenticationProvider(localAuthProvider);
2✔
200
        }
2✔
201

202
        private String oidcPath(String suffix) {
203
                return urlMaker.systemUrl("perform_oidc/" + suffix);
2✔
204
        }
205

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

242
        /**
243
         * How we handle the mechanics of login with the REST API.
244
         *
245
         * @param http
246
         *            Where the configuration is applied to.
247
         * @throws Exception
248
         *             If anything goes wrong with setting up. Not expected.
249
         */
250
        private void defineAPILoginRules(HttpSecurity http) throws Exception {
251
                if (properties.isBasic()) {
2!
252
                        http.httpBasic((authorize) -> authorize
2✔
253
                                        .authenticationEntryPoint(authenticationEntryPoint));
2✔
254
                }
255
                if (properties.getOpenid().isEnable()) {
2!
UNCOV
256
                        http.oauth2ResourceServer((authorize) -> authorize
×
UNCOV
257
                                        .authenticationEntryPoint(authenticationEntryPoint)
×
UNCOV
258
                                        .opaqueToken(oauth2 -> oauth2
×
UNCOV
259
                                                .introspector(new UserInfoOpaqueTokenIntrospector())));
×
260
                }
261
        }
2✔
262

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

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

320
        /**
321
         * Define our main security controls.
322
         *
323
         * @param http
324
         *            Used to build the filter chain.
325
         * @param introspector
326
         *            The introspector used to build request matchers.
327
         * @return The filter chain that implements the controls.
328
         * @throws Exception
329
         *             If anything goes wrong with setting up. Not expected.
330
         */
331
        @Bean
332
        @Role(ROLE_SUPPORT)
333
        public SecurityFilterChain securityFilter(HttpSecurity http,
334
                        HandlerMappingIntrospector introspector)
335
                        throws Exception {
336
                defineAccessPolicy(http, introspector);
2✔
337
                defineAPILoginRules(http);
2✔
338
                defineWebUILoginRules(http);
2✔
339
                defineLogoutRules(http);
2✔
340
                http.addFilterAfter(authApplicationFilter,
2✔
341
                                BasicAuthenticationFilter.class);
342
                return http.build();
2✔
343
        }
344

345
        private final class UserInfoOpaqueTokenIntrospector
346
                        implements OpaqueTokenIntrospector {
347
                private final OpaqueTokenIntrospector delegate;
348

349
                private final String userInfoUri;
350

UNCOV
351
                private UserInfoOpaqueTokenIntrospector() {
×
UNCOV
352
                        var p = properties.getOpenid();
×
353

UNCOV
354
                        delegate = new SpringOpaqueTokenIntrospector(p.getIntrospection(),
×
UNCOV
355
                                        p.getId(), p.getSecret());
×
UNCOV
356
                        userInfoUri = p.getUserinfo();
×
UNCOV
357
                }
×
358

359
                @Override
360
                public OAuth2AuthenticatedPrincipal introspect(String token) {
UNCOV
361
                        var authorized = delegate.introspect(token);
×
UNCOV
362
                        Instant issuedAt = authorized.getAttribute("issued-at");
×
UNCOV
363
                        Instant expiresAt = authorized.getAttribute("expires-at");
×
364

365
                        var userAttributes = userinfo(token);
×
366
                        var authorities = new LinkedHashSet<GrantedAuthority>();
×
UNCOV
367
                        var auth = new OidcUserAuthority(
×
368
                                        new OidcIdToken(token, issuedAt, expiresAt, userAttributes),
369
                                        new OidcUserInfo(userAttributes));
370
                        localAuthProvider.mapAuthorities(auth, authorities);
×
371
                        return new DefaultOAuth2User(authorities, userAttributes,
×
372
                                        "preferred_username");
373
                }
374

375
                private Map<String, Object> userinfo(String token) {
376
                        var headers = new HttpHeaders();
×
377
                        headers.setAccept(List.of(APPLICATION_JSON));
×
UNCOV
378
                        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
×
379
                        var request = new RequestEntity<>(ACCESS_TOKEN + "=" + token,
×
380
                                        headers, POST, URI.create(userInfoUri));
×
381

UNCOV
382
                        var restLog = LogFactory.getLog(LoggingCustomizer.class);
×
UNCOV
383
                        var restTemplate = new RestTemplateBuilder().customizers(
×
384
                                        new LoggingCustomizer(restLog, new Formatter())).build();
×
385
                        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
×
UNCOV
386
                        var response =
×
UNCOV
387
                                        restTemplate.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
×
388

UNCOV
389
                        return response.getBody();
×
390
                }
391
        }
392

393
        private final class Formatter implements LogFormatter {
×
394

395
                @Override
396
                public String formatResponse(ClientHttpResponse response)
397
                                throws IOException {
398
                        return String.format("Response:\n    Headers: %s\n    Body: %s",
×
399
                                        response.getHeaders(),
×
400
                                        new String(copyToByteArray(response.getBody())));
×
401
                }
402

403
                @Override
404
                public String formatRequest(HttpRequest request, byte[] body) {
UNCOV
405
                        return String.format(
×
406
                                        "%s Request to %s:\n"
407
                                        + "    Headers: %s\n"
408
                                        + "    Body: %s",
UNCOV
409
                                        request.getMethod(), request.getURI(), request.getHeaders(),
×
410
                                        new String(body));
411
                }
412
        }
413

414
        /**
415
         * @return A converter that handles the initial extraction of collabratories
416
         *         and organisations from the info we have available when a user
417
         *         logs in explicitly in the web UI.
418
         * @see LocalAuthProviderImpl#mapAuthorities(OidcUserAuthority, Collection)
419
         */
420
        @Bean("hbp.collab-and-org.user-converter.shim")
421
        @Role(ROLE_SUPPORT)
422
        GrantedAuthoritiesMapper userAuthoritiesMapper() {
423
                var baseMapper = new SimpleAuthorityMapper();
2✔
424
                return authorities -> {
2✔
UNCOV
425
                        var mappedAuthorities = baseMapper.mapAuthorities(authorities);
×
UNCOV
426
                        authorities.forEach(authority -> {
×
427
                                /*
428
                                 * Check for OidcUserAuthority because Spring Security 5.2
429
                                 * returns each scope as a GrantedAuthority, which we don't care
430
                                 * about.
431
                                 */
UNCOV
432
                                if (authority instanceof OidcUserAuthority) {
×
UNCOV
433
                                        localAuthProvider.mapAuthorities(
×
434
                                                        (OidcUserAuthority) authority, mappedAuthorities);
435
                                }
UNCOV
436
                                mappedAuthorities.add(authority);
×
UNCOV
437
                        });
×
UNCOV
438
                        return mappedAuthorities;
×
439
                };
440
        }
441

442
        @Bean
443
        @Role(ROLE_SUPPORT)
444
        LogoutHandler logoutHandler() {
445
                var handler = new SecurityContextLogoutHandler();
2✔
446
                handler.setClearAuthentication(true);
2✔
447
                handler.setInvalidateHttpSession(true);
2✔
448
                return handler;
2✔
449
        }
450
}
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