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

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

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

53.64
/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 java.nio.charset.StandardCharsets.ISO_8859_1;
19
import static org.slf4j.LoggerFactory.getLogger;
20
import static org.springframework.beans.factory.config.BeanDefinition.ROLE_APPLICATION;
21
import static org.springframework.beans.factory.config.BeanDefinition.ROLE_SUPPORT;
22
import static org.springframework.http.HttpMethod.POST;
23
import static org.springframework.http.MediaType.APPLICATION_JSON;
24
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.ACCESS_TOKEN;
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.annotation.authentication.builders.AuthenticationManagerBuilder;
58
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
59
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
60
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
61
import org.springframework.security.core.GrantedAuthority;
62
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
63
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
64
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
65
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
66
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
67
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
68
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
69
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
70
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
71
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
72
import org.springframework.security.web.SecurityFilterChain;
73
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
74
import org.springframework.security.web.authentication.logout.LogoutHandler;
75
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
76
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
77

78
import uk.ac.manchester.spinnaker.alloc.ServiceConfig.URLPathMaker;
79
import uk.ac.manchester.spinnaker.alloc.SpallocProperties.AuthProperties;
80
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
81

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

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

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

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

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

114
        private static final ParameterizedTypeReference<
115
                        Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
1✔
116
                                        new ParameterizedTypeReference<>() {
1✔
117
                                        };
118

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

125
        private static final String SESSION_COOKIE = "JSESSIONID";
126

127
        // ------------------------------------------------------------------------
128
        // What follows is UGLY stuff to make Java open HTTPS right
129
        private static X509TrustManager customTm;
130

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

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

168
        // ------------------------------------------------------------------------
169

170
        @Autowired
171
        private BasicAuthEntryPoint authenticationEntryPoint;
172

173
        @Autowired
174
        private LocalAuthenticationProvider<?> localAuthProvider;
175

176
        @Autowired
177
        private AppAuthTransformationFilter authApplicationFilter;
178

179
        @Autowired
180
        private AuthenticationFailureHandler authenticationFailureHandler;
181

182
        @Autowired
183
        private AuthProperties properties;
184

185
        @Autowired
186
        private URLPathMaker urlMaker;
187

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

199
        private String oidcPath(String suffix) {
200
                return urlMaker.systemUrl("perform_oidc/" + suffix);
1✔
201
        }
202

203
        /**
204
         * Set up access control policies where they're not done by method security.
205
         * The {@code /info} part reveals admin details; you need {@code ROLE_ADMIN}
206
         * to view it. Everything to do with logging in <strong>must not</strong>
207
         * require being logged in. For anything else, as long as you are
208
         * authenticated we're happy. <em>Some</em> calls have additional
209
         * requirements; those are annotated with {@link PreAuthorize @PreAuthorize}
210
         * and a suitable auth expression.
211
         *
212
         * @param http
213
         *            Where the configuration is applied to.
214
         * @throws Exception
215
         *             If anything goes wrong with setting up.
216
         */
217
        private void defineAccessPolicy(HttpSecurity http) throws Exception {
218
                http.authorizeRequests()
1✔
219
                                // General metadata pages require ADMIN access
220
                                .antMatchers(urlMaker.serviceUrl("info*"),
1✔
221
                                                urlMaker.serviceUrl("info/**"))
1✔
222
                                .hasRole("ADMIN")
1✔
223
                                // Login process and static resources are available to all
224
                                .antMatchers(urlMaker.systemUrl("login*"),
1✔
225
                                                urlMaker.systemUrl("perform_*"), oidcPath("**"),
1✔
226
                                                urlMaker.systemUrl("error"),
1✔
227
                                                urlMaker.systemUrl("resources/*"))
1✔
228
                                .permitAll()
1✔
229
                                // Everything else requires post-login
230
                                .anyRequest().authenticated();
1✔
231
        }
1✔
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()) {
1✔
243
                        http.httpBasic().authenticationEntryPoint(authenticationEntryPoint);
1✔
244
                }
245
                if (properties.getOpenid().isEnable()) {
1✔
246
                        http.oauth2ResourceServer()
×
247
                                        .authenticationEntryPoint(authenticationEntryPoint)
×
248
                                        .opaqueToken()
×
249
                                        .introspector(new UserInfoOpaqueTokenIntrospector());
×
250
                }
