diff --git a/config/application.tmpl b/config/application.tmpl index 053f28947172e5f4404fbbcada65f157f5f6bf01..cb8167e694800fb6cfc1f384d420a0c7c065e9b7 100644 --- a/config/application.tmpl +++ b/config/application.tmpl @@ -1,26 +1,28 @@ # Configuration template for the portal running inside a Docker container + +### EMBEDDED SERVER CONFIGURATION ### +server: + servlet: + contextPath: "/services" + port: 8080 + forward-headers-strategy: native + + ### LOG LEVELS ### logging: level: root: {{ default .Env.LOG_LEVEL_FRAMEWORK "ERROR" }} - org: {{ default .Env.LOG_LEVEL_FRAMEWORK "ERROR" }} eu: hbp: {{ default .Env.LOG_LEVEL "INFO" }} -### AUTHENTICATION ### -authentication: - enabled: {{ default .Env.AUTHENTICATION "true" }} - - ### DATABASE CONFIGURATION ### spring: - portal-datasource: - url: {{ default .Env.PORTAL_DB_URL "jdbc:postgresql://88.197.53.106:5432/portal" }} - schema: {{ default .Env.PORTAL_DB_SCHEMA "public" }} - username: {{ default .Env.PORTAL_DB_USER "postgres" }} - password: {{ .Env.PORTAL_DB_PASSWORD }} + datasource: + url: {{ default .Env.PORTAL_DB_URL "jdbc:postgresql://172.17.0.1:5433/portal" }} + username: {{ default .Env.PORTAL_DB_USER "portal" }} + password: {{ default .Env.PORTAL_DB_PASSWORD "portalpwd" }} driver-class-name: org.postgresql.Driver data: jpa: @@ -28,12 +30,33 @@ spring: bootstrap-mode: default jpa: hibernate: - dialect: org.hibernate.dialect.PostgreSQL9Dialect ddl-auto: validate - mvc: pathmatch: matching-strategy: ant_path_matcher + security: + oauth2: + client: + registration: + keycloak: + authorization-grant-type: authorization_code + client-id: {{ .Env.KEYCLOAK_REALM }} + client-secret: {{ .Env.KEYCLOAK_CLIENT_SECRET }} + provider: keycloak + scope: openid + redirect-uri: http://172.17.0.1/${server.servlet.contextPath}/login/oauth2/code/{{ .Env.KEYCLOAK_REALM }} + provider: + keycloak: + issuer-uri: http://172.17.0.1/auth/realms/{{ .Env.KEYCLOAK_REALM }} + user-name-attribute: preferred_username + + +### AUTHENTICATION ### +authentication: + enabled: {{ default .Env.AUTHENTICATION "1" }} + all_datasets_allowed_claim: research_dataset_all + all_experiments_allowed_claim: research_experiment_all + dataset_claim_prefix: research_dataset_ ### EXTERNAL SERVICES ### @@ -43,44 +66,13 @@ services: algorithmsUrl: {{ .Env.EXAREME2_URL}}/algorithms attributesUrl: {{ .Env.EXAREME2_URL}}/data_models_attributes cdesMetadataUrl: {{ .Env.EXAREME2_URL}}/cdes_metadata - exareme: queryExaremeUrl: {{ default .Env.EXAREME_URL "http://localhost:9090" }}/mining/query algorithmsUrl: {{ default .Env.EXAREME_URL "http://localhost:9090" }}/mining/algorithms.json -### KEYCLOAK ### -keycloak: - enabled: true - auth-server-url: {{ .Env.KEYCLOAK_AUTH_URL }} - realm: {{ .Env.KEYCLOAK_REALM }} - resource: {{ .Env.KEYCLOAK_CLIENT_ID }} - use-resource-role-mappings: true - enable-basic-auth: true - credentials: - secret: {{ .Env.KEYCLOAK_CLIENT_SECRET }} - principal-attribute: "preferred_username" - ssl-required: {{ .Env.KEYCLOAK_SSL_REQUIRED }} - ### EXTERNAL FILES ### # Files are imported when building the docker image files: pathologies_json: "file:/opt/portal/api/pathologies.json" disabledAlgorithms_json: "file:{{ .Env.DISABLED_ALGORITHMS_CONFIG_PATH}}" - - -### EMBEDDED SERVER CONFIGURATION ### -server: - servlet: - contextPath: "/services" - port: 8080 - forward-headers-strategy: native - - -### ENDPOINTS ### -endpoints: - enabled: true - health: - enabled: true - endpoint: /health - sensitive: false diff --git a/pom.xml b/pom.xml index 54473e10ad46575f5f03bb9029a3991d4e339332..d5a08e25c3b4037cad62906e587b3d50ec173864 100644 --- a/pom.xml +++ b/pom.xml @@ -13,17 +13,17 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.7.13</version> + <version>3.1.2</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>17</java.version> - <spring-context.version>5.3.29</spring-context.version> + <spring-context.version>6.0.11</spring-context.version> <postgresql.version>42.6.0</postgresql.version> - <hibernate.version>5.6.15.Final</hibernate.version> + <jakarta.version>3.1.0</jakarta.version> + <hibernate.version>6.2.7.Final</hibernate.version> <flyway-core.version>4.2.0</flyway-core.version> - <keycloak-spring.version>13.0.1</keycloak-spring.version> <gson.version>2.10.1</gson.version> <commons-dbcp.version>2.9.0</commons-dbcp.version> <lombok.version>1.18.28</lombok.version> @@ -48,13 +48,17 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-actuator</artifactId> + <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> @@ -65,17 +69,12 @@ <artifactId>spring-data-jpa</artifactId> </dependency> <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-spring-boot-starter</artifactId> - <version>${keycloak-spring.version}</version> - </dependency> - <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-spring-security-adapter</artifactId> - <version>${keycloak-spring.version}</version> + <groupId>jakarta.persistence</groupId> + <artifactId>jakarta.persistence-api</artifactId> + <version>${jakarta.version}</version> </dependency> <dependency> - <groupId>org.hibernate</groupId> + <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> diff --git a/src/main/java/eu/hbp/mip/configurations/PersistenceConfiguration.java b/src/main/java/eu/hbp/mip/configurations/PersistenceConfiguration.java index b87771dbd484ba692e9e82135bc4e1661dffe2ce..23df36815b2305ed368cd9fa3e1aa0339528af9f 100644 --- a/src/main/java/eu/hbp/mip/configurations/PersistenceConfiguration.java +++ b/src/main/java/eu/hbp/mip/configurations/PersistenceConfiguration.java @@ -20,8 +20,8 @@ import javax.sql.DataSource; public class PersistenceConfiguration { @Primary - @Bean(name = "portal-datasource") - @ConfigurationProperties(prefix = "spring.portal-datasource") + @Bean(name = "datasource") + @ConfigurationProperties(prefix = "spring.datasource") public DataSource portalDataSource() { return DataSourceBuilder.create().build(); } diff --git a/src/main/java/eu/hbp/mip/configurations/SecurityConfiguration.java b/src/main/java/eu/hbp/mip/configurations/SecurityConfiguration.java index bb27681312d0f145c0d04b6c46ca66f7b788a1c6..e57bc1432126556c0c931b56ce94c9947052f631 100644 --- a/src/main/java/eu/hbp/mip/configurations/SecurityConfiguration.java +++ b/src/main/java/eu/hbp/mip/configurations/SecurityConfiguration.java @@ -1,2 +1,129 @@ -package eu.hbp.mip.configurations;public class SecurityConfiguration { +package eu.hbp.mip.configurations; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfiguration { + + @Value("${authentication.enabled}") + private boolean authenticationEnabled; + + // This Bean is used when there is no authentication and there is no keycloak server running due to this bug: + // https://github.com/spring-projects/spring-security/issues/11397#issuecomment-1655906163 + // So we overwrite the ClientRegistrationRepository Bean to avoid the IP server lookup. + @Bean + @ConditionalOnProperty(prefix = "authentication", name = "enabled", havingValue = "0") + public ClientRegistrationRepository clientRegistrationRepository() { + ClientRegistration dummyRegistration = ClientRegistration.withRegistrationId("dummy") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .build(); + return new InMemoryClientRegistrationRepository(dummyRegistration); + } + + @Bean + SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepo) throws Exception { + if (authenticationEnabled) { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/login/**", "/oauth2/**", "/actuator/**").permitAll() + .requestMatchers("/**").authenticated() + ); + + http.oauth2Login(login -> login.defaultSuccessUrl("/", true)); + + // Open ID Logout + // https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-oidc-logout + OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepo); + successHandler.setPostLogoutRedirectUri("{baseUrl}"); + http.logout(logout -> logout.logoutSuccessHandler(successHandler)); + + // ---> XSRF Token handling + // https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#deferred-csrf-token + // https://stackoverflow.com/questions/74447118/csrf-protection-not-working-with-spring-security-6 + XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler(); + // set the name of the attribute the CsrfToken will be populated on + requestHandler.setCsrfRequestAttributeName(null); + + // Change cookie path + CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); + tokenRepository.setCookiePath("/"); + + http.csrf((csrf) -> csrf + .csrfTokenRepository(tokenRepository) + .csrfTokenRequestHandler(requestHandler::handle) + .ignoringRequestMatchers("/logout") + ); + // <--- XSRF Token handling + + + } else { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/**").permitAll() + ); + http.csrf((csrf) -> csrf + .ignoringRequestMatchers("/**") + ); + + } + return http.build(); + } + + @Component + @RequiredArgsConstructor + static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper { + private static Collection<GrantedAuthority> extractAuthorities(Map<String, Object> claims) { + return ((Collection<String>) claims.get("authorities")).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + @Override + public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) { + Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); + + authorities.forEach(authority -> { + if (authority instanceof OidcUserAuthority oidcUserAuthority) { + mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims())); + } + }); + + return mappedAuthorities; + } + } } + diff --git a/src/main/java/eu/hbp/mip/controllers/ActiveUserAPI.java b/src/main/java/eu/hbp/mip/controllers/ActiveUserAPI.java index 9b8a42cbee92f2d962b8beb45545042989fefdd5..5aaeaa77ea48fd959bbee2205f246abb4ae6bbff 100644 --- a/src/main/java/eu/hbp/mip/controllers/ActiveUserAPI.java +++ b/src/main/java/eu/hbp/mip/controllers/ActiveUserAPI.java @@ -4,9 +4,8 @@ import eu.hbp.mip.models.DAOs.UserDAO; import eu.hbp.mip.services.ActiveUserService; import eu.hbp.mip.utils.Logger; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -20,18 +19,19 @@ public class ActiveUserAPI { this.activeUserService = activeUserService; } - @RequestMapping(method = RequestMethod.GET) - public ResponseEntity<UserDAO> getTheActiveUser() { - Logger logger = new Logger(activeUserService.getActiveUser().getUsername(), "(GET) /activeUser"); + @GetMapping + public ResponseEntity<UserDAO> getTheActiveUser(Authentication authentication) { + UserDAO activeUser = activeUserService.getActiveUser(authentication); + Logger logger = new Logger(activeUser.getUsername(), "(GET) /activeUser"); logger.LogUserAction("Loading the details of the activeUser"); - return ResponseEntity.ok(activeUserService.getActiveUser()); + return ResponseEntity.ok(activeUser); } - @RequestMapping(value = "/agreeNDA", method = RequestMethod.POST) - public ResponseEntity<UserDAO> activeUserServiceAgreesToNDA() { - Logger logger = new Logger(activeUserService.getActiveUser().getUsername(), "(GET) /activeUser/agreeNDA"); + @PostMapping(value = "/agreeNDA") + public ResponseEntity<UserDAO> activeUserServiceAgreesToNDA(Authentication authentication) { + Logger logger = new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(GET) /activeUser/agreeNDA"); logger.LogUserAction("The user agreed to the NDA"); - return ResponseEntity.ok(activeUserService.agreeToNDA()); + return ResponseEntity.ok(activeUserService.agreeToNDA(authentication)); } } diff --git a/src/main/java/eu/hbp/mip/controllers/AlgorithmsAPI.java b/src/main/java/eu/hbp/mip/controllers/AlgorithmsAPI.java index 771c61fb84b8f1ec6148fe05b9f26b92ce4f8d6f..181c5eb22bda24b15971bbd9a47b565e4766ad24 100644 --- a/src/main/java/eu/hbp/mip/controllers/AlgorithmsAPI.java +++ b/src/main/java/eu/hbp/mip/controllers/AlgorithmsAPI.java @@ -5,6 +5,8 @@ import eu.hbp.mip.services.ActiveUserService; import eu.hbp.mip.services.AlgorithmService; import eu.hbp.mip.utils.Logger; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -24,9 +26,9 @@ public class AlgorithmsAPI { this.algorithmService = algorithmService; } - @RequestMapping(method = RequestMethod.GET) - public ResponseEntity<List<ExaremeAlgorithmDTO>> getAlgorithms() { - Logger logger = new Logger(activeUserService.getActiveUser().getUsername(), "(GET) /algorithms"); + @GetMapping + public ResponseEntity<List<ExaremeAlgorithmDTO>> getAlgorithms(Authentication authentication) { + Logger logger = new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(GET) /algorithms"); logger.LogUserAction("Executing..."); List<ExaremeAlgorithmDTO> algorithms = algorithmService.getAlgorithms(); diff --git a/src/main/java/eu/hbp/mip/controllers/ExperimentAPI.java b/src/main/java/eu/hbp/mip/controllers/ExperimentAPI.java index c722918d92e9af9a6318902a87b0aa371f5eecd2..bdb82d8a20f528b23a71d6d19106a14d58e89981 100644 --- a/src/main/java/eu/hbp/mip/controllers/ExperimentAPI.java +++ b/src/main/java/eu/hbp/mip/controllers/ExperimentAPI.java @@ -4,7 +4,6 @@ package eu.hbp.mip.controllers; import eu.hbp.mip.models.DTOs.ExperimentDTO; import eu.hbp.mip.services.ActiveUserService; import eu.hbp.mip.services.ExperimentService; -import eu.hbp.mip.utils.JsonConverters; import eu.hbp.mip.utils.Logger; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -22,6 +21,7 @@ public class ExperimentAPI { private final ExperimentService experimentService; private final ActiveUserService activeUserService; + public ExperimentAPI( ExperimentService experimentService, ActiveUserService activeUserService @@ -30,19 +30,19 @@ public class ExperimentAPI { this.activeUserService = activeUserService; } - @RequestMapping(method = RequestMethod.GET) - public ResponseEntity<String> getExperiments(Authentication authentication, - @RequestParam(name = "name", required = false) String name, - @RequestParam(name = "algorithm", required = false) String algorithm, - @RequestParam(name = "shared", required = false) Boolean shared, - @RequestParam(name = "viewed", required = false) Boolean viewed, - @RequestParam(name = "includeShared", required = false, defaultValue = "true") boolean includeShared, - @RequestParam(name = "orderBy", required = false, defaultValue = "created") String orderBy, - @RequestParam(name = "descending", required = false, defaultValue = "true") Boolean descending, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size + @GetMapping + public ResponseEntity<Object> getExperiments(Authentication authentication, + @RequestParam(name = "name", required = false) String name, + @RequestParam(name = "algorithm", required = false) String algorithm, + @RequestParam(name = "shared", required = false) Boolean shared, + @RequestParam(name = "viewed", required = false) Boolean viewed, + @RequestParam(name = "includeShared", required = false, defaultValue = "true") boolean includeShared, + @RequestParam(name = "orderBy", required = false, defaultValue = "created") String orderBy, + @RequestParam(name = "descending", required = false, defaultValue = "true") Boolean descending, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size ) { - Map experiments = experimentService.getExperiments(authentication, + Map<String, Object> experiments = experimentService.getExperiments(authentication, name, algorithm, shared, @@ -52,43 +52,61 @@ public class ExperimentAPI { size, orderBy, descending, - new Logger(activeUserService.getActiveUser().getUsername(),"(GET) /experiments")); - return new ResponseEntity(experiments, HttpStatus.OK); + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(GET) /experiments")); + return new ResponseEntity<>(experiments, HttpStatus.OK); } - @RequestMapping(value = "/{uuid}", method = RequestMethod.GET) - public ResponseEntity<String> getExperiment(Authentication authentication, @PathVariable("uuid") String uuid) { - ExperimentDTO experimentDTO = experimentService.getExperiment(authentication, uuid, new Logger(activeUserService.getActiveUser().getUsername(),"(GET) /experiments/{uuid}")); - return new ResponseEntity<>(JsonConverters.convertObjectToJsonString(experimentDTO), HttpStatus.OK); + @GetMapping(value = "/{uuid}") + public ResponseEntity<ExperimentDTO> getExperiment(Authentication authentication, @PathVariable("uuid") String uuid) { + ExperimentDTO experimentDTO = experimentService.getExperiment( + authentication, uuid, + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(GET) /experiments/{uuid}") + ); + return new ResponseEntity<>(experimentDTO, HttpStatus.OK); } - @RequestMapping(method = RequestMethod.POST) - public ResponseEntity<String> createExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentDTO) { - experimentDTO = experimentService.createExperiment(authentication, experimentDTO, new Logger(activeUserService.getActiveUser().getUsername(),"(POST) /experiments")); - return new ResponseEntity<>(JsonConverters.convertObjectToJsonString(experimentDTO), HttpStatus.CREATED); + @PostMapping + public ResponseEntity<ExperimentDTO> createExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentDTO) { + experimentDTO = experimentService.createExperiment( + authentication, experimentDTO, + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(POST) /experiments") + ); + return new ResponseEntity<>(experimentDTO, HttpStatus.CREATED); } - @RequestMapping(value = "/{uuid}", method = RequestMethod.PATCH) - public ResponseEntity<String> updateExperiment(@RequestBody ExperimentDTO experimentDTO, @PathVariable("uuid") String uuid) { - experimentDTO = experimentService.updateExperiment(uuid, experimentDTO, new Logger(activeUserService.getActiveUser().getUsername(),"(PATCH) /experiments/{uuid}")); - return new ResponseEntity<>(JsonConverters.convertObjectToJsonString(experimentDTO), HttpStatus.OK); + @PatchMapping(value = "/{uuid}") + public ResponseEntity<ExperimentDTO> updateExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentDTO, @PathVariable("uuid") String uuid) { + experimentDTO = experimentService.updateExperiment( + authentication, + uuid, + experimentDTO, + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(PATCH) /experiments/{uuid}") + ); + return new ResponseEntity<>(experimentDTO, HttpStatus.OK); } @RequestMapping(value = "/{uuid}", method = RequestMethod.DELETE) - public ResponseEntity<String> deleteExperiment(@PathVariable("uuid") String uuid) { - experimentService.deleteExperiment(uuid, new Logger(activeUserService.getActiveUser().getUsername(), "(DELETE) /experiments/{uuid}")); + public ResponseEntity<String> deleteExperiment(Authentication authentication, @PathVariable("uuid") String uuid) { + experimentService.deleteExperiment( + authentication, + uuid, + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(DELETE) /experiments/{uuid}") + ); return new ResponseEntity<>(HttpStatus.OK); } - @RequestMapping(value = "/transient", method = RequestMethod.POST) - public ResponseEntity<String> createTransientExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentDTO) { - experimentDTO = experimentService. - runTransientExperiment(authentication, experimentDTO, new Logger(activeUserService.getActiveUser().getUsername(), "(POST) /experiments/transient")); - return new ResponseEntity<>(JsonConverters.convertObjectToJsonString(experimentDTO), HttpStatus.OK); + @PostMapping (value = "/transient") + public ResponseEntity<ExperimentDTO> createTransientExperiment(Authentication authentication, @RequestBody ExperimentDTO experimentDTO) { + experimentDTO = experimentService.runTransientExperiment( + authentication, + experimentDTO, + new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(POST) /experiments/transient") + ); + return new ResponseEntity<>(experimentDTO, HttpStatus.OK); } } \ No newline at end of file diff --git a/src/main/java/eu/hbp/mip/controllers/PathologiesAPI.java b/src/main/java/eu/hbp/mip/controllers/PathologiesAPI.java index 6e1b643c28818b77aec635f7566855816d92778a..73c12fbea00972faac57caca22024a846362b718 100644 --- a/src/main/java/eu/hbp/mip/controllers/PathologiesAPI.java +++ b/src/main/java/eu/hbp/mip/controllers/PathologiesAPI.java @@ -11,6 +11,7 @@ import eu.hbp.mip.utils.Exceptions.InternalServerError; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -34,15 +35,19 @@ public class PathologiesAPI { @Value("#{'${services.exareme2.cdesMetadataUrl}'}") private String exareme2CDEsMetadataUrl; + private final ActiveUserService activeUserService; - public PathologiesAPI(ActiveUserService activeUserService) { + private final ClaimUtils claimUtils; + + public PathologiesAPI(ActiveUserService activeUserService, ClaimUtils claimUtils) { this.activeUserService = activeUserService; + this.claimUtils = claimUtils; } - @RequestMapping(name = "/pathologies", method = RequestMethod.GET) + @GetMapping public ResponseEntity<String> getPathologies(Authentication authentication) { - Logger logger = new Logger(activeUserService.getActiveUser().getUsername(), "(GET) /pathologies"); + Logger logger = new Logger(activeUserService.getActiveUser(authentication).getUsername(), "(GET) /pathologies"); logger.LogUserAction("Loading pathologies ..."); Map<String, List<PathologyDTO.EnumerationDTO>> datasetsPerPathology = getExareme2DatasetsPerPathology(logger); @@ -70,7 +75,7 @@ public class PathologiesAPI { } logger.LogUserAction("Successfully loaded all authorized pathologies"); - return ResponseEntity.ok().body(gson.toJson(ClaimUtils.getAuthorizedPathologies(logger, authentication, pathologyDTOS))); + return ResponseEntity.ok().body(gson.toJson(claimUtils.getAuthorizedPathologies(logger, authentication, pathologyDTOS))); } public Map<String, List<PathologyDTO.EnumerationDTO>> getExareme2DatasetsPerPathology(Logger logger) { @@ -93,13 +98,11 @@ public class PathologiesAPI { exareme2CDEsMetadata.forEach( (pathology, cdePerDataset) -> { List<PathologyDTO.EnumerationDTO> pathologyDatasetDTOS = new ArrayList<>(); - Map datasetEnumerations = (Map) cdePerDataset.get("dataset").getEnumerations(); - datasetEnumerations.forEach((code, label) -> pathologyDatasetDTOS.add(new PathologyDTO.EnumerationDTO((String) code, (String) label))); + Map<String, String> datasetEnumerations = (Map<String, String>) cdePerDataset.get("dataset").getEnumerations(); + datasetEnumerations.forEach((code, label) -> pathologyDatasetDTOS.add(new PathologyDTO.EnumerationDTO(code, label))); datasetsPerPathology.put(pathology, pathologyDatasetDTOS); }); - - return datasetsPerPathology; } diff --git a/src/main/java/eu/hbp/mip/models/DAOs/ExperimentDAO.java b/src/main/java/eu/hbp/mip/models/DAOs/ExperimentDAO.java index 49b4f6670b0274dd0a506f3e2a35b41b9117d0aa..244b55d6f1f3d64514952335cead321525b2001b 100644 --- a/src/main/java/eu/hbp/mip/models/DAOs/ExperimentDAO.java +++ b/src/main/java/eu/hbp/mip/models/DAOs/ExperimentDAO.java @@ -3,16 +3,15 @@ package eu.hbp.mip.models.DAOs; import com.fasterxml.jackson.annotation.JsonInclude; import com.google.gson.Gson; import com.google.gson.annotations.Expose; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; -import javax.persistence.*; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.Date; +import java.util.UUID; + -/** - * Created by habfast on 21/04/16. - */ @Entity @Data @AllArgsConstructor @@ -25,7 +24,6 @@ public class ExperimentDAO { @Expose @Id @Column(columnDefinition = "uuid", updatable = false) - @org.hibernate.annotations.Type(type = "pg-uuid") private UUID uuid; @Expose diff --git a/src/main/java/eu/hbp/mip/models/DAOs/UserDAO.java b/src/main/java/eu/hbp/mip/models/DAOs/UserDAO.java index acbab0e320a294d5e17aefe74b612b5b8d6794bc..d1ab17988297d2d938d1c5818e7313a78a442bed 100644 --- a/src/main/java/eu/hbp/mip/models/DAOs/UserDAO.java +++ b/src/main/java/eu/hbp/mip/models/DAOs/UserDAO.java @@ -9,9 +9,9 @@ import com.google.gson.annotations.Expose; import lombok.AllArgsConstructor; import lombok.Data; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; @Entity @Data diff --git a/src/main/java/eu/hbp/mip/models/DTOs/UserDTO.java b/src/main/java/eu/hbp/mip/models/DTOs/UserDTO.java index 7550807246c05be32e70f67c544124aa96f5d292..448b50f1a506adce7bb7fae1bd68d1b7b0fed3b9 100644 --- a/src/main/java/eu/hbp/mip/models/DTOs/UserDTO.java +++ b/src/main/java/eu/hbp/mip/models/DTOs/UserDTO.java @@ -20,7 +20,6 @@ public class UserDTO { private String fullname; public UserDTO(){ - } public UserDTO(UserDAO userdao) { diff --git a/src/main/java/eu/hbp/mip/repositories/ExperimentRepository.java b/src/main/java/eu/hbp/mip/repositories/ExperimentRepository.java index fb119582801741bbaee66cea3e189bd8704d27a0..733737cee94ec5aaa56fe92af5405abe8c41b37c 100644 --- a/src/main/java/eu/hbp/mip/repositories/ExperimentRepository.java +++ b/src/main/java/eu/hbp/mip/repositories/ExperimentRepository.java @@ -12,12 +12,8 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.CrudRepository; import java.util.Date; -import java.util.Optional; import java.util.UUID; -/** - * Created by mirco on 11.07.16. - */ public interface ExperimentRepository extends CrudRepository<ExperimentDAO, UUID>, JpaSpecificationExecutor<ExperimentDAO> { @@ -69,30 +65,12 @@ public interface ExperimentRepository extends CrudRepository<ExperimentDAO, UUID try { save(experimentDAO); } catch (Exception e) { - logger.LogUserAction("Attempted to save changes to database but an error ocurred : " + e.getMessage() + "."); + logger.LogUserAction("Attempted to save changes to database but an error occurred : " + e.getMessage() + "."); throw new InternalServerError(e.getMessage()); } return experimentDAO; } - default void saveExperiment(ExperimentDAO experimentDAO, Logger logger) { - - logger.LogUserAction(" id : " + experimentDAO.getUuid()); - logger.LogUserAction(" algorithm : " + experimentDAO.getAlgorithm()); - logger.LogUserAction(" name : " + experimentDAO.getName()); - logger.LogUserAction(" historyId : " + experimentDAO.getWorkflowHistoryId()); - logger.LogUserAction(" status : " + experimentDAO.getStatus()); - - try { - save(experimentDAO); - } catch (Exception e) { - logger.LogUserAction("Attempted to save changes to database but an error ocurred : " + e.getMessage() + "."); - throw new InternalServerError(e.getMessage()); - } - - logger.LogUserAction("Saved experiment"); - } - default void finishExperiment(ExperimentDAO experimentDAO, Logger logger) { experimentDAO.setFinished(new Date()); diff --git a/src/main/java/eu/hbp/mip/repositories/ExperimentSpecifications.java b/src/main/java/eu/hbp/mip/repositories/ExperimentSpecifications.java index 08423757f2c937a2c0f823063e675726dc2335d0..ba1f7babccedb3a49906fff66fd1cd5fe732a77c 100644 --- a/src/main/java/eu/hbp/mip/repositories/ExperimentSpecifications.java +++ b/src/main/java/eu/hbp/mip/repositories/ExperimentSpecifications.java @@ -1,13 +1,14 @@ package eu.hbp.mip.repositories; import eu.hbp.mip.models.DAOs.ExperimentDAO; -import eu.hbp.mip.models.DAOs.UserDAO; import eu.hbp.mip.utils.Exceptions.BadRequestException; +import jakarta.persistence.criteria.*; +import lombok.NonNull; import org.springframework.data.jpa.domain.Specification; -import javax.persistence.criteria.*; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class ExperimentSpecifications { public static class ExperimentWithName implements Specification<ExperimentDAO> { @@ -20,7 +21,7 @@ public class ExperimentSpecifications { this.regExp = name; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (name == null) { return cb.isTrue(cb.literal(true)); } else { @@ -29,6 +30,16 @@ public class ExperimentSpecifications { return cb.like(cb.lower(root.get("name")), this.regExp.toLowerCase()); } + + @Override + public @NonNull Specification<ExperimentDAO> and(Specification<ExperimentDAO> other) { + return Specification.super.and(other); + } + + @Override + public @NonNull Specification<ExperimentDAO> or(Specification<ExperimentDAO> other) { + return Specification.super.or(other); + } } public static class ExperimentWithAlgorithm implements Specification<ExperimentDAO> { @@ -39,7 +50,7 @@ public class ExperimentSpecifications { this.algorithm = algorithm; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (algorithm == null) { return cb.isTrue(cb.literal(true)); } @@ -56,7 +67,7 @@ public class ExperimentSpecifications { this.viewed = viewed; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> query, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> query, @NonNull CriteriaBuilder cb) { if (viewed == null) { return cb.isTrue(cb.literal(true)); // always true = no filtering } @@ -72,7 +83,7 @@ public class ExperimentSpecifications { this.shared = shared; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (shared == null) { return cb.isTrue(cb.literal(true)); } @@ -88,11 +99,11 @@ public class ExperimentSpecifications { this.username = username; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (username == null) { return cb.isTrue(cb.literal(true)); } - Join<ExperimentDAO, UserDAO> experimentDAOUserDAOJoin = root.join("createdBy"); + Join<Object, Object> experimentDAOUserDAOJoin = root.join("createdBy"); return cb.equal(experimentDAOUserDAOJoin.get("username"), username); } } @@ -105,11 +116,11 @@ public class ExperimentSpecifications { this.shared = shared; } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (!shared) { return cb.isTrue(cb.literal(false)); } - return cb.equal(root.get("shared"), shared); + return cb.equal(root.get("shared"), true); } } @@ -123,13 +134,10 @@ public class ExperimentSpecifications { this.orderBy = orderBy; else throw new BadRequestException("Please provide proper column to order by."); - if (descending == null) - this.descending = true; - else - this.descending = descending; + this.descending = Objects.requireNonNullElse(descending, true); } - public Predicate toPredicate(Root<ExperimentDAO> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) { + public Predicate toPredicate(@NonNull Root<ExperimentDAO> root, @NonNull CriteriaQuery<?> criteriaQuery, @NonNull CriteriaBuilder cb) { if (descending) { criteriaQuery.orderBy(cb.desc(root.get(orderBy))); } else { diff --git a/src/main/java/eu/hbp/mip/repositories/UserRepository.java b/src/main/java/eu/hbp/mip/repositories/UserRepository.java index 99ae0b1dcfc237e875e7ba458175ae8441ec8c8e..cbfdc5b5bfd5bc392775470a961c2b1eabf5fd9d 100644 --- a/src/main/java/eu/hbp/mip/repositories/UserRepository.java +++ b/src/main/java/eu/hbp/mip/repositories/UserRepository.java @@ -3,11 +3,6 @@ package eu.hbp.mip.repositories; import eu.hbp.mip.models.DAOs.UserDAO; import org.springframework.data.repository.CrudRepository; -/** - * Created by mirco on 11.07.16. - */ - public interface UserRepository extends CrudRepository<UserDAO, String> { - UserDAO findByUsername(String username); } diff --git a/src/main/java/eu/hbp/mip/services/ActiveUserService.java b/src/main/java/eu/hbp/mip/services/ActiveUserService.java index 48252160ebd694b7eba73e66117b6370d5b8edb1..816a9a15b7d2585098b2074eb60b1098a507b60d 100644 --- a/src/main/java/eu/hbp/mip/services/ActiveUserService.java +++ b/src/main/java/eu/hbp/mip/services/ActiveUserService.java @@ -2,24 +2,25 @@ package eu.hbp.mip.services; import eu.hbp.mip.models.DAOs.UserDAO; import eu.hbp.mip.repositories.UserRepository; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.representations.IDToken; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.stereotype.Component; import java.util.Objects; + @Component @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) public class ActiveUserService { - @Value("#{'${authentication.enabled}'}") + @Value("${authentication.enabled}") private boolean authenticationIsEnabled; - private UserDAO user; + private UserDAO activeUserDetails; private final UserRepository userRepository; @@ -33,50 +34,48 @@ public class ActiveUserService { * * @return the userDAO */ - public UserDAO getActiveUser() { + public UserDAO getActiveUser(Authentication authentication) { + // TODO getActiveUser should not be called in so many places. + // It should be called in the controller and then passed internally in the other methods as UserDTO. + // TODO getActiveUser should return a UserDTO instead of a DAO - // User already loaded - if (user != null) - return user; + if (activeUserDetails != null) + return activeUserDetails; // If Authentication is OFF, create anonymous user with accepted NDA if (!authenticationIsEnabled) { - user = new UserDAO("anonymous", "anonymous", "anonymous@anonymous.com", "anonymousId"); - user.setAgreeNDA(true); - userRepository.save(user); - return user; + activeUserDetails = new UserDAO("anonymous", "anonymous", "anonymous@anonymous.com", "anonymousId"); + activeUserDetails.setAgreeNDA(true); + userRepository.save(activeUserDetails); + return activeUserDetails; } - // If authentication is ON get user info from Token - KeycloakPrincipal keycloakPrincipal = - (KeycloakPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - IDToken idToken = keycloakPrincipal.getKeycloakSecurityContext().getIdToken(); - user = new UserDAO(idToken.getPreferredUsername(), idToken.getName(), idToken.getEmail(), idToken.getId()); - - UserDAO userInDatabase = userRepository.findByUsername(user.getUsername()); - if (userInDatabase == null) { - userRepository.save(user); - return user; - } - if (!Objects.equals(user.getEmail(),userInDatabase.getEmail()) - || !Objects.equals(user.getFullname(),userInDatabase.getFullname()) - ) { - userInDatabase.setFullname(user.getFullname()); - userInDatabase.setEmail(user.getEmail()); + OidcUserInfo userinfo = ((DefaultOidcUser) authentication.getPrincipal()).getUserInfo(); + activeUserDetails = new UserDAO(userinfo.getPreferredUsername(), userinfo.getFullName(), userinfo.getEmail(), userinfo.getSubject()); + + UserDAO activeUserDatabaseDetails = userRepository.findByUsername(activeUserDetails.getUsername()); + if (activeUserDatabaseDetails != null) { + if ((!Objects.equals(activeUserDetails.getEmail(), activeUserDatabaseDetails.getEmail())) + || !Objects.equals(activeUserDetails.getFullname(), activeUserDatabaseDetails.getFullname()) + ) { + // Fullname and email are the only values allowed to change. + // username is the PK in our database and subjectid is the PK in keycloak + activeUserDatabaseDetails.setFullname(activeUserDetails.getFullname()); + activeUserDatabaseDetails.setEmail(activeUserDetails.getEmail()); + } + activeUserDetails = activeUserDatabaseDetails; } - - user = userInDatabase; - userRepository.save(user); - return user; + userRepository.save(activeUserDetails); + return activeUserDetails; } - public UserDAO agreeToNDA() { - getActiveUser(); + public UserDAO agreeToNDA(Authentication authentication) { + getActiveUser(authentication); - user.setAgreeNDA(true); - userRepository.save(user); + activeUserDetails.setAgreeNDA(true); + userRepository.save(activeUserDetails); - return user; + return activeUserDetails; } } diff --git a/src/main/java/eu/hbp/mip/services/ExperimentService.java b/src/main/java/eu/hbp/mip/services/ExperimentService.java index 081f44ea979571add6e1fdbf99f70067f1b83ead..7715def2481483126980c3069ab828ebc75f1c6b 100644 --- a/src/main/java/eu/hbp/mip/services/ExperimentService.java +++ b/src/main/java/eu/hbp/mip/services/ExperimentService.java @@ -31,21 +31,27 @@ import java.util.*; @Service public class ExperimentService { - private static final Gson gson = new Gson(); private final ActiveUserService activeUserService; private final AlgorithmService algorithmService; + private final ClaimUtils claimUtils; private final ExperimentRepository experimentRepository; - @Value("#{'${services.exareme.queryExaremeUrl}'}") + @Value("${services.exareme.queryExaremeUrl}") private String queryExaremeUrl; - @Value("#{'${services.exareme2.algorithmsUrl}'}") + @Value("${services.exareme2.algorithmsUrl}") private String exareme2AlgorithmsUrl; - @Value("#{'${authentication.enabled}'}") + @Value("${authentication.enabled}") private boolean authenticationIsEnabled; - public ExperimentService(ActiveUserService activeUserService, AlgorithmService algorithmService, ExperimentRepository experimentRepository) { + public ExperimentService( + ActiveUserService activeUserService, + AlgorithmService algorithmService, + ClaimUtils claimUtils, + ExperimentRepository experimentRepository + ) { this.algorithmService = algorithmService; this.activeUserService = activeUserService; + this.claimUtils = claimUtils; this.experimentRepository = experimentRepository; } @@ -60,19 +66,19 @@ public class ExperimentService { * @param page is the page that is required to be retrieved * @param size is the size of each page * @param orderBy is the column that is required to ordered by - * @param descending is a boolean to determine if the experiments will be order by descending or ascending + * @param descending is a boolean to determine if the experiments will be ordered by descending or ascending order * @param logger contains username and the endpoint. * @return a map experiments */ - public Map getExperiments(Authentication authentication, String name, String algorithm, Boolean shared, Boolean viewed, boolean includeShared, int page, int size, String orderBy, Boolean descending, Logger logger) { + public Map<String, Object> getExperiments(Authentication authentication, String name, String algorithm, Boolean shared, Boolean viewed, boolean includeShared, int page, int size, String orderBy, Boolean descending, Logger logger) { - UserDAO user = activeUserService.getActiveUser(); + UserDAO user = activeUserService.getActiveUser(authentication); logger.LogUserAction("Listing my experiments."); if (size > 50) throw new BadRequestException("Invalid size input, max size is 50."); Specification<ExperimentDAO> spec; - if (!authenticationIsEnabled || ClaimUtils.validateAccessRightsOnExperiments(authentication, logger)) { + if (!authenticationIsEnabled || claimUtils.validateAccessRightsOnALLExperiments(authentication, logger)) { spec = Specification .where(new ExperimentSpecifications.ExperimentWithName(name)) .and(new ExperimentSpecifications.ExperimentWithAlgorithm(algorithm)) @@ -118,7 +124,7 @@ public class ExperimentService { public ExperimentDTO getExperiment(Authentication authentication, String uuid, Logger logger) { ExperimentDAO experimentDAO; - UserDAO user = activeUserService.getActiveUser(); + UserDAO user = activeUserService.getActiveUser(authentication); logger.LogUserAction("Loading Experiment with uuid : " + uuid); @@ -127,7 +133,7 @@ public class ExperimentService { authenticationIsEnabled && !experimentDAO.isShared() && !experimentDAO.getCreatedBy().getUsername().equals(user.getUsername()) - && !ClaimUtils.validateAccessRightsOnExperiments(authentication, logger) + && !claimUtils.validateAccessRightsOnALLExperiments(authentication, logger) ) { logger.LogUserAction("Accessing Experiment is unauthorized."); throw new UnauthorizedException("You don't have access to the experiment."); @@ -159,10 +165,10 @@ public class ExperimentService { if (authenticationIsEnabled) { String experimentDatasets = getDatasetFromExperimentParameters(experimentDTO, logger); - ClaimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); + claimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); } - return createSynchronousExperiment(experimentDTO, algorithmEngineName, logger); + return createSynchronousExperiment(authentication, experimentDTO, algorithmEngineName, logger); } @@ -189,7 +195,7 @@ public class ExperimentService { if (authenticationIsEnabled) { String experimentDatasets = getDatasetFromExperimentParameters(experimentDTO, logger); - ClaimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); + claimUtils.validateAccessRightsOnDatasets(authentication, experimentDatasets, logger); } logger.LogUserAction("Completed, returning: " + experimentDTO); @@ -216,9 +222,9 @@ public class ExperimentService { * @param experimentDTO is the experiment information to be updated * @param logger contains username and the endpoint. */ - public ExperimentDTO updateExperiment(String uuid, ExperimentDTO experimentDTO, Logger logger) { + public ExperimentDTO updateExperiment(Authentication authentication, String uuid, ExperimentDTO experimentDTO, Logger logger) { ExperimentDAO experimentDAO; - UserDAO user = activeUserService.getActiveUser(); + UserDAO user = activeUserService.getActiveUser(authentication); logger.LogUserAction("Updating experiment with uuid : " + uuid + "."); experimentDAO = experimentRepository.loadExperiment(uuid, logger); @@ -259,9 +265,9 @@ public class ExperimentService { * @param uuid is the id of the experiment to be deleted * @param logger contains username and the endpoint. */ - public void deleteExperiment(String uuid, Logger logger) { + public void deleteExperiment(Authentication authentication, String uuid, Logger logger) { ExperimentDAO experimentDAO; - UserDAO user = activeUserService.getActiveUser(); + UserDAO user = activeUserService.getActiveUser(authentication); logger.LogUserAction("Deleting experiment with uuid : " + uuid + "."); experimentDAO = experimentRepository.loadExperiment(uuid, logger); @@ -408,11 +414,11 @@ public class ExperimentService { * @param logger contains username and the endpoint. * @return the experiment information that was retrieved from exareme */ - private ExperimentDTO createSynchronousExperiment(ExperimentDTO experimentDTO, String algorithmEngineName, Logger logger) { + private ExperimentDTO createSynchronousExperiment(Authentication authentication, ExperimentDTO experimentDTO, String algorithmEngineName, Logger logger) { logger.LogUserAction("Running the algorithm..."); - ExperimentDAO experimentDAO = experimentRepository.createExperimentInTheDatabase(experimentDTO, activeUserService.getActiveUser(), logger); + ExperimentDAO experimentDAO = experimentRepository.createExperimentInTheDatabase(experimentDTO, activeUserService.getActiveUser(authentication), logger); experimentDTO.setUuid(experimentDAO.getUuid()); logger.LogUserAction("Created experiment with uuid :" + experimentDAO.getUuid()); diff --git a/src/main/java/eu/hbp/mip/utils/CORSFilter.java b/src/main/java/eu/hbp/mip/utils/CORSFilter.java deleted file mode 100644 index 7d77b9022ef0855f0f5d6decf84601d0a00f75b0..0000000000000000000000000000000000000000 --- a/src/main/java/eu/hbp/mip/utils/CORSFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -package eu.hbp.mip.utils; - - -import javax.servlet.*; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - - -/** - * CORS Filter used only for development. - * - * Allows requests from all possible origins. - */ -public class CORSFilter implements Filter { - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - HttpServletResponse response = (HttpServletResponse) res; - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE"); - response.setHeader("Access-Control-Max-Age", "3600"); - response.setHeader("Access-Control-Allow-Headers", "*"); - response.setHeader("Access-Control-Request-Headers", "*"); - chain.doFilter(req, res); - } -} \ No newline at end of file diff --git a/src/main/java/eu/hbp/mip/utils/ClaimUtils.java b/src/main/java/eu/hbp/mip/utils/ClaimUtils.java index 650ac9c65fc4cf461159a3d94673d406cfe19dce..27b79af9fc5f291d5112b7fc1c22dcb0ced8893e 100644 --- a/src/main/java/eu/hbp/mip/utils/ClaimUtils.java +++ b/src/main/java/eu/hbp/mip/utils/ClaimUtils.java @@ -1,42 +1,56 @@ package eu.hbp.mip.utils; -import com.google.gson.Gson; import eu.hbp.mip.models.DTOs.PathologyDTO; -import eu.hbp.mip.utils.Exceptions.InternalServerError; import eu.hbp.mip.utils.Exceptions.UnauthorizedException; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; -import java.util.*; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +@Component public class ClaimUtils { - private static final Gson gson = new Gson(); + @Value("${authentication.all_datasets_allowed_claim}") + private String allDatasetsAllowedClaim; + + @Value("${authentication.all_experiments_allowed_claim}") + private String allExperimentsAllowedClaim; - public static String allDatasetsAllowedClaim() { - return "research_dataset_all"; + @Value("${authentication.dataset_claim_prefix}") + private String datasetClaimPrefix; + + private String getDatasetClaim(String datasetCode) { + return datasetClaimPrefix + datasetCode.toLowerCase(); } - public static String allExperimentsAllowedClaim() { - return "research_experiment_all"; + private static boolean hasRoleAccess(ArrayList<String> authorities, String role, Logger logger) { + List<String> userClaims = Arrays.asList(authorities.toString().toLowerCase() + .replaceAll("[\\s+\\]\\[]", "").split(",")); + + logger.LogUserAction("User Claims: " + userClaims); + return userClaims.contains(role.toLowerCase()); } - public static String getDatasetClaim(String datasetCode) { - return "research_dataset_" + datasetCode.toLowerCase(); + private static ArrayList<String> getAuthorityRoles(Authentication authentication) { + return (ArrayList<String>) authentication.getAuthorities().stream() + .map(Object::toString) + .collect(Collectors.toList()); } - public static void validateAccessRightsOnDatasets(Authentication authentication, + public void validateAccessRightsOnDatasets(Authentication authentication, String experimentDatasets, Logger logger) { - ArrayList<String> authorities = getKeycloakAuthorities(authentication, logger); + ArrayList<String> authorities = getAuthorityRoles(authentication); // Don't check for dataset claims if "super" claim exists allowing everything - if (!hasRoleAccess(authorities, ClaimUtils.allDatasetsAllowedClaim(), logger)) { + if (!hasRoleAccess(authorities, allDatasetsAllowedClaim, logger)) { for (String dataset : experimentDatasets.split(",")) { - String datasetRole = ClaimUtils.getDatasetClaim(dataset); + String datasetRole = getDatasetClaim(dataset); if (!hasRoleAccess(authorities, datasetRole, logger)) { logger.LogUserAction("You are not allowed to use dataset: " + dataset); throw new UnauthorizedException("You are not authorized to use these datasets."); @@ -46,70 +60,43 @@ public class ClaimUtils { } } - public static boolean validateAccessRightsOnExperiments(Authentication authentication, Logger logger) { - - ArrayList<String> authorities = getKeycloakAuthorities(authentication, logger); - - // Check for experiment_all claims - return hasRoleAccess(authorities, ClaimUtils.allExperimentsAllowedClaim(), logger); + public boolean validateAccessRightsOnALLExperiments(Authentication authentication, Logger logger) { + ArrayList<String> authorities = getAuthorityRoles(authentication); + return hasRoleAccess(authorities, allExperimentsAllowedClaim, logger); } - public static List<PathologyDTO> getAuthorizedPathologies(Logger logger, Authentication authentication, + public List<PathologyDTO> getAuthorizedPathologies(Logger logger, Authentication authentication, List<PathologyDTO> allPathologies) { - // --- Providing only the allowed pathologies/datasets to the user --- - logger.LogUserAction("Filter out the unauthorised datasets."); - ArrayList<String> authorities = getKeycloakAuthorities(authentication, logger); - - // If the "dataset_all" claim exists then return everything - if (hasRoleAccess(authorities, ClaimUtils.allDatasetsAllowedClaim(), logger)) { - return allPathologies; - } + ArrayList<String> authorities = getAuthorityRoles(authentication); List<PathologyDTO> userPathologies = new ArrayList<>(); - for (PathologyDTO curPathology : allPathologies) { - List<PathologyDTO.EnumerationDTO> userPathologyDatasets = new ArrayList<>(); - for (PathologyDTO.EnumerationDTO dataset : curPathology.getDatasets()) { - if (hasRoleAccess(authorities, ClaimUtils.getDatasetClaim(dataset.getCode()), logger)) { - logger.LogUserAction("Added dataset: " + dataset.getCode()); - userPathologyDatasets.add(dataset); + if (hasRoleAccess(authorities, allDatasetsAllowedClaim, logger)) { + userPathologies = allPathologies; + + } else { + for (PathologyDTO curPathology : allPathologies) { + List<PathologyDTO.EnumerationDTO> userPathologyDatasets = new ArrayList<>(); + for (PathologyDTO.EnumerationDTO dataset : curPathology.getDatasets()) { + if (hasRoleAccess(authorities, getDatasetClaim(dataset.getCode()), logger)) { + userPathologyDatasets.add(dataset); + } } - } - if (userPathologyDatasets.size() > 0) { - logger.LogUserAction("Added pathology '" + curPathology.getLabel() - + "' with datasets: '" + userPathologyDatasets + "'"); - - PathologyDTO userPathology = new PathologyDTO(); - userPathology.setCode(curPathology.getCode()); - userPathology.setLabel(curPathology.getLabel()); - userPathology.setMetadataHierarchyDTO(curPathology.getMetadataHierarchyDTO()); - userPathology.setDatasets(userPathologyDatasets); - userPathologies.add(userPathology); + if (userPathologyDatasets.size() > 0) { + PathologyDTO userPathology = new PathologyDTO(); + userPathology.setCode(curPathology.getCode()); + userPathology.setLabel(curPathology.getLabel()); + userPathology.setMetadataHierarchyDTO(curPathology.getMetadataHierarchyDTO()); + userPathology.setDatasets(userPathologyDatasets); + userPathologies.add(userPathology); + } } } + String userPathologiesSTR = userPathologies.stream().map(PathologyDTO::toString) + .collect(Collectors.joining(", ")); + logger.LogUserAction("Allowed pathologies: [" + userPathologiesSTR + "]"); return userPathologies; } - - private static boolean hasRoleAccess(ArrayList<String> authorities, String role, Logger logger) - { - List<String> userClaims = Arrays.asList(authorities.toString().toLowerCase() - .replaceAll("[\\s+\\]\\[]", "").split(",")); - - logger.LogUserAction("User Claims: " + userClaims); - return userClaims.contains(role.toLowerCase()); - } - - private static ArrayList<String> getKeycloakAuthorities(Authentication authentication, Logger logger){ - KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication; - KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) token.getPrincipal(); - if(keycloakPrincipal.getKeycloakSecurityContext().getIdToken().getOtherClaims().get("authorities") == null) - { - logger.LogUserAction("Your user has no roles."); - throw new InternalServerError("Your user has no roles."); - } - - return (ArrayList<String>)keycloakPrincipal.getKeycloakSecurityContext().getIdToken().getOtherClaims().get("authorities"); - } } diff --git a/src/main/java/eu/hbp/mip/utils/ControllerExceptionHandler.java b/src/main/java/eu/hbp/mip/utils/ControllerExceptionHandler.java index 09d39615150bad777d94689cdd96c2f24143da84..bd4fd42aefada816b535e173f9dd3b772e1dbea2 100644 --- a/src/main/java/eu/hbp/mip/utils/ControllerExceptionHandler.java +++ b/src/main/java/eu/hbp/mip/utils/ControllerExceptionHandler.java @@ -14,6 +14,8 @@ import java.util.Date; @ControllerAdvice public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { + public record ErrorMessage (int statusCode, Date timestamp, String message, String description) {} + @ExceptionHandler(ExperimentNotFoundException.class) public ResponseEntity<Object> handleExperimentNotFoundException(ExperimentNotFoundException ex, WebRequest request) { ErrorMessage message = new ErrorMessage( @@ -47,21 +49,6 @@ public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { return new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED); } - @ExceptionHandler(InternalServerError.class) - public ResponseEntity<ErrorMessage> handleInternalServerError(InternalServerError er, WebRequest request) { - logger.error("An unexpected exception occurred: " + er.getClass() + - "\nMessage: " + er.getMessage() + - "\nStacktrace: " + Arrays.toString(er.getStackTrace()).replaceAll(", ", "\n") - ); - ErrorMessage message = new ErrorMessage( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - new Date(), - er.getMessage(), - request.getDescription(false)); - - return new ResponseEntity<>(message, HttpStatus.INTERNAL_SERVER_ERROR); - } - @ExceptionHandler(NoContent.class) public ResponseEntity<ErrorMessage> handleNoContent(NoContent nc, WebRequest request) { ErrorMessage message = new ErrorMessage( @@ -73,7 +60,7 @@ public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { return new ResponseEntity<>(message, HttpStatus.NO_CONTENT); } - @ExceptionHandler(Exception.class) + @ExceptionHandler({InternalServerError.class, Exception.class}) public ResponseEntity<ErrorMessage> globalExceptionHandler(Exception ex, WebRequest request) { logger.error("An unexpected exception occurred: " + ex.getClass() + "\nMessage: " + ex.getMessage() + diff --git a/src/main/java/eu/hbp/mip/utils/CustomResourceLoader.java b/src/main/java/eu/hbp/mip/utils/CustomResourceLoader.java index b7365762468eb400aaec9172e13ae3960986a773..d4d5d407f9be349ecd2a46abdf45b1bc4ae02598 100644 --- a/src/main/java/eu/hbp/mip/utils/CustomResourceLoader.java +++ b/src/main/java/eu/hbp/mip/utils/CustomResourceLoader.java @@ -1,5 +1,6 @@ package eu.hbp.mip.utils; +import lombok.NonNull; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -10,7 +11,7 @@ public class CustomResourceLoader implements ResourceLoaderAware { private ResourceLoader resourceLoader; - public void setResourceLoader(ResourceLoader resourceLoader) { + public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } diff --git a/src/main/java/eu/hbp/mip/utils/ErrorMessage.java b/src/main/java/eu/hbp/mip/utils/ErrorMessage.java deleted file mode 100644 index fdaac15921a846f710f89398a799c41e7a6befa8..0000000000000000000000000000000000000000 --- a/src/main/java/eu/hbp/mip/utils/ErrorMessage.java +++ /dev/null @@ -1,34 +0,0 @@ -package eu.hbp.mip.utils; - - -import java.util.Date; - -public class ErrorMessage { - private final int statusCode; - private final Date timestamp; - private final String message; - private final String description; - - public ErrorMessage(int statusCode, Date timestamp, String message, String description) { - this.statusCode = statusCode; - this.timestamp = timestamp; - this.message = message; - this.description = description; - } - - public int getStatusCode() { - return statusCode; - } - - public Date getTimestamp() { - return timestamp; - } - - public String getMessage() { - return message; - } - - public String getDescription() { - return description; - } -} diff --git a/src/main/java/eu/hbp/mip/utils/HTTPUtil.java b/src/main/java/eu/hbp/mip/utils/HTTPUtil.java index fdd7a9179cbb95bccb73277212a2dd29b5b20b41..2ef8e58809e88d0c50eecb4659e0186f1bff3dbc 100644 --- a/src/main/java/eu/hbp/mip/utils/HTTPUtil.java +++ b/src/main/java/eu/hbp/mip/utils/HTTPUtil.java @@ -18,19 +18,14 @@ public class HTTPUtil { throw new IllegalAccessError("HTTPUtil class"); } - public static int sendGet(String url, StringBuilder resp) throws IOException { - return sendHTTP(url, "", resp, "GET", null); + public static void sendGet(String url, StringBuilder resp) throws IOException { + sendHTTP(url, "", resp, "GET", null); } public static int sendPost(String url, String query, StringBuilder resp) throws IOException { return sendHTTP(url, query, resp, "POST", null); } - public static int sendAuthorizedHTTP(String url, String query, StringBuilder resp, String httpVerb, - String authorization) throws IOException { - return sendHTTP(url, query, resp, httpVerb, authorization); - } - private static int sendHTTP(String url, String query, StringBuilder resp, String httpVerb, String authorization) throws IOException { diff --git a/src/main/java/eu/hbp/mip/utils/InputStreamConverter.java b/src/main/java/eu/hbp/mip/utils/InputStreamConverter.java index ebb8f9d999dd73381ae9f203c12361fb381cba9b..050ba74604f9dc9b658ee439e4ed8a94ac518414 100644 --- a/src/main/java/eu/hbp/mip/utils/InputStreamConverter.java +++ b/src/main/java/eu/hbp/mip/utils/InputStreamConverter.java @@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets; public class InputStreamConverter { // Pure Java + // https://stackoverflow.com/questions/309424/how-do-i-read-convert-an-inputstream-into-a-string-in-java public static String convertInputStreamToString(InputStream inputStream) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; @@ -16,6 +17,6 @@ public class InputStreamConverter { result.write(buffer, 0, length); } - return result.toString(StandardCharsets.UTF_8.name()); + return result.toString(StandardCharsets.UTF_8); } } diff --git a/src/main/java/eu/hbp/mip/utils/Logger.java b/src/main/java/eu/hbp/mip/utils/Logger.java index 7ea51d74f008f3e5eef6760fc8693dc5d69d5240..60e67c7aa8815ccb10b91909871024da4bb48a4a 100644 --- a/src/main/java/eu/hbp/mip/utils/Logger.java +++ b/src/main/java/eu/hbp/mip/utils/Logger.java @@ -2,8 +2,6 @@ package eu.hbp.mip.utils; import org.slf4j.LoggerFactory; -import java.util.UUID; - public class Logger { @@ -11,7 +9,7 @@ public class Logger { private final String username; private final String endpoint; - public Logger(String username, String endpoint){ + public Logger(String username, String endpoint) { this.username = username; this.endpoint = endpoint; } @@ -21,11 +19,4 @@ public class Logger { + "Endpoint -> " + endpoint + " ," + "Info -> " + actionInfo); } - - // Deprecated, should be removed - public static void LogBackgroundAction(String experimentName, UUID experimentId, String actionInfo) { - LOGGER.info(" Experiment -> " + experimentName - + "(" + experimentId + ") ," - + "Info -> " + actionInfo); - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 89fb72844192a95de2b4d216623980224eb3fa97..0f6d06f61412fb761a7dd0af894b95c251187e10 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,24 +1,26 @@ # Configuration for development purposes + +### EMBEDDED SERVER CONFIGURATION ### +server: + servlet: + contextPath: "/services" + port: 8080 + forward-headers-strategy: native + + ### LOG LEVELS ### logging: level: - root: "ERROR" - org: "ERROR" + root: "INFO" eu: hbp: "DEBUG" -### AUTHENTICATION ### -authentication: - enabled: false - - ### DATABASE CONFIGURATION ### spring: - portal-datasource: + datasource: url: "jdbc:postgresql://127.0.0.1:5433/portal" - schema: "public" username: "portal" password: "portalpwd" driver-class-name: org.postgresql.Driver @@ -28,12 +30,33 @@ spring: bootstrap-mode: default jpa: hibernate: - dialect: org.hibernate.dialect.PostgreSQL9Dialect ddl-auto: validate - mvc: pathmatch: matching-strategy: ant_path_matcher + security: + oauth2: + client: + registration: + keycloak: + authorization-grant-type: authorization_code + client-id: MIP + client-secret: dae83a6b-c769-4186-8383-f0984c6edf05 + provider: keycloak + scope: openid + redirect-uri: http://172.17.0.1/${server.servlet.contextPath}/login/oauth2/code/MIP + provider: + keycloak: + user-name-attribute: preferred_username + issuer-uri: http://172.17.0.1/auth/realms/MIP + + +### AUTHENTICATION ### +authentication: + enabled: 0 + all_datasets_allowed_claim: research_dataset_all + all_experiments_allowed_claim: research_experiment_all + dataset_claim_prefix: research_dataset_ ### EXTERNAL SERVICES ### @@ -48,28 +71,7 @@ services: algorithmsUrl: "http://127.0.0.1:9090/mining/algorithms.json" -### KEYCLOAK ### -keycloak: - enabled: true - auth-server-url: "https://iam.humanbrainproject.eu/auth" - realm: "MIP" - resource: "mipfedqa" - use-resource-role-mappings: true - enable-basic-auth: true - credentials: - secret: "dae83a6b-c769-4186-8383-f0984c6edf05" - principal-attribute: "preferred_username" - - ### EXTERNAL FILES ### # Files are loaded from the resources files: disabledAlgorithms_json: "classPath:/disabledAlgorithms.json" - - -### EMBEDDED SERVER CONFIGURATION ### -server: - servlet: - contextPath: "/services" - port: 8080 - forward-headers-strategy: native