diff --git a/Dockerfile b/Dockerfile index 458fbc8a353233f0bda3dbff9be8b421d7fbaf5b..866c01bf47a4cff2589e9298b166d5b374fbe62b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,17 +43,16 @@ WORKDIR /home/portal ENTRYPOINT ["/run.sh"] # 8080: Web service API, health checks on http://host:8080$CONTEXT_PATH/health -# 4089: Akka cluster -EXPOSE 4089 8080 +EXPOSE 8080 HEALTHCHECK --start-period=60s CMD curl -v --silent http://localhost:8080$CONTEXT_PATH/health 2>&1 | grep UP LABEL org.label-schema.build-date=$BUILD_DATE \ org.label-schema.name="hbpmip/portal-backend" \ - org.label-schema.description="Java backend for the MIP portal" \ + org.label-schema.description="Spring backend for the MIP portal" \ org.label-schema.url="https://mip.humanbrainproject.eu" \ org.label-schema.vcs-type="git" \ - org.label-schema.vcs-url="https://github.com/LREN-CHUV/portal-backend" \ + org.label-schema.vcs-url="https://github.com/HBPMedical/portal-backend" \ org.label-schema.vcs-ref=$VCS_REF \ org.label-schema.version="$VERSION" \ org.label-schema.vendor="LREN CHUV" \ diff --git a/docker/README.md b/docker/README.md index 3f3437aec65ab4cc28b6236bf31a305b79572ea5..cc8c59fe1d79a1a50fcfd566c439cc9d8c5c90e4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -72,4 +72,6 @@ To use this image, you need a running instance of PostgreSQL and to configure th * RELEASE_STAGE: Release stage used when reporting errors to Bugsnag. Values are dev, staging, production * DATA_CENTER_LOCATION: Location of the datacenter, used when reporting errors to Bugsnag -* CONTAINER_ORCHESTRATION: Container orchestration system used to execute the Docker containers. Values are mesos, docker-compose, kubernetes + + +# TODO Refactor variables \ No newline at end of file diff --git a/docker/config/application.tmpl b/docker/config/application.tmpl index 4e723b8a3b27ed8f420e8b7f6293277ad17194f0..190f04f90ee8eb630c9b70819392b335363f9fee 100644 --- a/docker/config/application.tmpl +++ b/docker/config/application.tmpl @@ -30,26 +30,14 @@ hbp: client: clientId: {{ default .Env.CLIENT_ID "996f97c5-a3ca-460e-b18b-00df3e2be89a" }} clientSecret: {{ .Env.CLIENT_SECRET }} - accessTokenUri: {{ default .Env.TOKEN_URI "https://services.humanbrainproject.eu/oidc/token" }} - userAuthorizationUri: {{ default .Env.AUTH_URI "https://services.humanbrainproject.eu/oidc/authorize" }} logoutUri: {{ .Env.LOGOUT_URI }} - tokenName: access_token - authenticationScheme: query - clientAuthenticationScheme: form - useCurrentUri: false - preEstablishedRedirectUri: {{ default .Env.FRONTEND_LOGIN_URL "http://frontend/services/login/hbp" }} - resource: - userInfoUri: {{ default .Env.USER_INFO_URI "https://services.humanbrainproject.eu/oidc/userinfo" }} - revokeTokenUri: {{ default .Env.REVOKE_TOKEN_URI "https://services.humanbrainproject.eu/oidc/slo" }} - sso: - login-path: # WEB FRONTEND frontend: loginUrl: {{ default .Env.FRONTEND_LOGIN_URL "http://frontend/services/login/hbp" }} + redirectAfterLoginUrl: {{ default .Env.FRONTEND_AFTER_LOGIN_URL "http://frontend/" }} redirectAfterLogoutUrl: {{ default .Env.FRONTEND_AFTER_LOGOUT_URL (default .Env.LOGIN_URI "http://frontend/services/login/hbp") }} - redirectAfterLoginUrl: {{ default .Env.FRONTEND_AFTER_LOGIN_URL "http://frontend/home" }} - + logging: level: root: {{ default .Env.LOG_LEVEL_FRAMEWORK "ERROR" }} diff --git a/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java b/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java index afd3e56ed97164627dd5694e945eed97f18f9aaa..ff28602a1b33e541fd89858d4a6f9d0bf9787605 100644 --- a/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java +++ b/src/main/java/eu/hbp/mip/configuration/SecurityConfiguration.java @@ -1,7 +1,8 @@ package eu.hbp.mip.configuration; -import eu.hbp.mip.model.UserInfo; -import eu.hbp.mip.utils.*; +import eu.hbp.mip.utils.CORSFilter; +import eu.hbp.mip.utils.CustomAccessDeniedHandler; +import eu.hbp.mip.utils.CustomLoginUrlAuthenticationEntryPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -9,14 +10,16 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.client.OAuth2ClientContext; @@ -57,8 +60,9 @@ import java.util.List; import java.util.Map; -// See https://spring.io/guides/tutorials/spring-boot-oauth2/ for reference about configuring OAuth2 login +// Reference for OAuth2 login: https://spring.io/guides/tutorials/spring-boot-oauth2/ // also http://cscarioni.blogspot.ch/2013/04/pro-spring-security-and-oauth-2.html +// Security with Keycloak: https://www.thomasvitale.com/keycloak-authentication-flow-sso-client/ @Configuration @EnableOAuth2Client @@ -72,38 +76,40 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { /** * Enable HBP collab authentication (1) or disable it (0). Default is 1 */ - @Value("#{'${hbp.authentication.enabled:1}'}") - private boolean authentication; + @Value("#{'${hbp.authentication.enabled}'}") + private boolean authenticationEnabled; /** * Absolute URL to redirect to when login is required */ - @Value("#{'${frontend.loginUrl:/login/hbp}'}") + @Value("#{'${frontend.loginUrl}'}") private String loginUrl; /** * Absolute URL to redirect to when logout is required */ @Value("#{'${hbp.client.logoutUri}'}") - private String logoutUri; + private String logoutUrl; /** * Absolute URL to redirect to after successful login */ - @Value("#{'${frontend.redirectAfterLoginUrl:http://frontend/home}'}") + @Value("#{'${frontend.redirectAfterLoginUrl}'}") private String frontendRedirectAfterLogin; /** - * Absolute URL to redirect to after logout has occurred + * Absolute URL to redirect to after successful logout */ - @Value("#{'${frontend.redirectAfterLogoutUrl:/login/hbp}'}") + @Value("#{'${frontend.redirectAfterLogoutUrl}'}") private String redirectAfterLogoutUrl; - /** - * URL to revoke auth token - */ - @Value("#{'${hbp.resource.revokeTokenUri:https://services.humanbrainproject.eu/oidc/revoke}'}") - private String revokeTokenURI; + public boolean getAuthenticationEnabled() { + return authenticationEnabled; + } + + public String getFrontendRedirectAfterLogin() { + return frontendRedirectAfterLogin; + } @Override protected void configure(HttpSecurity http) throws Exception { @@ -111,15 +117,15 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // @formatter:off http.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class); - if (authentication) { + if (authenticationEnabled) { http.antMatcher("/**") .authorizeRequests() .antMatchers( - "/", "/login/**", "/health/**", "/info/**", "/metrics/**", "/trace/**", "/frontend/**", "/webjars/**", "/v2/api-docs", "/swagger-ui.html", "/swagger-resources/**" - ) - .permitAll() + "/", "/login/**", "/health/**", "/info/**", "/metrics/**", + "/trace/**", "/frontend/**", "/webjars/**", "/v2/api-docs", + "/swagger-ui.html", "/swagger-resources/**" + ).permitAll() .antMatchers("/galaxy*", "/galaxy/*").hasRole("Data Manager") - //.anyRequest().authenticated() .anyRequest().hasRole("Researcher") .and().exceptionHandling().authenticationEntryPoint(new CustomLoginUrlAuthenticationEntryPoint(loginUrl)) .accessDeniedHandler(new CustomAccessDeniedHandler()) @@ -131,7 +137,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { } else { http.antMatcher("/**") .authorizeRequests() - .antMatchers("/**").permitAll().and().csrf().disable(); + .antMatchers("/**").permitAll() + .and().csrf().disable(); } } @@ -165,13 +172,6 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { return new ResourceServerProperties(); } - public boolean isAuthentication() { - return authentication; - } - - public String getFrontendRedirectAfterLogin() { - return frontendRedirectAfterLogin; - } private Filter csrfHeaderFilter() { return new OncePerRequestFilter() { @@ -199,43 +199,6 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { return repository; } - private class CustomLogoutHandler implements LogoutHandler { - @Override - public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { - - // Hackish way of accessing to this information... - final UserInfo userInfo = (UserInfo) httpServletRequest.getSession().getAttribute("userInfo"); - if (userInfo != null) { - userInfo.setFakeAuth(false); - } - - if (oauth2ClientContext == null || oauth2ClientContext.getAccessToken() == null) { - return; - } - - String idToken = oauth2ClientContext.getAccessToken().getAdditionalInformation().get("id_token").toString(); - - StringBuilder query = new StringBuilder(); - query.append("{"); - query.append("\"token\":"); - query.append("\"").append(idToken).append("\""); - query.append("}"); - - try { - int responseCode = HTTPUtil.sendPost(revokeTokenURI, query.toString(), new StringBuilder()); - if (responseCode != 200) { - LOGGER.warn("Cannot send request to OIDC server for revocation ! "); - } else { - LOGGER.info("Should be logged out"); - } - } catch (IOException e) { - LOGGER.warn("Cannot notify logout to OIDC server !"); - LOGGER.trace("Cannot notify logout", e); - } - - } - } - @Bean public AuthoritiesExtractor keycloakAuthoritiesExtractor() { return new KeycloakAuthoritiesExtractor(); @@ -273,33 +236,31 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { public void logout() { - // POSTã™ã‚‹ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‘ãƒ©ãƒ¡ãƒ¼ã‚¿ãƒ¼ã‚’ä½œæˆ + // TODO Try removing + RestTemplate restTemplate = new RestTemplate(); MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>(); formParams.add("client_id", hbp().getClientId()); formParams.add("client_secret", hbp().getClientSecret()); formParams.add("refresh_token", this.oauth2ClientContext.getAccessToken().getRefreshToken().getValue()); - // ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ˜ãƒƒãƒ€ãƒ¼ã‚’ä½œæˆ + HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); - // ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ä½œæˆ RequestEntity<MultiValueMap<String, String>> requestEntity = new RequestEntity<>(formParams, httpHeaders, HttpMethod.POST, - URI.create(logoutUri)); - // POSTリクエストé€ä¿¡ï¼ˆãƒã‚°ã‚¢ã‚¦ãƒˆå®Ÿè¡Œï¼‰ - - ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class); + URI.create(logoutUrl)); + restTemplate.exchange(requestEntity, String.class); } @Value("#{'${services.keycloak.keycloakUrl}'}") private String keycloakUrl; - // static { - // disableCertificateValidation(); - // } - public void disableCertificateValidation() { + + //TODO Refactor logging + LOGGER.info("disabling certificate validation host : " + keycloakUrl); + // Create a trust manager that does not validate certificate chains TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @@ -316,18 +277,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // Ignore differences between given hostname and certificate hostname - HostnameVerifier hv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - - // System.out.println("Warning: URL Host: " + hostname + " vs. " - // + session.getPeerHost()); - if (hostname.equals(keycloakUrl) && session.getPeerHost().equals(keycloakUrl)) { - return true; - } else { - return false; - } - } - }; + HostnameVerifier hv = + (hostname, session) -> hostname.equals(keycloakUrl) && session.getPeerHost().equals(keycloakUrl); // Install the all-trusting trust manager try { @@ -336,6 +287,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection.setDefaultHostnameVerifier(hv); } catch (Exception e) { + // TODO add log message } } diff --git a/src/main/java/eu/hbp/mip/controllers/SecurityApi.java b/src/main/java/eu/hbp/mip/controllers/SecurityApi.java index ff8d70a80405ad2e7579198c5ef4f8a9a42d38d5..e179b3172446ddd9906f36bca3f7efe6b195c165 100644 --- a/src/main/java/eu/hbp/mip/controllers/SecurityApi.java +++ b/src/main/java/eu/hbp/mip/controllers/SecurityApi.java @@ -55,7 +55,7 @@ public class SecurityApi { //LOGGER.trace("Cannot read user json", e); } - if (!securityConfiguration.isAuthentication()) { + if (!securityConfiguration.getAuthenticationEnabled()) { if (userInfo.isFakeAuth()) { response.setStatus(401); } @@ -85,7 +85,7 @@ public class SecurityApi { @ConditionalOnExpression("${hbp.authentication.enabled:0}") public void noLogin(HttpServletResponse httpServletResponse) throws IOException { userInfo.setFakeAuth(true); - Logging.LogUserAction(userInfo.getUser().getUsername(), "(GET) /user/login/hbp", "Unathorized login."); + Logging.LogUserAction(userInfo.getUser().getUsername(), "(GET) /user/login/hbp", "Unauthorized login."); httpServletResponse.sendRedirect(securityConfiguration.getFrontendRedirectAfterLogin()); } @@ -104,7 +104,6 @@ public class SecurityApi { * * @return Return a @{@link ResponseEntity} with the token. */ - @RequestMapping(path = "/galaxy", method = RequestMethod.GET, produces = "application/json") @PreAuthorize("hasRole('Data Manager')") @ResponseStatus(value = HttpStatus.OK)