251
        }
1✔
252

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

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

306
        /**
307
         * Define our main security controls.
308
         *
309
         * @param http
310
         *            Used to build the filter chain.
311
         * @return The filter chain that implements the controls.
312
         * @throws Exception
313
         *             If anything goes wrong with setting up. Not expected.
314
         */
315
        @Bean
316
        @Role(ROLE_SUPPORT)
317
        public SecurityFilterChain securityFilter(HttpSecurity http)
318
                        throws Exception {
319
                defineAccessPolicy(http);
1✔
320
                defineAPILoginRules(http);
1✔
321
                defineWebUILoginRules(http);
1✔
322
                defineLogoutRules(http);
1✔
323
                http.addFilterAfter(authApplicationFilter,
1✔
324
                                BasicAuthenticationFilter.class);
325
                return http.build();
1✔
326
        }
327

328
        private final class UserInfoOpaqueTokenIntrospector
329
                        implements OpaqueTokenIntrospector {
330
                private final OpaqueTokenIntrospector delegate;
331

332
                private final String userInfoUri;
333

334
                private UserInfoOpaqueTokenIntrospector() {
×
335
                        var p = properties.getOpenid();
×
336

337
                        delegate = new SpringOpaqueTokenIntrospector(p.getIntrospection(),
×
338
                                        p.getId(), p.getSecret());
×
339
                        userInfoUri = p.getUserinfo();
×
340
                }
×
341

342
                @Override
343
                public OAuth2AuthenticatedPrincipal introspect(String token) {
344
                        var authorized = delegate.introspect(token);
×
345
                        Instant issuedAt = authorized.getAttribute("issued-at");
×
346
                        Instant expiresAt = authorized.getAttribute("expires-at");
×
347

348
                        var userAttributes = userinfo(token);
×
349
                        var authorities = new LinkedHashSet<GrantedAuthority>();
×
350
                        var auth = new OidcUserAuthority(
×
351
                                        new OidcIdToken(token, issuedAt, expiresAt, userAttributes),
352
                                        new OidcUserInfo(userAttributes));
353
                        localAuthProvider.mapAuthorities(auth, authorities);
×
354
                        return new DefaultOAuth2User(authorities, userAttributes,
×
355
                                        "preferred_username");
356
                }
357

358
                private Map<String, Object> userinfo(String token) {
359
                        var headers = new HttpHeaders();
×
360
                        headers.setAccept(List.of(APPLICATION_JSON));
×
361
                        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
×
362
                        var request = new RequestEntity<>(ACCESS_TOKEN + "=" + token,
×
363
                                        headers, POST, URI.create(userInfoUri));
×
364

365
                        var restLog = LogFactory.getLog(LoggingCustomizer.class);
×
366
                        var restTemplate = new RestTemplateBuilder().customizers(
×
367
                                        new LoggingCustomizer(restLog, new Formatter())).build();
×
368
                        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
×
369
                        var response =
×
370
                                        restTemplate.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
×
371

372
                        return response.getBody();
×
373
                }
374
        }
375

376
        private static final class Formatter implements LogFormatter {
377
                @Override
378
                public String formatResponse(ClientHttpResponse response)
379
                                throws IOException {
380
                        return String.format("Response:\n    Headers: %s\n    Body: %s",
×
381
                                        response.getHeaders(),
×
382
                                        new String(copyToByteArray(response.getBody())));
×
383
                }
384

385
                @Override
386
                public String formatRequest(HttpRequest request, byte[] body) {
387
                        return String.format(
×
388
                                        "%s Request to %s:\n    Headers: %s\n    Body: %s",
389
                                        request.getMethod(), request.getURI(), request.getHeaders(),
×
390
                                        new String(body, ISO_8859_1)); // A "safe" encoding
391
                }
392
        }
393

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

421
        @Bean
422
        @Role(ROLE_SUPPORT)
423
        LogoutHandler logoutHandler() {
424
                var handler = new SecurityContextLogoutHandler();
1✔
425
                handler.setClearAuthentication(true);
1✔
426
                handler.setInvalidateHttpSession(true);
1✔
427
                return handler;
1✔
428
        }
429
}
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