diff --git a/pom.xml b/pom.xml index d5c06e0a8..2d5f94257 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,6 @@ knowledge of the CeCILL-B license and that you accept its terms. logback-classic 1.2.3 - @@ -217,6 +216,8 @@ knowledge of the CeCILL-B license and that you accept its terms. 2.2.0 test + + diff --git a/vip-api/pom.xml b/vip-api/pom.xml index 86f470023..27ce4a663 100644 --- a/vip-api/pom.xml +++ b/vip-api/pom.xml @@ -98,6 +98,11 @@ knowledge of the CeCILL-B license and that you accept its terms. spring-security-web ${springsecurity.version} + + org.springframework.security + spring-security-oauth2-resource-server + ${springoauth.version} + diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/ApiPropertiesInitializer.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/ApiPropertiesInitializer.java index e52b972fb..058fa9e31 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/ApiPropertiesInitializer.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/ApiPropertiesInitializer.java @@ -96,7 +96,7 @@ private void verifyProperties() { if (env.getProperty(KEYCLOAK_ACTIVATED, Boolean.class, Boolean.FALSE)) { - logger.info("Keycloak/OIDC activated, but this has no effect yet"); + logger.info("Keycloak/OIDC activated"); } else { logger.info("Keycloak/OIDC NOT active"); } diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/SpringWebConfig.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/SpringWebConfig.java index da9d8f103..f29c10ec2 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/SpringWebConfig.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/SpringWebConfig.java @@ -63,15 +63,6 @@ public SpringWebConfig(Environment env, VipConfigurer vipConfigurer) { this.vipConfigurer = vipConfigurer; } - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - // Otherwise all that follow a dot in an URL is considered an extension and removed - // It's a problem for URL like "/pipelines/gate/3.2 - // The below will become the default values in Spring 5.3 - // Safe to use in 5.2 as long as disabling pattern match - configurer.setUseSuffixPatternMatch(false); - } - @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // necessary in the content negotiation stuff of carmin data diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/DataApiBusiness.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/DataApiBusiness.java index 08c04378a..13a31d67d 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/DataApiBusiness.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/business/DataApiBusiness.java @@ -52,6 +52,7 @@ import fr.insalyon.creatis.vip.datamanager.server.business.TransferPoolBusiness; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.input.ReaderInputStream; +import org.apache.commons.io.input.ReaderInputStream.Builder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -391,8 +392,12 @@ private boolean isOperationOver(String operationId, User user) private void writeFileFromBase64(String base64Content, String localFilePath) throws ApiException { Base64.Decoder decoder = Base64.getDecoder(); StringReader stringReader = new StringReader(base64Content); - InputStream inputStream = new ReaderInputStream(stringReader, StandardCharsets.UTF_8); - try (InputStream base64InputStream = decoder.wrap(inputStream)) { + try { + InputStream inputStream = ReaderInputStream.builder() + .setReader(new StringReader(base64Content)) + .setCharset(StandardCharsets.UTF_8) + .get(); + InputStream base64InputStream = decoder.wrap(inputStream); Files.copy(base64InputStream, Paths.get(localFilePath)); } catch (IOException e) { logger.error("Error writing base64 file in {}", localFilePath, e); diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/ApiSecurityConfig.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/ApiSecurityConfig.java index 2518f79a5..9b214560e 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/ApiSecurityConfig.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/ApiSecurityConfig.java @@ -38,6 +38,9 @@ import fr.insalyon.creatis.vip.api.security.apikey.ApikeyAuthenticationProvider; import fr.insalyon.creatis.vip.core.client.bean.User; +import fr.insalyon.creatis.vip.api.security.oidc.OidcConfig; +import fr.insalyon.creatis.vip.api.security.oidc.OidcResolver; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -47,9 +50,6 @@ import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.Customizer; @@ -60,21 +60,17 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.firewall.DefaultHttpFirewall; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher; import java.util.function.Supplier; -import java.util.ArrayList; - -import static fr.insalyon.creatis.vip.api.CarminProperties.KEYCLOAK_ACTIVATED; /** * VIP API configuration for API key and OIDC authentications. * * Authenticates /rest requests with either: - * - a static per-user API key (ApikeyAuthenticationFilter) - * - or an OIDC Bearer token (ex-Keycloak). This part is currently work-in-progress, - * with org.keycloak currently removed, and proper OIDC connector not implemented yet. + * - a static per-user API key, in apikeyAuthenticationFilter() + * - or an OIDC Bearer token, in oauth2ResourceServer() */ @Configuration @EnableWebSecurity @@ -84,53 +80,65 @@ public class ApiSecurityConfig { private final Environment env; private final VipAuthenticationEntryPoint vipAuthenticationEntryPoint; - private final AuthenticationManager vipAuthenticationManager; + private final ApikeyAuthenticationProvider apikeyAuthenticationProvider; + private final OidcConfig oidcConfig; + private final OidcResolver oidcResolver; @Autowired public ApiSecurityConfig( Environment env, ApikeyAuthenticationProvider apikeyAuthenticationProvider, - VipAuthenticationEntryPoint vipAuthenticationEntryPoint) { + VipAuthenticationEntryPoint vipAuthenticationEntryPoint, + OidcConfig oidcConfig, OidcResolver oidcResolver) { this.env = env; this.vipAuthenticationEntryPoint = vipAuthenticationEntryPoint; - // Build our AuthenticationManager instance, with one provider for each authentication method - ArrayList providers = new ArrayList<>(); - providers.add(apikeyAuthenticationProvider); - // providers.add(oidcAuthenticationProvider); - this.vipAuthenticationManager = new ProviderManager(providers); - } - - protected boolean isOIDCActive() { - return env.getProperty(KEYCLOAK_ACTIVATED, Boolean.class, Boolean.FALSE); + this.apikeyAuthenticationProvider = apikeyAuthenticationProvider; + this.oidcConfig = oidcConfig; + this.oidcResolver = oidcResolver; } @Bean @Order(1) public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { - // It is required to used AntPathRequestMatcher.antMatcher() everywhere below, - // otherwise Spring users MvcRequestMatcher as the default requestMatchers implementation. + // Spring Security configuration for /rest API endpoints, common to both API key and OIDC authentications. + // Note that it is required to used AntPathRequestMatcher.antMatcher() everywhere below, + // otherwise Spring uses MvcRequestMatcher as the default requestMatchers implementation. http - .securityMatcher(AntPathRequestMatcher.antMatcher("/rest/**")) + .securityMatcher(antMatcher("/rest/**")) .authorizeHttpRequests((authorize) -> authorize - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/platform")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/authenticate")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/session")).permitAll() + .requestMatchers(antMatcher("/rest/platform")).permitAll() + .requestMatchers(antMatcher("/rest/authenticate")).permitAll() + .requestMatchers(antMatcher("/rest/session")).permitAll() .requestMatchers(new RegexRequestMatcher("/rest/pipelines\\?public", "GET")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/publications")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/reset-password")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/register")).permitAll() - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/executions/{executionId}/summary")).hasAnyRole("SERVICE") - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/statistics/**")).hasAnyRole("ADVANCED", "ADMINISTRATOR") - .requestMatchers(AntPathRequestMatcher.antMatcher("/rest/**")).authenticated() + .requestMatchers(antMatcher("/rest/publications")).permitAll() + .requestMatchers(antMatcher("/rest/reset-password")).permitAll() + .requestMatchers(antMatcher("/rest/register")).permitAll() + .requestMatchers(antMatcher("/rest/executions/{executionId}/summary")).hasAnyRole("SERVICE") + .requestMatchers(antMatcher("/rest/statistics/**")).hasAnyRole("ADVANCED", "ADMINISTRATOR") + .requestMatchers(antMatcher("/rest/**")).authenticated() .anyRequest().permitAll() ) - .addFilterBefore(apikeyAuthenticationFilter(), BasicAuthenticationFilter.class) - //.addFilterBefore(oidcAuthenticationFilter(), BasicAuthenticationFilter.class) .exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(vipAuthenticationEntryPoint)) // session must be activated otherwise OIDC auth info will be lost when accessing /loginEgi // .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .cors(Customizer.withDefaults()) .headers((headers) -> headers.frameOptions((frameOptions) -> frameOptions.sameOrigin())) .csrf((csrf) -> csrf.disable()); + // API key authentication always active + http.addFilterBefore(apikeyAuthenticationFilter(), BasicAuthenticationFilter.class); + // OIDC Bearer token authentication, if enabled + if (oidcConfig.isOIDCActive()) { + // We configure each OIDC server with issuerLocation instead of jwks_uri: on first token verification, + // this does two requests to the relevant OIDC server (obtained from the JWT "iss" field): + // - a GET .well-known/openid-configuration request to the OIDC server, to get this server jwks_uri + // - then a GET on the jwks_uri, to get the public key which is then used to verify the token + // Note that these two requests are done just once per OIDC server (not once per inbound API request). + // We also use a customized authenticationManagerResolver instead of simpler JwtDecoder bean, so that: + // - requests to the OIDC server happen at inbound-request-time instead of boot, and can be retried on failure + // - multiple servers can be supported + // - on successful authentication, Jwt principal is converted to a User principal, so DB lookup happens only once + http.oauth2ResourceServer((oauth2) -> oauth2 + .authenticationManagerResolver(oidcResolver.getAuthenticationManagerResolver())); + } return http.build(); } @@ -138,9 +146,10 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { public ApikeyAuthenticationFilter apikeyAuthenticationFilter() throws Exception { return new ApikeyAuthenticationFilter( env.getRequiredProperty(CarminProperties.APIKEY_HEADER_NAME), - vipAuthenticationEntryPoint, vipAuthenticationManager); + vipAuthenticationEntryPoint, apikeyAuthenticationProvider); } + // Provide authenticated user after a successful API key or OIDC token authentication @Service public static class CurrentUserProvider implements Supplier { @@ -148,26 +157,19 @@ public static class CurrentUserProvider implements Supplier { @Override public User get() { - Authentication authentication = - SecurityContextHolder.getContext().getAuthentication(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { return null; } - User user = getApikeyUser(authentication); - if (user != null) { - return user; - } - // user = getOidcUser(authentication); - return null; - } - - private User getApikeyUser(Authentication authentication) { - if ( ! (authentication.getPrincipal() instanceof SpringApiPrincipal)) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SpringApiPrincipal) { // API key authentication + return ((SpringApiPrincipal) principal).getVipUser(); + } else if (principal instanceof User) { // OIDC authentication + return (User) principal; + } else { // no resolvable user found (shouldn't happen) + logger.error("CurrentUserProvider: unknown principal class {}", principal.getClass()); return null; } - SpringApiPrincipal springCompatibleUser = - (SpringApiPrincipal) authentication.getPrincipal(); - return springCompatibleUser.getVipUser(); } } diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/VipAuthenticationEntryPoint.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/VipAuthenticationEntryPoint.java index 6e4af2519..79bcf16d9 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/VipAuthenticationEntryPoint.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/VipAuthenticationEntryPoint.java @@ -72,7 +72,7 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - // keycloak may already have set it up + // OIDC resource server handler may already have set this header if ( ! response.containsHeader("WWW-Authenticate")) { response.addHeader("WWW-Authenticate", "API-key"); } diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/apikey/ApikeyAuthenticationFilter.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/apikey/ApikeyAuthenticationFilter.java index cc00c683b..3a2a821b5 100644 --- a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/apikey/ApikeyAuthenticationFilter.java +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/apikey/ApikeyAuthenticationFilter.java @@ -33,8 +33,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; @@ -59,21 +59,21 @@ public class ApikeyAuthenticationFilter extends OncePerRequestFilter { private final String apikeyHeader; private final AuthenticationEntryPoint authenticationEntryPoint; - private final AuthenticationManager authenticationManager; + private final AuthenticationProvider authenticationProvider; public ApikeyAuthenticationFilter( String apikeyHeader, AuthenticationEntryPoint authenticationEntryPoint, - AuthenticationManager authenticationManager) { + AuthenticationProvider authenticationProvider) { this.apikeyHeader = apikeyHeader; this.authenticationEntryPoint = authenticationEntryPoint; - this.authenticationManager = authenticationManager; + this.authenticationProvider = authenticationProvider; } @Override public void afterPropertiesSet() { - Assert.notNull(this.authenticationManager, - "An AuthenticationManager is required"); + Assert.notNull(this.authenticationProvider, + "An AuthenticationProvider is required"); Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required"); @@ -96,8 +96,7 @@ protected void doFilterInternal( logger.debug("apikey header found."); ApikeyAuthenticationToken authRequest = new ApikeyAuthenticationToken(apikey); - Authentication authResult = this.authenticationManager - .authenticate(authRequest); + Authentication authResult = this.authenticationProvider.authenticate(authRequest); logger.debug("Authentication success for : " + authResult); diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcConfig.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcConfig.java new file mode 100644 index 000000000..a8ad59a04 --- /dev/null +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcConfig.java @@ -0,0 +1,105 @@ +package fr.insalyon.creatis.vip.api.security.oidc; + +import fr.insalyon.creatis.vip.core.server.business.BusinessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.env.Environment; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.net.URI; + +import fr.insalyon.creatis.vip.api.CarminProperties; + +@Service +public class OidcConfig { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final Environment env; + private final Map servers; + + public class OidcServer { + public final String issuer; + public final Boolean useResourceRoleMappings; + public final String resourceName; + + OidcServer(String issuer, Boolean useResourceRoleMappings, String resourceName) { + this.issuer = issuer; + if (useResourceRoleMappings && (resourceName == null || resourceName.isEmpty())) { + throw new IllegalArgumentException("useResourceRoleMappings enabled but no resourceName defined"); + } + this.useResourceRoleMappings = useResourceRoleMappings; + this.resourceName = resourceName; + } + } + + @Autowired + public OidcConfig(Environment env, Resource vipConfigFolder) throws IOException, URISyntaxException, BusinessException { + this.env = env; + // Build the list of OIDC servers from config file. If OIDC is disabled, just create an empty list. + HashMap servers = new HashMap<>(); + if (isOIDCActive()) { + // Many errors are possible here: + // IOException in getFile() (can't read file), JsonProcessingException in readTree() (bad JSON), + // URISyntaxException in URI() (bad URL syntax), NullPointerException in get().asText() (missing JSON key), + // and probably more... + // As long as isOIDCActive(), we do not try to handle them: just let them bubble up, causing a boot-time error. + final String basename = "keycloak.json"; + // read and parse keycloak.json file into one OidcServer config + File file = vipConfigFolder.getFile().toPath().resolve(basename).toFile(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(file); + // mandatory fields: just check for their presence, content will be validated by URI() below + String baseURL, realm; + if (node.hasNonNull("auth-server-url") && node.hasNonNull("realm")) { + baseURL = node.get("auth-server-url").asText(); + realm = node.get("realm").asText(); + } else { + throw new BusinessException("Failed parsing " + basename + ": missing mandatory fields"); + } + // optional fields + Boolean useResourceRoleMappings; + if (node.hasNonNull("use-resource-role-mapping")) { + useResourceRoleMappings = node.get("use-resource-role-mapping").asBoolean(); + } else { + useResourceRoleMappings = false; + } + String resourceName; + if (node.hasNonNull("resource")) { + resourceName = node.get("resource").asText(); + } else { + resourceName = ""; + } + + // Build OIDC server URL from auth-server-url + realm name (this is Keycloak-specific). + // We use URI.resolve() instead of just concatenation, to correctly handle optional '/' at the end of baseURL. + URI url = new URI(baseURL).resolve("realms/" + realm); + String issuer = url.toASCIIString(); + servers.put(issuer, new OidcServer(issuer, useResourceRoleMappings, resourceName)); + } + this.servers = servers; + } + + public boolean isOIDCActive() { + return env.getProperty(CarminProperties.KEYCLOAK_ACTIVATED, Boolean.class, Boolean.FALSE); + } + + // list of issuers URLs + public Collection getServers() { + return servers.keySet(); + } + + // get resource name property for a given issuer URL + public OidcServer getServerConfig(String issuer) { + return servers.get(issuer); + } +} diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcResolver.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcResolver.java new file mode 100644 index 000000000..9d343ba65 --- /dev/null +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcResolver.java @@ -0,0 +1,135 @@ +package fr.insalyon.creatis.vip.api.security.oidc; + +import fr.insalyon.creatis.vip.core.client.bean.User; +import fr.insalyon.creatis.vip.core.server.business.BusinessException; +import fr.insalyon.creatis.vip.core.server.business.ConfigurationBusiness; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class OidcResolver { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final ConfigurationBusiness configurationBusiness; + private final OidcConfig oidcConfig; + + @Autowired + public OidcResolver(ConfigurationBusiness configurationBusiness, OidcConfig oidcConfig) { + this.configurationBusiness = configurationBusiness; + this.oidcConfig = oidcConfig; + } + + // Common "Bad credentials" exception for Jwt to User resolution errors + private BadCredentialsException authError() { + return new BadCredentialsException( + SpringSecurityMessageSource.getAccessor().getMessage( + "AbstractUserDetailsAuthenticationProvider.badCredentials", + "Bad credentials")); + } + + // Get DB User from authenticated JWT, using email-based resolution + private User getVipUser(Jwt jwt) throws BadCredentialsException { + String email = jwt.getClaim("email"); + if (email == null) { // no email field in token + logger.warn("Can't authenticate from OIDC token: no email in token"); + throw authError(); + } + User vipUser; + try { + vipUser = configurationBusiness.getUserWithGroups(email); + } catch (BusinessException e) { // DB lookup failed + logger.error("Error when getting user from OIDC token: doing as if there is an auth error", e); + throw authError(); + } + if (vipUser == null) { // user not found + logger.warn("Can't authenticate from OIDC token: user does not exist in VIP: {}", email); + throw authError(); + } + if (vipUser.isAccountLocked()) { // account locked + logger.info("Can't authenticate from OIDC token: account is locked: {}", email); + throw authError(); + } + return vipUser; + } + + // Create authorities list from jwt claims. + // Parsing realm_access.roles or resource_access..roles is Keycloak-specific. + @SuppressWarnings("unchecked") + private List parseAuthorities(User user, Jwt jwt) { + List roles = new ArrayList<>(); // default to no roles + // At this point, jwt has already been verified by Spring resource server, so we assume the issuer is known (server != null). + // Also, we only handle Keycloak-generated tokens for now, so we assume realm_access and roles fields to exist. + // Thus, the only error case we handle in token parsing below is a mismatch on resourceName: + // any other issue will deliberately cause an exception and request error 500. + OidcConfig.OidcServer server = oidcConfig.getServerConfig(jwt.getIssuer().toString()); + if (server.useResourceRoleMappings) { // use resource-level roles + String resource = server.resourceName; + Map resourceAccess = jwt.getClaimAsMap("resource_access"); + Map realmAccess = (Map) resourceAccess.get(resource); + if (realmAccess != null) { + roles = (List) realmAccess.get("roles"); + } else { + logger.warn("Can't get roles for user {}: resource '{}' not found in token, defaulting to no roles", + user.getEmail(), resource); + } + } else { // use realm-level roles + Map realmAccess = jwt.getClaimAsMap("realm_access"); + roles = (List) realmAccess.get("roles"); + } + // here we could also map an authority from user level, as done by Apikey auth: + // roles.add("ROLE_" + user.getLevel().name().toUpperCase()); + // but the existing Keycloak only used JWT-provided authorities + return AuthorityUtils.createAuthorityList(roles); + } + + // Custom converter class to transform Spring-provided Jwt into our own OidcToken, so that we can resolve and cache + // User as the authentication principal, and thus avoid multiple DB lookups per token within a given request. + static private class OidcJwtConverter implements Converter { + private final OidcResolver oidcResolver; + public OidcJwtConverter(OidcResolver oidcResolver) { + this.oidcResolver = oidcResolver; + } + public AbstractAuthenticationToken convert(Jwt jwt) { + // At this point, jwt has been checked for: iss, exp, nbf: i.e. trusted issuer + validity dates. + // We could also, but do not, check aud: this is optional in OIDC spec, and wasn't done by previous implementation. + // Now we have to resolve DB user, and map authorizations. + User user = oidcResolver.getVipUser(jwt); + List authorities = oidcResolver.parseAuthorities(user, jwt); + return new OidcToken(user, jwt, authorities); + } + } + + // Create a JwtIssuerAuthenticationManagerResolver instance with our custom converter. + // This is functionally equivalent to jwt.jwtAuthenticationConverter(), but for multi-tenant environment + // (see https://github.com/spring-projects/spring-security/issues/9096#issuecomment-973224956). + public JwtIssuerAuthenticationManagerResolver getAuthenticationManagerResolver() { + OidcJwtConverter converter = new OidcJwtConverter(this); + Map managers = new HashMap<>(); + for (String issuer : oidcConfig.getServers()) { + JwtDecoder decoder = new SupplierJwtDecoder(() -> JwtDecoders.fromIssuerLocation(issuer)); + JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder); + provider.setJwtAuthenticationConverter(converter); + managers.put(issuer, provider::authenticate); + } + return new JwtIssuerAuthenticationManagerResolver(managers::get); + } +} diff --git a/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcToken.java b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcToken.java new file mode 100644 index 000000000..321c38ca0 --- /dev/null +++ b/vip-api/src/main/java/fr/insalyon/creatis/vip/api/security/oidc/OidcToken.java @@ -0,0 +1,41 @@ +package fr.insalyon.creatis.vip.api.security.oidc; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import fr.insalyon.creatis.vip.core.client.bean.User; + +import java.util.List; + +public class OidcToken extends AbstractAuthenticationToken { + private final User user; + private Jwt jwt; + + public OidcToken(User user, Jwt jwt, List authorities) { + super(authorities); + this.user = user; + this.jwt = jwt; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { return jwt; } + + @Override + public Object getPrincipal() { return user; } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + if (isAuthenticated) { + throw new IllegalArgumentException( + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + } + super.setAuthenticated(false); + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + jwt = null; + } +} diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/AuthenticationInfoTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/AuthenticationInfoTestUtils.java index b1d9b5521..213fb03ad 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/AuthenticationInfoTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/AuthenticationInfoTestUtils.java @@ -51,6 +51,7 @@ public class AuthenticationInfoTestUtils { authenticationInfoSuppliers = getAuthenticationInfoSuppliers(); } + @SuppressWarnings("unchecked") public static Map getAuthenticationInfoSuppliers() { return JsonCustomObjectMatcher.formatSuppliers( Arrays.asList("httpHeader", "httpHeaderValue"), diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ErrorCodeAndMessageTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ErrorCodeAndMessageTestUtils.java index 1ebba4a37..7f4fda5ce 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ErrorCodeAndMessageTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ErrorCodeAndMessageTestUtils.java @@ -53,6 +53,7 @@ public class ErrorCodeAndMessageTestUtils { errorCodeAndMessageSuppliers = getErrorCodeAndMessageSuppliers(); } + @SuppressWarnings("unchecked") public static Map getErrorCodeAndMessageSuppliers() { return JsonCustomObjectMatcher.formatSuppliers( Arrays.asList("errorCode", "errorMessage"), diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java index 36b092606..5aed0536b 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/ExecutionTestUtils.java @@ -158,6 +158,7 @@ public static Simulation copySimulationWithNewName(Simulation simu, String newNa return newSimulation; } + @SuppressWarnings("unchecked") public static Map getExecutionSuppliers() { return JsonCustomObjectMatcher.formatSuppliers( Arrays.asList( diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PathTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PathTestUtils.java index 445f4fcd3..c3ea3e523 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PathTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PathTestUtils.java @@ -178,6 +178,8 @@ public static PathProperties getPath(Data data, boolean isDirectory, return pathProperties; } + // [WARNING] VIP-portal/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PathTestUtils.java:[182,55] unchecked generic array creation for varargs parameter of type java.util.function.Function[] + @SuppressWarnings("unchecked") private static Map getPathSuppliers() { return JsonCustomObjectMatcher.formatSuppliers( Arrays.asList("path", "lastModificationDate", "isDirectory", "exists", diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PipelineTestUtils.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PipelineTestUtils.java index d4c8212fb..8331b740c 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PipelineTestUtils.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/data/PipelineTestUtils.java @@ -49,6 +49,7 @@ /** * Created by abonnet on 8/3/16. */ +@SuppressWarnings("unchecked") public class PipelineTestUtils { public static final Map pipelineSuppliers; diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/config/BaseWebSpringIT.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/config/BaseWebSpringIT.java index ddbdbb02f..e860404ed 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/config/BaseWebSpringIT.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/config/BaseWebSpringIT.java @@ -98,6 +98,8 @@ abstract public class BaseWebSpringIT extends BaseApplicationSpringIT { protected LFCPermissionBusiness lfcPermissionBusiness; @Autowired protected GRIDAClient gridaClient; + @Autowired + protected WorkflowExecutionBusiness workflowExecutionBusiness; @BeforeEach @Override diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java index 90c465aa4..f4b3a0cba 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/rest/itest/processing/ExecutionControllerIT.java @@ -46,6 +46,7 @@ import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; import fr.insalyon.creatis.vip.application.server.business.ResourceBusiness; import fr.insalyon.creatis.vip.application.server.business.simulation.ParameterSweep; +import fr.insalyon.creatis.vip.application.server.business.util.FileUtil; import fr.insalyon.creatis.vip.core.client.bean.GroupType; import fr.insalyon.creatis.vip.core.integrationtest.ServerMockConfig; import org.hamcrest.MatcherAssert; @@ -93,11 +94,12 @@ public void setUp() throws Exception { } @Test + @SuppressWarnings("unchecked") public void shouldListExecutions() throws Exception { - when(workflowDAO.get(eq(simulation1.getID()))).thenReturn(w1, null); - when(workflowDAO.get(eq(simulation2.getID()))).thenReturn(w2, null); + when(workflowDAO.get(eq(simulation1.getID()))).thenReturn(w1, (Workflow) null); + when(workflowDAO.get(eq(simulation2.getID()))).thenReturn(w2, (Workflow) null); when(workflowDAO.get(Collections.singletonList(baseUser1.getFullName()), null, null, null, null, null, null)) - .thenReturn(Arrays.asList(w1, w2), null); + .thenReturn(Arrays.asList(w1, w2), (List) null); // perform a getWorkflows() mockMvc.perform( @@ -115,9 +117,10 @@ public void shouldListExecutions() throws Exception { } @Test + @SuppressWarnings("unchecked") public void shouldCountExecutions() throws Exception { when(workflowDAO.get(Collections.singletonList(baseUser1.getFullName()), null, null, null, null, null, null)) - .thenReturn(Arrays.asList(w1, w2), null); + .thenReturn(Arrays.asList(w1, w2), (List) null); // perform a getWorkflows() mockMvc.perform( @@ -324,12 +327,13 @@ public void testPlayExecutionIsNotImplemented() throws Exception { } @Test + @SuppressWarnings("unchecked") public void shouldGetExecution2Results() throws Exception { String resultPath = "/root/user/user1/path/to/result.res"; - when(workflowDAO.get(eq(simulation2.getID()))).thenReturn(w2, null); + when(workflowDAO.get(eq(simulation2.getID()))).thenReturn(w2, (Workflow) null); Output output = new Output(new OutputID("workflowID", resultPath, "processor"), DataType.URI, "port"); - when(outputDAO.get(eq(simulation2.getID()))).thenReturn(Arrays.asList(output), null); + when(outputDAO.get(eq(simulation2.getID()))).thenReturn(Arrays.asList(output), (List) null); Mockito.when(server.getDataManagerUsersHome()).thenReturn("/root/user"); Mockito.when(gridaClient.exist(resultPath)).thenReturn(true); @@ -356,14 +360,15 @@ public void shouldKillExecution2() throws Exception put("/rest/executions/" + simulation2.getID() + "/kill").with(baseUser1())) .andDo(print()); - verify(webServiceEngine).kill(simulation2.getID()); + verify(webServiceEngine).kill(w2.getEngine(), simulation2.getID()); } @Test + @SuppressWarnings("unchecked") public void testInitGwendiaExecution() throws Exception { String appName = "test application", groupName = "testGroup", versionName = "4.2"; - String engineName = "testEngine", engineEndpoint = "endpoint", worflowId = "test-workflow-id"; + String engineEndpoint = "engineURL", workflowId = "test-workflow-id"; Date startDate = new Date(); configureGwendiaTestApp(appName, groupName, versionName); @@ -371,24 +376,22 @@ public void testInitGwendiaExecution() throws Exception createGroup("testResources", GroupType.RESOURCE); createUserInGroups(baseUser1.getEmail(), "", groupName, "testResources"); - ArgumentCaptor workflowFile = ArgumentCaptor.forClass(File.class); - ArgumentCaptor> inputsCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor inputsCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor workflowContentCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor workflowCaptor = ArgumentCaptor.forClass(Workflow.class); Mockito.when(server.getVoName()).thenReturn("test-vo-name"); Mockito.when(server.getServerProxy("test-vo-name")).thenReturn("/path/to/proxy"); - Mockito.when(getWebServiceEngine().launch("/path/to/proxy", null)).thenReturn("full-test-workflow-id", (String) null); - Mockito.when(getWebServiceEngine().getSimulationId("full-test-workflow-id")).thenReturn(worflowId, (String) null); - Mockito.when(getWebServiceEngine().getStatus(worflowId)).thenReturn(SimulationStatus.Running, (SimulationStatus) null); - Mockito.when(getWebServiceEngine().getAddressWS()).thenReturn(engineEndpoint, (String) null); + Mockito.when(getWebServiceEngine().launch(eq(engineEndpoint), workflowContentCaptor.capture(), inputsCaptor.capture(), eq(""), eq("/path/to/proxy"))).thenReturn(workflowId, (String) null); + Mockito.when(getWebServiceEngine().getStatus(engineEndpoint, workflowId)).thenReturn(SimulationStatus.Running, (SimulationStatus) null); - Workflow w = new Workflow(worflowId, baseUser1.getFullName(), WorkflowStatus.Running, startDate, null, "Exec test 1", appName, versionName, "", engineName, null); - when(workflowDAO.get(worflowId)).thenReturn(w, (Workflow) null); + Workflow w = new Workflow(workflowId, baseUser1.getFullName(), WorkflowStatus.Running, startDate, null, "Exec test 1", appName, versionName, "", engineEndpoint, null); + when(workflowDAO.get(workflowId)).thenReturn(w, (Workflow) null); - Execution expectedExecution = new Execution(worflowId, "Exec test 1", appName + "/" + versionName, 0, ExecutionStatus.RUNNING, null, null, startDate.getTime(), null, null); + Execution expectedExecution = new Execution(workflowId, "Exec test 1", appName + "/" + versionName, 0, ExecutionStatus.RUNNING, null, null, startDate.getTime(), null, null); expectedExecution.clearReturnedFiles(); - setUpResourceAndEngine(appName, versionName); + setUpResourceAndEngine(appName, versionName, engineEndpoint); mockMvc.perform( post("/rest/executions").contentType("application/json") @@ -402,32 +405,25 @@ public void testInitGwendiaExecution() throws Exception )); // verify workflow path - Mockito.verify(getWebServiceEngine()).setWorkflow(workflowFile.capture()); - Assertions.assertEquals(getGwendiaTestFile().getAbsolutePath(), workflowFile.getValue().getAbsolutePath()); + Assertions.assertEquals(FileUtil.read(getGwendiaTestFile()), workflowContentCaptor.getValue()); // verify inputs - Mockito.verify(getWebServiceEngine()).setInput(inputsCaptor.capture()); - List inputs = inputsCaptor.getValue(); - Assertions.assertEquals(5, inputs.size()); - MatcherAssert.assertThat(inputs, Matchers.containsInAnyOrder( - both(hasProperty("parameterName", is("testFileInput"))). - and(hasProperty("values", Matchers.contains(ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder() + "/path/to/input.in"))), - both(hasProperty("parameterName", is("testTextInput"))). - and(hasProperty("values", Matchers.contains("best test text value"))), - both(hasProperty("parameterName", is("results-directory"))). - and(hasProperty("values", Matchers.contains(ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder()))), - both(hasProperty("parameterName", is("testOptionalTextInput"))). - and(hasProperty("values", Matchers.contains("No_value_provided"))), - both(hasProperty("parameterName", is("testFlagInput"))). - and(hasProperty("values", Matchers.contains("false"))) - )); + String inputs = inputsCaptor.getValue(); + List expectedParams = new ArrayList<>(); + expectedParams.add(new ParameterSweep("testFileInput", ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder() + "/path/to/input.in")); + expectedParams.add(new ParameterSweep("testTextInput", "best test text value")); + expectedParams.add(new ParameterSweep("testFlagInput", "false")); + expectedParams.add(new ParameterSweep("results-directory", ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder())); + expectedParams.add(new ParameterSweep("testOptionalTextInput", "No_value_provided")); + String expectedInputs = workflowExecutionBusiness.getParametersAsXMLInput(expectedParams); + Assertions.assertEquals(expectedInputs, inputs); // verify created workflow Mockito.verify(workflowDAO).add(workflowCaptor.capture()); Workflow workflow = workflowCaptor.getValue(); Assertions.assertEquals(appName, workflow.getApplication()); Assertions.assertEquals(versionName, workflow.getApplicationVersion()); - Assertions.assertEquals(worflowId, workflow.getId()); + Assertions.assertEquals(workflowId, workflow.getId()); Assertions.assertEquals(WorkflowStatus.Running, workflow.getStatus()); Assertions.assertEquals("Exec test 1", workflow.getDescription()); Assertions.assertEquals(engineEndpoint, workflow.getEngine()); @@ -440,10 +436,11 @@ public void testInitGwendiaExecution() throws Exception // the difference (at the moment) is that with moteurLite the optional and absent parameters are not included @Test + @SuppressWarnings("unchecked") public void testInitBoutiquesExecution() throws Exception { String appName = "test application", groupName = "testGroup", versionName = "4.2"; - String engineName = "testEngine", engineEndpoint = "endpoint", worflowId = "test-workflow-id"; + String engineEndpoint = "endpoint", workflowId = "test-workflow-id"; Date startDate = new Date(); configureBoutiquesTestApp(appName, groupName, versionName); @@ -451,25 +448,23 @@ public void testInitBoutiquesExecution() throws Exception createGroup("testResources", GroupType.RESOURCE); createUserInGroups(baseUser1.getEmail(), "", groupName, "testResources"); - ArgumentCaptor workflowFile = ArgumentCaptor.forClass(File.class); - ArgumentCaptor> inputsCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor inputsCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor workflowContentCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor workflowCaptor = ArgumentCaptor.forClass(Workflow.class); Mockito.when(server.useMoteurlite()).thenReturn(true); Mockito.when(server.getVoName()).thenReturn("test-vo-name"); Mockito.when(server.getServerProxy("test-vo-name")).thenReturn("/path/to/proxy"); - Mockito.when(getWebServiceEngine().launch("/path/to/proxy", null)).thenReturn("full-test-workflow-id", (String) null); - Mockito.when(getWebServiceEngine().getSimulationId("full-test-workflow-id")).thenReturn(worflowId, (String) null); - Mockito.when(getWebServiceEngine().getStatus(worflowId)).thenReturn(SimulationStatus.Running, (SimulationStatus) null); - Mockito.when(getWebServiceEngine().getAddressWS()).thenReturn(engineEndpoint, (String) null); + Mockito.when(getWebServiceEngine().launch(eq(engineEndpoint), workflowContentCaptor.capture(), inputsCaptor.capture(), eq(""), eq("/path/to/proxy"))).thenReturn(workflowId, (String) null); + Mockito.when(getWebServiceEngine().getStatus(engineEndpoint, workflowId)).thenReturn(SimulationStatus.Running, (SimulationStatus) null); - Workflow w = new Workflow(worflowId, baseUser1.getFullName(), WorkflowStatus.Running, startDate, null, "Exec test 1", appName, versionName, "", engineName, null); - when(workflowDAO.get(worflowId)).thenReturn(w, (Workflow) null); + Workflow w = new Workflow(workflowId, baseUser1.getFullName(), WorkflowStatus.Running, startDate, null, "Exec test 1", appName, versionName, "", engineEndpoint, null); + when(workflowDAO.get(workflowId)).thenReturn(w, (Workflow) null); - Execution expectedExecution = new Execution(worflowId, "Exec test 1", appName + "/" + versionName, 0, ExecutionStatus.RUNNING, null, null, startDate.getTime(), null, null); + Execution expectedExecution = new Execution(workflowId, "Exec test 1", appName + "/" + versionName, 0, ExecutionStatus.RUNNING, null, null, startDate.getTime(), null, null); expectedExecution.clearReturnedFiles(); - setUpResourceAndEngine(appName, versionName); + setUpResourceAndEngine(appName, versionName, engineEndpoint); mockMvc.perform( post("/rest/executions").contentType("application/json") @@ -483,30 +478,24 @@ public void testInitBoutiquesExecution() throws Exception )); // verify workflow path - Mockito.verify(getWebServiceEngine()).setWorkflow(workflowFile.capture()); - Assertions.assertEquals(getBoutiquesTestFile().getAbsolutePath(), workflowFile.getValue().getAbsolutePath()); - - // verify inputs - Mockito.verify(getWebServiceEngine()).setInput(inputsCaptor.capture()); - List inputs = inputsCaptor.getValue(); - Assertions.assertEquals(4, inputs.size()); - MatcherAssert.assertThat(inputs, Matchers.containsInAnyOrder( - both(hasProperty("parameterName", is("testFileInput"))). - and(hasProperty("values", Matchers.contains(ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder() + "/path/to/input.in"))), - both(hasProperty("parameterName", is("testTextInput"))). - and(hasProperty("values", Matchers.contains("best test text value"))), - both(hasProperty("parameterName", is("results-directory"))). - and(hasProperty("values", Matchers.contains(ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder()))), - both(hasProperty("parameterName", is("testFlagInput"))). - and(hasProperty("values", Matchers.contains("false"))) - )); + Assertions.assertEquals(FileUtil.read(getBoutiquesTestFile()), workflowContentCaptor.getValue()); + + // verify inputs / same as gwendia without optional one + String inputs = inputsCaptor.getValue(); + List expectedParams = new ArrayList<>(); + expectedParams.add(new ParameterSweep("testFileInput", ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder() + "/path/to/input.in")); + expectedParams.add(new ParameterSweep("testTextInput", "best test text value")); + expectedParams.add(new ParameterSweep("testFlagInput", "false")); + expectedParams.add(new ParameterSweep("results-directory", ServerMockConfig.TEST_USERS_ROOT + "/" + baseUser1.getFolder())); + String expectedInputs = workflowExecutionBusiness.getParametersAsXMLInput(expectedParams); + Assertions.assertEquals(expectedInputs, inputs); // verify created workflow Mockito.verify(workflowDAO).add(workflowCaptor.capture()); Workflow workflow = workflowCaptor.getValue(); Assertions.assertEquals(appName, workflow.getApplication()); Assertions.assertEquals(versionName, workflow.getApplicationVersion()); - Assertions.assertEquals(worflowId, workflow.getId()); + Assertions.assertEquals(workflowId, workflow.getId()); Assertions.assertEquals(WorkflowStatus.Running, workflow.getStatus()); Assertions.assertEquals("Exec test 1", workflow.getDescription()); Assertions.assertEquals(engineEndpoint, workflow.getEngine()); @@ -517,8 +506,8 @@ public void testInitBoutiquesExecution() throws Exception } - public void setUpResourceAndEngine(String appName, String version) throws Exception { - Engine engine = new Engine("testEngine", "bla", "enabled"); + public void setUpResourceAndEngine(String appName, String version, String endpoint) throws Exception { + Engine engine = new Engine("testEngine", endpoint, "enabled"); Resource resource = new Resource( "testResource", true, diff --git a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/tools/spring/JsonCustomObjectMatcher.java b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/tools/spring/JsonCustomObjectMatcher.java index 1c64a9602..91994d1eb 100644 --- a/vip-api/src/test/java/fr/insalyon/creatis/vip/api/tools/spring/JsonCustomObjectMatcher.java +++ b/vip-api/src/test/java/fr/insalyon/creatis/vip/api/tools/spring/JsonCustomObjectMatcher.java @@ -65,6 +65,7 @@ public JsonCustomObjectMatcher(T expectedBean, this(expectedBean, suppliers, new HashMap<>()); } + @SuppressWarnings("unchecked") public JsonCustomObjectMatcher(T expectedBean, Map suppliers, Map> suppliersRegistry) { @@ -88,6 +89,7 @@ public JsonCustomObjectMatcher(T expectedBean, nonNullPropertiesCountMatcher = equalTo(propertyMatchers.size()); } + @SuppressWarnings("unchecked") private static Matcher getGenericMatcher( Object expectedValue, Map> suppliersRegistry) { @@ -133,6 +135,7 @@ private static Matcher getGenericMatcher( } } + @SuppressWarnings("unchecked") private static Matcher> getCustomObjectMatcherFromRegistry( Object o, Map> suppliersRegistry) { @@ -157,6 +160,7 @@ public static Matcher> jsonCorrespondsTo( return new JsonCustomObjectMatcher(expectedBean, suppliers, suppliersRegistry); } + @SuppressWarnings("unchecked") public static Map formatSuppliers( List mapKeys, Function... suppliers) { if (mapKeys.size() != suppliers.length) { @@ -207,6 +211,7 @@ private static class JsonMapMatcher extends TypeSafeDiagnosingMatcher sizeMatcher; private final List> mapEntriesMatchers = new ArrayList<>(); + @SuppressWarnings("unchecked") private JsonMapMatcher( Map expectedMap, Map> suppliersRegistry) { diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/ChartsTab.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/ChartsTab.java index 5e020c24f..37c873ee4 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/ChartsTab.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/ChartsTab.java @@ -123,7 +123,7 @@ private void configureForm() { chartsItem.addChangedHandler(new ChangedHandler() { @Override public void onChanged(ChangedEvent event) { - int value = new Integer(chartsItem.getValueAsString()); + int value = Integer.parseInt(chartsItem.getValueAsString()); if (value == 1 || value == 2 || value == 6) { binItem.setDisabled(true); } else { diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/StatsTab.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/StatsTab.java index 4d2c8f9af..18bdc469a 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/StatsTab.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/StatsTab.java @@ -123,13 +123,6 @@ private void configureForm() { chartsItem.setWidth(250); chartsItem.setValueMap(chartsMap); chartsItem.setEmptyDisplayValue("Select a chart..."); - chartsItem.addChangedHandler(new ChangedHandler() { - - @Override - public void onChanged(ChangedEvent event) { - int value = new Integer(chartsItem.getValueAsString()); - } - }); generateButton = WidgetUtil.getIButton("Get Stats", null, new ClickHandler() { diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/chart/GeneralBarChart.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/chart/GeneralBarChart.java index c443dffba..0e1a90da5 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/chart/GeneralBarChart.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/client/view/monitor/chart/GeneralBarChart.java @@ -135,7 +135,7 @@ public void build(String caption, String color, int binSize) { max = localMax; } if (res.length > 4) { - sum += new Integer(res[4]); + sum += Integer.parseInt(res[4]); } } } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/SpringApplicationConfig.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/SpringApplicationConfig.java index 9e749f773..fafc225a3 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/SpringApplicationConfig.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/SpringApplicationConfig.java @@ -1,6 +1,10 @@ package fr.insalyon.creatis.vip.application.server; import fr.insalyon.creatis.moteur.plugins.workflowsdb.dao.*; +import fr.insalyon.creatis.vip.application.server.business.simulation.RestServiceEngine; +import fr.insalyon.creatis.vip.application.server.business.simulation.SoapServiceEngine; +import fr.insalyon.creatis.vip.application.server.business.simulation.WorkflowEngineInstantiator; +import fr.insalyon.creatis.vip.core.server.business.Server; import org.hibernate.SessionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,4 +58,12 @@ public StatsDAO getStatsDAO() throws WorkflowsDBDAOException { return workflowsDBDAOFactory().getStatsDAO(); } + @Bean + public WorkflowEngineInstantiator getWorkflowEngineInstantiator(Server server) { + if (server.useRestMoteurServer()) { + return new RestServiceEngine(server); + } + return new SoapServiceEngine(); + } + } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/ReproVipBusiness.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/ReproVipBusiness.java index 393426701..8d91c0c36 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/ReproVipBusiness.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/ReproVipBusiness.java @@ -310,6 +310,7 @@ private Map mapOutputDataPathsByFilenames(List outputDat return outputDataMap; } + @SuppressWarnings("unchecked") private Map getOutputFilenamesFromProvenanceFile(Path provenanceFilePath) throws BusinessException { try { Map map = new ObjectMapper().readValue(provenanceFilePath.toFile(), Map.class); diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java index b5d816385..c0ac2bc3e 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowBusiness.java @@ -65,6 +65,7 @@ import org.springframework.transaction.annotation.Transactional; import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; import java.util.*; @@ -98,6 +99,7 @@ public class WorkflowBusiness { private final GRIDAPoolClient gridaPoolClient; private final GRIDAClient gridaClient; private final ExternalPlatformBusiness externalPlatformBusiness; + private final WorkflowExecutionBusiness workflowExecutionBusiness; @Autowired public WorkflowBusiness( @@ -108,7 +110,8 @@ public WorkflowBusiness( DataManagerBusiness dataManagerBusiness, EmailBusiness emailBusiness, LfcPathsBusiness lfcPathsBusiness, GRIDAPoolClient gridaPoolClient, GRIDAClient gridaClient, ExternalPlatformBusiness externalPlatformBusiness, - ResourceBusiness resourceBusiness, AppVersionBusiness appVersionBusiness) { + ResourceBusiness resourceBusiness, AppVersionBusiness appVersionBusiness, + WorkflowExecutionBusiness workflowExecutionBusiness) { this.server = server; this.simulationStatsDAO = simulationStatsDAO; this.workflowDAO = workflowDAO; @@ -126,22 +129,17 @@ public WorkflowBusiness( this.externalPlatformBusiness = externalPlatformBusiness; this.resourceBusiness = resourceBusiness; this.appVersionBusiness = appVersionBusiness; + this.workflowExecutionBusiness = workflowExecutionBusiness; } /* - The 4 next dependencies cannot be injected by spring in a classic way as + The 2 next dependencies cannot be injected by spring in a classic way as they cannot be singleton (spring default scope). A new instance must be created at each use and so we use the prototype scope with lookup methods to inject them. They need to be injected by spring (and not created with "new") so spring can handle their own dependencies. */ - @Lookup - protected WorkflowExecutionBusiness getWorkflowExecutionBusiness(String endpoint) { - // will be generated by spring to return a new instance each time - return null; - } - @Lookup protected GwendiaParser getGwendiaParser() { // will be generated by spring to return a new instance each time @@ -175,10 +173,9 @@ public synchronized String launch(User user, List groups, Map simulations) throws Busine if (simulation.getStatus() == SimulationStatus.Running || simulation.getStatus() == SimulationStatus.Unknown) { - WorkflowExecutionBusiness executionBusiness = getWorkflowExecutionBusiness(simulation.getEngine()); - SimulationStatus simulationStatus = executionBusiness.getStatus(simulation.getID()); + SimulationStatus simulationStatus = workflowExecutionBusiness.getStatus(simulation.getEngine(), simulation.getID()); if (simulationStatus != SimulationStatus.Running && simulationStatus != SimulationStatus.Unknown) { diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowExecutionBusiness.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowExecutionBusiness.java index b92ecee41..886923060 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowExecutionBusiness.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/WorkflowExecutionBusiness.java @@ -36,18 +36,19 @@ import fr.insalyon.creatis.vip.application.client.bean.AppVersion; import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; import fr.insalyon.creatis.vip.application.server.business.simulation.ParameterSweep; -import fr.insalyon.creatis.vip.application.server.business.simulation.WebServiceEngine; +import fr.insalyon.creatis.vip.application.server.business.simulation.WorkflowEngineInstantiator; +import fr.insalyon.creatis.vip.application.server.business.util.FileUtil; import fr.insalyon.creatis.vip.core.client.bean.User; import fr.insalyon.creatis.vip.core.server.business.BusinessException; import fr.insalyon.creatis.vip.core.server.business.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; -import jakarta.annotation.PostConstruct; +import javax.xml.rpc.ServiceException; import java.io.File; +import java.rmi.RemoteException; import java.util.Date; import java.util.List; @@ -55,103 +56,97 @@ * Wrapper on WebServiceEngine to configure it and to create a Workflow object * after a launch. * - * WorkflowExecutionBusiness and WebServiceEngine both have spring prototype - * scope, so each WorkflowExecutionBusiness use creates a new intance with a - * dedicated WebServiceEngine instance that it will wrap. - * This is needed as each time an engine is used, the endpoint can be different. - * * @author Rafael Ferreira da Silva */ @Service -@Scope("prototype") public class WorkflowExecutionBusiness { private final Logger logger = LoggerFactory.getLogger(getClass()); private Server server; - private WebServiceEngine engine; - private String engineEndpoint; + private WorkflowEngineInstantiator engine; - @Autowired - public final void setServer(Server server) { - this.server = server; - } - /* - WebServiceEngine is also prototype scoped, this creates a new instance - every time. - */ @Autowired - public final void setEngine(WebServiceEngine engine) { + public WorkflowExecutionBusiness(Server server, WorkflowEngineInstantiator engine) { + this.server = server; this.engine = engine; } - public WorkflowExecutionBusiness(String engineEndpoint) throws BusinessException { - - //HACK for testing while still having simulations launched with VIP 1.16.1; to be removed before getting in production or replaced with a proper constant - if(engineEndpoint == null){ - logger.info("WorkflowExecutionBusiness, endpoint is null, setting it to http://data-manager.grid.creatis.insa-lyon.fr/cgi-bin/m2Server-gasw3.1/moteur_server"); - engineEndpoint="http://data-manager.grid.creatis.insa-lyon.fr/cgi-bin/m2Server-gasw3.1/moteur_server"; - } - this.engineEndpoint = engineEndpoint; - } - - @PostConstruct - public final void configureWebServiceEngine() { - engine.setAddressWS(engineEndpoint); - String settings = "GRID=DIRAC\n" - + "SE=ccsrm02.in2p3.fr\n" - + "TIMEOUT=100000\n" - + "RETRYCOUNT=3\n" - + "MULTIJOB=1"; - engine.setSettings(settings); - - } + public Workflow launch(String engineEndpoint, AppVersion appVersion, User user, String simulationName, + String workflowPath, List parameters) throws BusinessException { - public Workflow launch(AppVersion appVersion, User user, String simulationName, String workflowPath, List parameters) throws BusinessException { try { - engine.setWorkflow(new File(workflowPath)); - engine.setInput(parameters); - String launchID = engine.launch(server.getServerProxy(server.getVoName()), null); - String workflowID = engine.getSimulationId(launchID); - + String workflowContent = FileUtil.read(new File(workflowPath)); + String inputs = (parameters != null) ? getParametersAsXMLInput(parameters) : null; + String proxyFileName = server.getServerProxy(server.getVoName()); + String workflowID = engine.launch(engineEndpoint, workflowContent, inputs, "", proxyFileName); return new Workflow(workflowID, user.getFullName(), WorkflowStatus.Running, new Date(), null, simulationName, appVersion.getApplicationName(), appVersion.getVersion(), "", - engine.getAddressWS(), null); + engineEndpoint, null); - } catch (javax.xml.rpc.ServiceException | java.rmi.RemoteException ex) { + } catch (ServiceException | RemoteException ex) { logger.error("Error launching simulation {} ({}/{})", simulationName, appVersion.getApplicationName(), appVersion.getVersion(), ex); throw new BusinessException(ex); } } - public SimulationStatus getStatus(String simulationID) throws BusinessException { - + public SimulationStatus getStatus(String engineEndpoint, String simulationID) throws BusinessException { SimulationStatus status = SimulationStatus.Unknown; try { - status = engine.getStatus(simulationID); - } catch (javax.xml.rpc.ServiceException ex) { - logger.error("Error getting status for {}", simulationID, ex); - throw new BusinessException(ex); - } catch (java.rmi.RemoteException ex) { - logger.error("Error getting status for {}. Ignoring", simulationID, ex); + status = engine.getStatus(engineEndpoint, simulationID); + } catch (RemoteException | ServiceException e) { + logger.error("Error getting status of simulation {} on engine {}", simulationID, engineEndpoint, e); + throw new BusinessException(e); } return status; } - public void kill(String simulationID) throws BusinessException { - + public void kill(String engineEndpoint, String simulationID) throws BusinessException { try { - engine.kill(simulationID); + engine.kill(engineEndpoint, simulationID); + } catch (RemoteException | ServiceException e) { + logger.error("Error killing simulation {} on engine {}", simulationID, engineEndpoint, e); + throw new BusinessException(e); + } + } - } catch (javax.xml.rpc.ServiceException ex) { - logger.error("Error killing simulation {}", simulationID, ex); - throw new BusinessException(ex); - } catch (java.rmi.RemoteException ex) { - logger.error("Error killing simulation {}. Ignoring", simulationID, ex); + public String getParametersAsXMLInput(List parameters) { + + //generate the xml input file according to the user input on the GUI + StringBuilder xml = new StringBuilder("\n"); + xml.append("\n"); + + for (ParameterSweep parameter : parameters) { + + xml.append("\t\n") + .append("\n"); + + int counter = 0; + for (String value : parameter.getValues()) { + + + xml.append("\t\t") + .append("") + .append(value) + .append("\n"); + + counter++; + } + + xml.append("\n"); + xml.append("\t\n"); } + + xml.append("\n"); + + return xml.toString(); } } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/ParameterSweep.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/ParameterSweep.java index 7b0b65af2..1b86193c5 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/ParameterSweep.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/ParameterSweep.java @@ -48,6 +48,11 @@ public ParameterSweep(String parameterName) { this.values = new ArrayList(); } + public ParameterSweep(String parameterName, String value) { + this(parameterName); + addValue(value); + } + public String getParameterName() { return parameterName; } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/RestServiceEngine.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/RestServiceEngine.java new file mode 100644 index 000000000..ad8ce86aa --- /dev/null +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/RestServiceEngine.java @@ -0,0 +1,203 @@ +/* + * Copyright and authors: see LICENSE.txt in base repository. + * + * This software is a web portal for pipeline execution on distributed systems. + * + * This software is governed by the CeCILL-B license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-B + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-B license and that you accept its terms. + */ +package fr.insalyon.creatis.vip.application.server.business.simulation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; +import fr.insalyon.creatis.vip.application.server.business.util.ProxyUtil; +import fr.insalyon.creatis.vip.core.server.business.BusinessException; +import fr.insalyon.creatis.vip.core.server.business.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; + +import javax.xml.rpc.ServiceException; +import java.nio.charset.StandardCharsets; +import java.rmi.RemoteException; +import java.util.Base64; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +/** + * @author Rafael Ferreira da Silva, Ibrahim kallel + */ +public class RestServiceEngine extends WorkflowEngineInstantiator { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Server server; + + @Autowired + public RestServiceEngine(Server server) { + this.server = server; + } + + + private static class RestWorkflow { + @JsonProperty("workflow") + String workflow; + + @JsonProperty("inputs") + String inputs; + + @JsonProperty("proxy") + String proxy; + + @JsonProperty("settings") + String settings; + + public RestWorkflow(String workflow, String inputs, String proxy, String settings) { + this.workflow = workflow; + this.inputs = inputs; + this.proxy = proxy; + this.settings = settings; + } + } + + /** + * Call the WS that is going to run the workflow and return the HTTP link + * that can be used to monitor the workflow status. + * + * @return the HTTP link that shows the workflow current status + */ + @Override + public String launch(String addressWS, String workflow, String inputs, String settings, String proxyFileName) + throws RemoteException, ServiceException, BusinessException { + + loadTrustStore(server); + + String strProxy = null; + if (server.getMyProxyEnabled()) { + strProxy = ProxyUtil.readAsString(proxyFileName); + } + + String base64Workflow = Base64.getEncoder().encodeToString(workflow.getBytes(StandardCharsets.UTF_8)); + String base64Input = Base64.getEncoder().encodeToString(inputs.getBytes(StandardCharsets.UTF_8)); + String base64Proxy = Base64.getEncoder().encodeToString(strProxy != null ? strProxy.getBytes(StandardCharsets.UTF_8) : null); + String base64Settings = Base64.getEncoder().encodeToString(settings.getBytes(StandardCharsets.UTF_8)); + + RestWorkflow restWorkflow = new RestWorkflow(base64Workflow, base64Input, base64Proxy, base64Settings); + + try { + ObjectMapper mapper = new ObjectMapper(); + String jsonBody = mapper.writeValueAsString(restWorkflow); + + RestClient restClient = buildRestClient(addressWS); + return restClient.post() + .uri("/submit") + .contentType(APPLICATION_JSON) + .body(jsonBody) + .retrieve() + .body(String.class); + } catch (HttpServerErrorException | HttpClientErrorException e) { + logger.error("Server error while fetching workflow status: {}", e.getResponseBodyAsString(), e); + throw new BusinessException("Internal server error while fetching workflow status", e); + } catch (RestClientException e) { + logger.error("REST client error while fetching workflow status", e); + throw new BusinessException("REST client error while fetching workflow status", e); + } catch (JsonProcessingException e) { + logger.error("Error serializing RestWorkflow to JSON", e); + throw new BusinessException("Error serializing RestWorkflow to JSON", e); + } + } + + + @Override + public void kill(String addressWS, String workflowID) throws BusinessException { + + loadTrustStore(server); + + try { + RestClient restClient = buildRestClient(addressWS); + + restClient + .put() + .uri("/kill") + .body(Map.of("workflowID", workflowID)) + .contentType(APPLICATION_JSON) + .retrieve() + .body(String.class); + + + logger.info("Successfully sent kill request for workflow ID: {}", workflowID); + } catch (HttpServerErrorException | HttpClientErrorException e) { + logger.error("Server error while fetching workflow status: {}", e.getResponseBodyAsString(), e); + throw new BusinessException("Internal server error while fetching workflow status", e); + } catch (RestClientException e) { + logger.error("REST client error while fetching workflow status", e); + throw new BusinessException("REST client error while fetching workflow status", e); + } + } + + public SimulationStatus getStatus(String addressWS, String workflowID) throws BusinessException { + loadTrustStore(server); + + try { + RestClient restClient = buildRestClient(addressWS); + + String workflowStatus = restClient + .get() + .uri("/status/{workflowID}", workflowID) + .retrieve() + .body(String.class); + + MoteurStatus moteurStatus = MoteurStatus.valueOf(workflowStatus != null ? workflowStatus.toUpperCase() : null); + return switch (moteurStatus) { + case RUNNING -> SimulationStatus.Running; + case COMPLETE -> SimulationStatus.Completed; + case TERMINATED -> SimulationStatus.Killed; + default -> SimulationStatus.Unknown; + }; + } catch (HttpServerErrorException | HttpClientErrorException e) { + logger.error("Server error while fetching workflow status: {}", e.getResponseBodyAsString(), e); + throw new BusinessException("Internal server error while fetching workflow status", e); + } catch (RestClientException e) { + logger.error("REST client error while fetching workflow status", e); + throw new BusinessException("REST client error while fetching workflow status", e); + } + } + + private RestClient buildRestClient(String addressWS) { + return RestClient.builder() + .baseUrl(addressWS) + .defaultHeaders(headers -> headers.setBasicAuth("user", server.getMoteurServerPassword())) + .build(); + } +} \ No newline at end of file diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WebServiceEngine.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/SoapServiceEngine.java similarity index 65% rename from vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WebServiceEngine.java rename to vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/SoapServiceEngine.java index 399dd4321..5d1c58e45 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WebServiceEngine.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/SoapServiceEngine.java @@ -4,16 +4,16 @@ * This software is a web portal for pipeline execution on distributed systems. * * This software is governed by the CeCILL-B license under French law and - * abiding by the rules of distribution of free software. You can use, + * abiding by the rules of distribution of free software. You can use, * modify and/ or redistribute the software under the terms of the CeCILL-B * license as circulated by CEA, CNRS and INRIA at the following URL - * "http://www.cecill.info". + * "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, * modify and redistribute granted by the license, users are provided only * with a limited warranty and the software's author, the holder of the * economic rights, and the successive licensors have only limited - * liability. + * liability. * * In this respect, the user's attention is drawn to the risks associated * with loading, using, modifying and/or developing or reproducing the @@ -22,9 +22,9 @@ * therefore means that it is reserved for developers and experienced * professionals having in-depth computer knowledge. Users are therefore * encouraged to load and test the software's suitability as regards their - * requirements in conditions enabling the security of their systems and/or - * data to be ensured and, more generally, to use and operate it in the - * same conditions as regards security. + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. * * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-B license and that you accept its terms. @@ -33,61 +33,21 @@ import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; import fr.insalyon.creatis.vip.application.server.business.util.ProxyUtil; +import fr.insalyon.creatis.vip.core.server.business.BusinessException; import fr.insalyon.creatis.vip.core.server.business.Server; -import java.io.File; -import java.io.InputStream; -import java.util.List; import localhost.moteur_service_wsdl.Moteur_ServiceLocator; import org.apache.axis.EngineConfiguration; import org.apache.axis.configuration.FileProvider; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Service; +import java.io.InputStream; /** - * Communicates with a moteur server through a web service. - * - * Each call is relative to a unique endpoint and must create a new instance, - * so this needs the spring prototype scope. - * * @author Rafael Ferreira da Silva, Ibrahim kallel */ -@Service -@Scope("prototype") -public class WebServiceEngine extends WorkflowEngineInstantiator { - - // URI address of the Moteur Web service - private String addressWS; - // settings to send to the web service. - private String settings; - - public String getAddressWS() { - return addressWS; - } - - public void setAddressWS(String addressWS) { - this.addressWS = addressWS; - } - - public String getSettings() { - return settings; - } - - public void setSettings(String settings) { - this.settings = settings; - } - - public WebServiceEngine() { - this(null, null); - } +public class SoapServiceEngine extends WorkflowEngineInstantiator { private Server server; - private WebServiceEngine(File workflow, List parameters) { - super(workflow, parameters); - } - @Autowired public final void setServer(Server server) { this.server = server; @@ -100,15 +60,11 @@ public final void setServer(Server server) { * @return the HTTP link that shows the workflow current status */ @Override - public String launch(String proxyFileName, String userDN) - throws - java.rmi.RemoteException, - javax.xml.rpc.ServiceException { + public String launch(String addressWS, String workflowContent, String inputs, String settings, String proxyFileName) + throws java.rmi.RemoteException, javax.xml.rpc.ServiceException, BusinessException { String strProxy = null; - System.setProperty("javax.net.ssl.trustStore", server.getTruststoreFile()); - System.setProperty("javax.net.ssl.trustStorePassword", server.getTruststorePass()); - System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + loadTrustStore(server); // String settings = "This is going to contain settings..."; // Get Proxy from current User's context @@ -122,18 +78,20 @@ public String launch(String proxyFileName, String userDN) EngineConfiguration engineConfig = new FileProvider(is); Moteur_ServiceLocator wfS = new Moteur_ServiceLocator(addressWS, engineConfig); - return wfS.getmoteur_service().workflowSubmit(workflow, input, strProxy, settings); + return getSimulationId(wfS.getmoteur_service().workflowSubmit(workflowContent, inputs, strProxy, settings)); + } + + public String getSimulationId(String launchID) { + return launchID.substring(launchID.lastIndexOf("/") + 1, launchID.lastIndexOf(".")); } @Override - public void kill(String workflowID) + public void kill(String addressWS, String workflowID) throws java.rmi.RemoteException, javax.xml.rpc.ServiceException { - System.setProperty("javax.net.ssl.trustStore", server.getTruststoreFile()); - System.setProperty("javax.net.ssl.trustStorePassword", server.getTruststorePass()); - System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + loadTrustStore(server); String resourcename = "moteur-client-config.wsdd"; InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourcename); @@ -144,14 +102,12 @@ public void kill(String workflowID) } @Override - public SimulationStatus getStatus(String workflowID) + public SimulationStatus getStatus(String addressWS, String workflowID) throws java.rmi.RemoteException, javax.xml.rpc.ServiceException { - System.setProperty("javax.net.ssl.trustStore", server.getTruststoreFile()); - System.setProperty("javax.net.ssl.trustStorePassword", server.getTruststorePass()); - System.setProperty("javax.net.ssl.trustStoreType", "JKS"); + loadTrustStore(server); String resourcename = "moteur-client-config.wsdd"; InputStream is = this.getClass().getClassLoader().getResourceAsStream(resourcename); @@ -161,7 +117,6 @@ public SimulationStatus getStatus(String workflowID) String workflowStatus = wfS.getmoteur_service().getWorkflowStatus(workflowID); MoteurStatus moteurStatus = MoteurStatus.valueOf(workflowStatus); switch (moteurStatus) { - case RUNNING: return SimulationStatus.Running; case COMPLETE: @@ -175,8 +130,5 @@ public SimulationStatus getStatus(String workflowID) } } - static enum MoteurStatus { - RUNNING, COMPLETE, TERMINATED, UNKNOWN - }; -} +} \ No newline at end of file diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WorkflowEngineInstantiator.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WorkflowEngineInstantiator.java index cb7409ceb..4650c1d79 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WorkflowEngineInstantiator.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/WorkflowEngineInstantiator.java @@ -35,9 +35,14 @@ import fr.insalyon.creatis.vip.application.server.business.util.FileUtil; import java.io.File; +import java.nio.file.Path; import java.util.List; + +import fr.insalyon.creatis.vip.core.server.business.BusinessException; +import fr.insalyon.creatis.vip.core.server.business.Server; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; /** * @@ -46,94 +51,29 @@ */ public abstract class WorkflowEngineInstantiator { - // content of the xml file that describe the workflow (read on a file) */ - protected String workflow; - // content of the input for the workflow (generated depending of the user)*/ - protected String input; - - public String getSimulationId(String launchID) { - - return launchID.substring(launchID.lastIndexOf("/") + 1, launchID.lastIndexOf(".")); - } - - /** - * - * @param workflow workflow file - * @param parameters list of parameters - */ - public WorkflowEngineInstantiator(File workflow, List parameters) { - - this.workflow = (workflow != null) ? FileUtil.read(workflow) : null; - this.input = (parameters != null) ? WorkflowEngineInstantiator.setParametersAsXMLInput(parameters) : null; + static enum MoteurStatus { + RUNNING, COMPLETE, TERMINATED, UNKNOWN } - public abstract String launch(String proxyFileName, String userDN) - throws - java.rmi.RemoteException, - javax.xml.rpc.ServiceException; + public abstract String launch(String addressWS, String workflowContent, String inputs, String settings, String proxyFileName) + throws java.rmi.RemoteException, javax.xml.rpc.ServiceException, BusinessException; - public abstract void kill(String workflowID) + public abstract void kill(String addressWS, String workflowID) throws java.rmi.RemoteException, - javax.xml.rpc.ServiceException; + javax.xml.rpc.ServiceException, BusinessException; - public abstract SimulationStatus getStatus(String workflowID) + public abstract SimulationStatus getStatus(String addressWS, String workflowID) throws java.rmi.RemoteException, - javax.xml.rpc.ServiceException; - - public String getWorkflow() { - - return workflow; - } - - public void setWorkflow(File workflow) { - this.workflow = FileUtil.read(workflow); - } - - public String getInput() { - - return input; - } - - public void setInput(List parameters) { - - this.input = WorkflowEngineInstantiator.setParametersAsXMLInput(parameters); - } - - private static String setParametersAsXMLInput(List parameters) { - - //generate the xml input file according to the user input on the GUI - StringBuilder xml = new StringBuilder("\n"); - xml.append("\n"); - - for (ParameterSweep parameter : parameters) { - - xml.append("\t\n") - .append("\n"); - - int counter = 0; - for (String value : parameter.getValues()) { - - - xml.append("\t\t") - .append("") - .append(value) - .append("\n"); - - counter++; - } - - xml.append("\n"); - xml.append("\t\n"); + javax.xml.rpc.ServiceException, BusinessException; + + protected void loadTrustStore(Server server) { + // Configuration SSL + if (Path.of(server.getTruststoreFile()).toFile().exists()) { + System.setProperty("javax.net.ssl.trustStore", server.getTruststoreFile()); + System.setProperty("javax.net.ssl.trustStorePassword", server.getTruststorePass()); + System.setProperty("javax.net.ssl.trustStoreType", "JKS"); } - - xml.append("\n"); - - return xml.toString(); } } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/AbstractWorkflowParser.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/AbstractWorkflowParser.java index cf392ca73..c174259d3 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/AbstractWorkflowParser.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/AbstractWorkflowParser.java @@ -39,6 +39,8 @@ import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; import java.io.FileReader; import java.io.IOException; import java.io.Reader; @@ -61,16 +63,18 @@ protected AbstractWorkflowParser() { sources = new ArrayList(); } - public Descriptor parse(String fileName) throws IOException, SAXException { + public Descriptor parse(String fileName) throws IOException, SAXException, ParserConfigurationException { return parse(new FileReader(fileName)); } - public Descriptor parseString(String workflowString) throws IOException, SAXException { + public Descriptor parseString(String workflowString) throws IOException, SAXException, ParserConfigurationException { return parse(new StringReader(workflowString)); } - private Descriptor parse(Reader workflowReader) throws IOException, SAXException { - reader = XMLReaderFactory.createXMLReader(); + private Descriptor parse(Reader workflowReader) throws IOException, SAXException, ParserConfigurationException { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + reader = parserFactory.newSAXParser().getXMLReader(); reader.setContentHandler(this); reader.parse(new InputSource(workflowReader)); diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputM2Parser.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputM2Parser.java index ece69ee16..f4e739995 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputM2Parser.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputM2Parser.java @@ -56,6 +56,9 @@ import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + /** * Parse a m2 input file. * @@ -93,13 +96,15 @@ public Map parse(String fileName) throws BusinessException { try { - XMLReader reader = XMLReaderFactory.createXMLReader(); + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + XMLReader reader = parserFactory.newSAXParser().getXMLReader(); reader.setContentHandler(this); reader.parse(new InputSource(new FileReader(fileName))); return inputs; - } catch (IOException | SAXException ex) { + } catch (IOException | SAXException | ParserConfigurationException ex) { logger.error("Error parsing {}", fileName, ex); throw new BusinessException(ex); } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputParser.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputParser.java index 77a91de43..f8566d2e8 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputParser.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/business/simulation/parser/InputParser.java @@ -45,6 +45,9 @@ import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + /** * Parse a input file. * @@ -69,13 +72,15 @@ public InputParser() { public String parse(String fileName) throws BusinessException { try { - reader = XMLReaderFactory.createXMLReader(); + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + reader = parserFactory.newSAXParser().getXMLReader(); reader.setContentHandler(this); reader.parse(new InputSource(new FileReader(fileName))); return inputs.toString(); - } catch (IOException | SAXException ex) { + } catch (IOException | SAXException | ParserConfigurationException ex) { logger.error("Error parsing file {}", fileName, ex); throw new BusinessException(ex); } diff --git a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java index 90b512d4b..ebb895bad 100644 --- a/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java +++ b/vip-application/src/main/java/fr/insalyon/creatis/vip/application/server/rpc/WorkflowServiceImpl.java @@ -52,6 +52,7 @@ import org.slf4j.LoggerFactory; import jakarta.servlet.ServletException; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; diff --git a/vip-application/src/main/java/localhost/moteur_service_wsdl/Moteur_ServiceLocator.java b/vip-application/src/main/java/localhost/moteur_service_wsdl/Moteur_ServiceLocator.java index e4f4d0078..d845dff50 100644 --- a/vip-application/src/main/java/localhost/moteur_service_wsdl/Moteur_ServiceLocator.java +++ b/vip-application/src/main/java/localhost/moteur_service_wsdl/Moteur_ServiceLocator.java @@ -31,6 +31,8 @@ */ package localhost.moteur_service_wsdl; +import java.net.URISyntaxException; + public class Moteur_ServiceLocator extends org.apache.axis.client.Service implements localhost.moteur_service_wsdl.Moteur_Service { /** @@ -71,9 +73,11 @@ public void setmoteur_serviceWSDDServiceName(java.lang.String name) { public localhost.moteur_service_wsdl.Moteur_servicePortType getmoteur_service() throws javax.xml.rpc.ServiceException { java.net.URL endpoint; try { - endpoint = new java.net.URL(moteur_service_address); + endpoint = new java.net.URI(moteur_service_address).toURL(); } catch (java.net.MalformedURLException e) { throw new javax.xml.rpc.ServiceException(e); + } catch (URISyntaxException e) { + throw new javax.xml.rpc.ServiceException(e); } return getmoteur_service(endpoint); } @@ -100,7 +104,7 @@ public void setmoteur_serviceEndpointAddress(java.lang.String address) { public java.rmi.Remote getPort(Class serviceEndpointInterface) throws javax.xml.rpc.ServiceException { try { if (localhost.moteur_service_wsdl.Moteur_servicePortType.class.isAssignableFrom(serviceEndpointInterface)) { - localhost.moteur_service_wsdl.Moteur_BindingStub _stub = new localhost.moteur_service_wsdl.Moteur_BindingStub(new java.net.URL(moteur_service_address), this); + localhost.moteur_service_wsdl.Moteur_BindingStub _stub = new localhost.moteur_service_wsdl.Moteur_BindingStub(new java.net.URI(moteur_service_address).toURL(), this); _stub.setPortName(getmoteur_serviceWSDDServiceName()); return _stub; } diff --git a/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/BaseApplicationSpringIT.java b/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/BaseApplicationSpringIT.java index 0fdc5d009..7f89fb4da 100644 --- a/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/BaseApplicationSpringIT.java +++ b/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/BaseApplicationSpringIT.java @@ -8,7 +8,7 @@ import fr.insalyon.creatis.vip.application.server.business.AppVersionBusiness; import fr.insalyon.creatis.vip.application.server.business.ApplicationBusiness; import fr.insalyon.creatis.vip.application.server.business.EngineBusiness; -import fr.insalyon.creatis.vip.application.server.business.simulation.WebServiceEngine; +import fr.insalyon.creatis.vip.application.server.business.simulation.WorkflowEngineInstantiator; import fr.insalyon.creatis.vip.core.client.bean.Group; import fr.insalyon.creatis.vip.core.integrationtest.database.BaseSpringIT; import fr.insalyon.creatis.vip.core.server.business.BusinessException; @@ -22,10 +22,10 @@ public class BaseApplicationSpringIT extends BaseSpringIT { @Autowired protected WorkflowDAO workflowDAO; @Autowired protected OutputDAO outputDAO; @Autowired protected InputDAO inputDAO; - @Autowired protected WebServiceEngine webServiceEngine; @Autowired protected ApplicationBusiness applicationBusiness; @Autowired protected EngineBusiness engineBusiness; @Autowired protected AppVersionBusiness appVersionBusiness; + @Autowired protected WorkflowEngineInstantiator webServiceEngine; @BeforeEach protected void setUp() throws Exception { @@ -48,7 +48,7 @@ protected AppVersionBusiness getAppVersionBusiness() { return appVersionBusiness; } - public WebServiceEngine getWebServiceEngine() { + public WorkflowEngineInstantiator getWebServiceEngine() { return webServiceEngine; } diff --git a/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/SpringApplicationTestConfig.java b/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/SpringApplicationTestConfig.java index 86525bccc..bebe2c0c5 100644 --- a/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/SpringApplicationTestConfig.java +++ b/vip-application/src/test/java/fr/insalyon/creatis/vip/application/integrationtest/SpringApplicationTestConfig.java @@ -1,7 +1,7 @@ package fr.insalyon.creatis.vip.application.integrationtest; import fr.insalyon.creatis.moteur.plugins.workflowsdb.dao.*; -import fr.insalyon.creatis.vip.application.server.business.simulation.WebServiceEngine; +import fr.insalyon.creatis.vip.application.server.business.simulation.WorkflowEngineInstantiator; import org.hibernate.SessionFactory; import org.mockito.Mockito; import org.springframework.context.annotation.Bean; @@ -61,8 +61,8 @@ public StatsDAO mockStatsDAO() { @Bean @Primary - public WebServiceEngine mockWebServiceEngine() { - return Mockito.mock(WebServiceEngine.class); + public WorkflowEngineInstantiator mockWebServiceEngine() { + return Mockito.mock(WorkflowEngineInstantiator.class); } } diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/client/view/CoreConstants.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/client/view/CoreConstants.java index d2f172898..d7f417579 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/client/view/CoreConstants.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/client/view/CoreConstants.java @@ -195,6 +195,9 @@ public class CoreConstants implements IsSerializable { //moteurlite public static final String USE_MOTEURLITE = "moteurlite.enabled"; + public static final String USE_REST_MOTEUR_SERVER = "moteur.rest.enabled"; + public static final String MOTEUR_REST_PASSWORD = "moteur.rest.password"; + public static enum GROUP_ROLE implements IsSerializable { diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/auth/SamlAuthenticationService.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/auth/SamlAuthenticationService.java index c9e7b7d19..a962a39f8 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/auth/SamlAuthenticationService.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/auth/SamlAuthenticationService.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; @@ -139,7 +140,7 @@ protected void checkValidRequest(HttpServletRequest request) throws BusinessExce logger.error("Error with SAML assertion {} : audience is not valid", new String(xmlAssertion)); throw new BusinessException("Assertion audience is not valid!"); } - } catch (MalformedURLException ex) { + } catch (MalformedURLException | URISyntaxException ex) { logger.error("Error with SAML assertion {}", new String(xmlAssertion), ex); throw new BusinessException(ex); } diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SamlTokenValidator.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SamlTokenValidator.java index d6d21ce05..1705aff2e 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SamlTokenValidator.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SamlTokenValidator.java @@ -39,7 +39,8 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URISyntaxException; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.Charset; import java.security.KeyFactory; @@ -174,7 +175,7 @@ public static boolean isTimeValid(Assertion assertion) { return true; } - public static boolean isAudienceValid(String server, Assertion assertion) throws MalformedURLException { + public static boolean isAudienceValid(String server, Assertion assertion) throws MalformedURLException, URISyntaxException { String serverHost = getHost(server); if (assertion.getConditions() == null) { return true; @@ -194,7 +195,7 @@ public static String getEmail(Assertion assertion) { } /// Private methods - private static String getHost(String serverURL) throws MalformedURLException { - return new URL(serverURL).getHost(); + private static String getHost(String serverURL) throws MalformedURLException, URISyntaxException { + return new URI(serverURL).toURL().getHost(); } } diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/Server.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/Server.java index 8d23346b1..beb2ae297 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/Server.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/Server.java @@ -112,4 +112,8 @@ public interface Server { String getReproVIPRootDir(); boolean useMoteurlite(); + + boolean useRestMoteurServer(); + + String getMoteurServerPassword(); } diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SpringConfigServer.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SpringConfigServer.java index 317d2c35a..ac5d37244 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SpringConfigServer.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/SpringConfigServer.java @@ -436,4 +436,14 @@ public String getReproVIPRootDir() { public boolean useMoteurlite() { return env.getProperty(CoreConstants.USE_MOTEURLITE, Boolean.class, false); } + + @Override + public boolean useRestMoteurServer() { + return env.getProperty(CoreConstants.USE_REST_MOTEUR_SERVER, Boolean.class, false); + } + + @Override + public String getMoteurServerPassword() { + return env.getRequiredProperty(CoreConstants.MOTEUR_REST_PASSWORD); + } } diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/proxy/ProxyClient.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/proxy/ProxyClient.java index fff17427b..28abaeb74 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/proxy/ProxyClient.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/business/proxy/ProxyClient.java @@ -152,12 +152,20 @@ private void addVomsExtension(String vo) throws Exception { // Voms Extension Server serverConf = server; long hours = Long.parseLong(serverConf.getMyProxyLifeTime()) / 3600; - String command = "voms-proxy-init -voms " + vo - + " -cert " + serverConf.getServerProxy() - + " -key " + serverConf.getServerProxy() - + " -out " + serverConf.getServerProxy(vo) - + " -noregen -valid " + hours + ":00"; - Process process = Runtime.getRuntime().exec(command); + List command = new ArrayList<>(); + command.add("voms-proxy-init"); + command.add("-voms"); + command.add(vo); + command.add("-cert"); + command.add(serverConf.getServerProxy()); + command.add("-key"); + command.add(serverConf.getServerProxy()); + command.add("-out"); + command.add(serverConf.getServerProxy(vo)); + command.add("-noregen"); + command.add("-valid"); + command.add(hours + ":00"); + Process process = Runtime.getRuntime().exec(command.toArray(new String[]{})); BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream())); String s = null; @@ -301,7 +309,7 @@ private Date saveCredentials(String proxyName) throws Exception { outFile.delete(); outFile.createNewFile(); // set permission - String command = "chmod 0600 " + proxyName; + String[] command = new String[]{"chmod", "0600", proxyName}; Runtime.getRuntime().exec(command); printStream = new PrintStream(new FileOutputStream(outFile)); diff --git a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/rpc/ConfigurationServiceImpl.java b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/rpc/ConfigurationServiceImpl.java index fea7b916c..ec9be7266 100644 --- a/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/rpc/ConfigurationServiceImpl.java +++ b/vip-core/src/main/java/fr/insalyon/creatis/vip/core/server/rpc/ConfigurationServiceImpl.java @@ -51,6 +51,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.Timestamp; import java.util.ArrayList; @@ -518,24 +520,24 @@ public String getCASLoginPageUrl() throws CoreException { URL url; try { url = getBaseURL(); - } catch (MalformedURLException e) { + } catch (MalformedURLException | URISyntaxException e) { throw new CoreException(e); } return configurationBusiness.getLoginUrlCas(url); } - private URL getBaseURL() throws MalformedURLException { + private URL getBaseURL() throws MalformedURLException, URISyntaxException { URL url; HttpServletRequest request = this.getThreadLocalRequest(); if ((request.getServerPort() == 80) || (request.getServerPort() == 443)) { - url = new URL(request.getScheme() + "://" + url = new URI(request.getScheme() + "://" + request.getServerName() - + request.getContextPath()); + + request.getContextPath()).toURL(); } else { - url = new URL(request.getScheme() + "://" + url = new URI(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() - + request.getContextPath()); + + request.getContextPath()).toURL(); } return url; } diff --git a/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringDatabaseIT.java b/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringDatabaseIT.java index 9d4f649aa..fade9e22a 100644 --- a/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringDatabaseIT.java +++ b/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringDatabaseIT.java @@ -16,6 +16,8 @@ import org.springframework.transaction.annotation.Transactional; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; @@ -148,10 +150,10 @@ public void shouldHandleConnectionCreationIssue() throws SQLException { @Test @Order(7) @Transactional(propagation = Propagation.NOT_SUPPORTED) - public void connectionShouldBeLazyInTransaction() throws SQLException, MalformedURLException { + public void connectionShouldBeLazyInTransaction() throws SQLException, MalformedURLException, URISyntaxException { // getConnection throw an exception but should not be called as 'getLoginUrlCas' do not need db access Mockito.doThrow(SQLException.class).when(dataSource).getConnection(); - String res = configurationBusiness.getLoginUrlCas(new URL("file:/plop")); + String res = configurationBusiness.getLoginUrlCas(new URI("file:/plop").toURL()); assertEquals(ServerMockConfig.TEST_CAS_URL + "/login?service=file:/plop", res); Mockito.reset(dataSource); } diff --git a/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringJndiIT.java b/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringJndiIT.java index 890fca494..cce5c02f5 100644 --- a/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringJndiIT.java +++ b/vip-core/src/test/java/fr/insalyon/creatis/vip/core/integrationtest/database/SpringJndiIT.java @@ -34,6 +34,8 @@ import javax.sql.DataSource; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; @@ -210,12 +212,12 @@ public void shouldHandleConnectionCreationIssue() throws SQLException { @Test @Order(8) - public void connectionShouldBeLazyInTransaction() throws SQLException, MalformedURLException { + public void connectionShouldBeLazyInTransaction() throws SQLException, MalformedURLException, URISyntaxException { JdbcTemplate jdbcTemplate = new JdbcTemplate(lazyDataSource); // close the datasource to make the next request fail try { jdbcTemplate.execute("SHUTDOWN"); } catch (Exception e) {e.printStackTrace();} // getConnection throw an exception but should not be called as 'getLoginUrlCas' do not need db access - String res = configurationBusiness.getLoginUrlCas(new URL("file:/plop")); + String res = configurationBusiness.getLoginUrlCas(new URI("file:/plop").toURL()); assertEquals(ServerMockConfig.TEST_CAS_URL + "/login?service=file:/plop", res); } } diff --git a/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/business/GirderStorageBusiness.java b/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/business/GirderStorageBusiness.java index 847de45f2..1407fd1f8 100644 --- a/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/business/GirderStorageBusiness.java +++ b/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/business/GirderStorageBusiness.java @@ -51,6 +51,8 @@ import java.io.InputStreamReader; import java.io.IOException; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.Connection; import java.util.Optional; @@ -154,7 +156,7 @@ public String getToken(String userEmail, String apiUrl, String storageId) ObjectNode node = new ObjectMapper().readValue(res.response, ObjectNode.class); return node.get("authToken").get("token").asText(); - } catch (IOException | NullPointerException ex) { + } catch (IOException | NullPointerException | URISyntaxException ex) { logger.error("Error getting girder token for {} with key {}", userEmail, key, ex); throw new BusinessException("Unable to get token from api key", ex); @@ -183,7 +185,7 @@ private String getFilename(String apiUrl, String fileId, String token) // clean filename as in an uploaded file return DataManagerUtil.getCleanFilename(name); - } catch (IOException ex) { + } catch (IOException | URISyntaxException ex) { logger.error("Error getting girder filename for {} with token {}", fileId, token, ex); throw new BusinessException("Unable to get file info", ex); @@ -196,9 +198,9 @@ private HttpResult makeHttpRequest( String surl, String method, Optional> connectionUpdater) - throws IOException { + throws IOException, URISyntaxException { - URL url = new URL(surl); + URL url = new URI(surl).toURL(); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod(method); diff --git a/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/rpc/FileUploadServiceImpl.java b/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/rpc/FileUploadServiceImpl.java index 3a6830de3..40a410d40 100644 --- a/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/rpc/FileUploadServiceImpl.java +++ b/vip-datamanagement/src/main/java/fr/insalyon/creatis/vip/datamanager/server/rpc/FileUploadServiceImpl.java @@ -41,9 +41,9 @@ import fr.insalyon.creatis.vip.datamanager.server.DataManagerUtil; import fr.insalyon.creatis.vip.datamanager.server.business.DataManagerBusiness; import fr.insalyon.creatis.vip.datamanager.server.business.LfcPathsBusiness; -import org.apache.commons.fileupload2.core.FileItem; -import org.apache.commons.fileupload2.core.FileItemFactory; +import org.apache.commons.fileupload2.core.DiskFileItem; import org.apache.commons.fileupload2.core.DiskFileItemFactory; +import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload; import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +57,6 @@ import java.io.File; import java.io.PrintWriter; import java.text.Normalizer; -import java.util.Iterator; import java.util.List; /** @@ -93,12 +92,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) logger.info("upload received from " + user.getEmail()); if (user != null && JakartaServletFileUpload.isMultipartContent(request)) { - FileItemFactory factory = DiskFileItemFactory.builder().get(); - JakartaServletFileUpload upload = new JakartaServletFileUpload(factory); - List items = upload.parseRequest(request); - Iterator iter = items.iterator(); + DiskFileItemFactory factory = DiskFileItemFactory.builder().get(); + JakartaServletDiskFileUpload upload = new JakartaServletDiskFileUpload(factory); + List items = upload.parseRequest(request); String fileName = null; - FileItem fileItem = null; + DiskFileItem fileItem = null; String path = null; String target = "uploadComplete"; boolean single = true; @@ -106,9 +104,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) boolean usePool = true; String operationID = "no-id"; - while (iter.hasNext()) { - FileItem item = (FileItem) iter.next(); - + for (DiskFileItem item : items) { switch (item.getFieldName()) { case "path": path = item.getString(); diff --git a/vip-gatelab/src/main/java/fr/insalyon/creatis/vip/gatelab/server/business/GateLabInputsParser.java b/vip-gatelab/src/main/java/fr/insalyon/creatis/vip/gatelab/server/business/GateLabInputsParser.java index bdcc8ae0d..7abb3b96d 100644 --- a/vip-gatelab/src/main/java/fr/insalyon/creatis/vip/gatelab/server/business/GateLabInputsParser.java +++ b/vip-gatelab/src/main/java/fr/insalyon/creatis/vip/gatelab/server/business/GateLabInputsParser.java @@ -33,6 +33,7 @@ import java.io.FileReader; import java.io.IOException; +import java.text.ParseException; import java.util.HashMap; import java.util.Map; @@ -47,6 +48,9 @@ import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + /** * Parse a gatelab input file. * @@ -72,13 +76,15 @@ public GateLabInputsParser() { public Map parse(String fileName) { try { - reader = XMLReaderFactory.createXMLReader(); + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + reader = parserFactory.newSAXParser().getXMLReader(); reader.setContentHandler(this); reader.parse(new InputSource(new FileReader(fileName))); return inputsMap; - } catch (IOException | SAXException ex) { + } catch (IOException | SAXException | ParserConfigurationException ex) { logger.error("Error parsing {}", fileName, ex); } return null; diff --git a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/GridaClientLocal.java b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/GridaClientLocal.java index 9cdc27666..1d99f13a8 100644 --- a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/GridaClientLocal.java +++ b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/GridaClientLocal.java @@ -5,6 +5,8 @@ import fr.insalyon.creatis.grida.common.bean.GridData; import fr.insalyon.creatis.grida.common.bean.GridData.Type; import fr.insalyon.creatis.vip.core.server.business.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.DependsOn; @@ -31,6 +33,8 @@ @DependsOn("localConfiguration") // to populate rootDirName @Value public class GridaClientLocal extends GRIDAClient { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private Server server; private Path localRoot; // local folder simulating remote LFN hierarchy @@ -42,6 +46,7 @@ public GridaClientLocal( @Value("${local.grida.root.dirname}") String rootDirName) throws IOException { super(null, 0, null); this.server = server; + logger.info("Creating local GRIDAClient with rootDirName: {}", rootDirName); localRoot = Paths.get(vipConfigFolder.getURI()).resolve(rootDirName); } @@ -49,6 +54,7 @@ public GridaClientLocal( @PostConstruct public void init() { // creating root if needed + logger.info("Local GRIDAClient root: {}", localRoot); if (localRoot.toFile().exists() && ! localRoot.toFile().isDirectory()) { throw new IllegalStateException("grida local root must be a directory"); } else if (localRoot.toFile().exists()) { @@ -66,6 +72,7 @@ public void init() { } private void createDirectory(Path dir, String description) { + logger.info("Creating {} directory: {}", description, dir); boolean mkdirOK = dir.toFile().mkdirs(); if ( ! mkdirOK) { throw new IllegalStateException("Error creating " + description + " directory"); @@ -195,6 +202,7 @@ public void createFolder(String path, String folderName) throws GRIDAClientExcep try { Files.createDirectory(localRoot.resolve(path).resolve(folderName)); } catch (IOException e) { + logger.error("Error creating folder {}", folderName, e); throw new GRIDAClientException(e); } } @@ -206,7 +214,7 @@ public void rename(String oldPath, String newPath) throws GRIDAClientException { @Override public boolean exist(String remotePath) throws GRIDAClientException { - while (remotePath.startsWith("/")) { + while (remotePath.startsWith("\\") || remotePath.startsWith("/")) { // TODO : / ou \ remotePath = remotePath.substring(1); } return localRoot.resolve(remotePath).toFile().exists(); diff --git a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalBashEngine.java b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalBashEngine.java index 2beafcb84..8910d5b09 100644 --- a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalBashEngine.java +++ b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalBashEngine.java @@ -77,10 +77,10 @@ public LocalBashEngine( } } - public String launch(File workflowFile, List parameters) { + public String launch(String workflowContent, List parameters) { try { LocalBashExecution newExecution = - createExecution(workflowFile, parameters); + createExecution(workflowContent, parameters); String execId = newExecution.id; executionsInfo.put(execId, newExecution); newExecution.status = SimulationStatus.Queued; @@ -130,22 +130,20 @@ public void kill(String workflowID) { execFuture.cancel(true); } - private LocalBashExecution createExecution(File workflowFile, List parameters) throws IOException { + private LocalBashExecution createExecution(String workflowContent, List parameters) throws IOException { LocalBashExecution exec = new LocalBashExecution(); - exec.workflowFile = workflowFile; exec.id = createWorkflowId(); exec.workflowDir = createWorkflowDir(exec.id); - exec.gwendiaInputs = getGwendiaInputs(workflowFile); - exec.gwendiaOutputs = getGwendiaOutputs(workflowFile); + exec.gwendiaInputs = getGwendiaInputs(workflowContent); + exec.gwendiaOutputs = getGwendiaOutputs(workflowContent); exec.execInputs = getExecInputs(parameters); - exec.scriptFileLFN = getGwendiaScriptFile(workflowFile); + exec.scriptFileLFN = getGwendiaScriptFile(workflowContent); exec.execOutputs = new HashMap<>(); return exec; } public static class LocalBashExecution { String id; - File workflowFile; Path workflowDir; String scriptFileLFN; Map execInputs; // name -> value @@ -167,33 +165,33 @@ private Path createWorkflowDir(String execId) throws IOException { return dir; } - private Map getGwendiaInputs(File workflowFile) throws IOException { + private Map getGwendiaInputs(String workflowContent) throws IOException { // Pattern pattern = Pattern.compile("\\s*\\s*"); - return Files.lines(workflowFile.toPath()) + return workflowContent.lines() .map(line -> pattern.matcher(line)) .filter(matcher -> matcher.find()) .collect(Collectors.toMap(matcher -> matcher.group(1), matcher -> matcher.group(2))); } - private Map getGwendiaOutputs(File workflowFile) throws IOException { + private Map getGwendiaOutputs(String workflowContent) throws IOException { // Pattern pattern = Pattern.compile("\\s*\\s*"); - return Files.lines(workflowFile.toPath()) + return workflowContent.lines() .map(line -> pattern.matcher(line)) .filter(matcher -> matcher.find()) .collect(Collectors.toMap(matcher -> matcher.group(1), matcher -> matcher.group(2))); } - private String getGwendiaScriptFile(File workflowFile) throws IOException { + private String getGwendiaScriptFile(String workflowContent) throws IOException { // Pattern pattern = Pattern.compile("\\s*\\s*"); - List bashScripts = Files.lines(workflowFile.toPath()) + List bashScripts = workflowContent.lines() .map(line -> pattern.matcher(line)) .filter(matcher -> matcher.find()) .map(matcher -> matcher.group(1)) diff --git a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalInitializer.java b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalInitializer.java index c44869f46..1fad87ff3 100644 --- a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalInitializer.java +++ b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalInitializer.java @@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalWorkflowExecutionBusiness.java b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalWorkflowExecutionBusiness.java new file mode 100644 index 000000000..5bd2d6bed --- /dev/null +++ b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/LocalWorkflowExecutionBusiness.java @@ -0,0 +1,62 @@ +package fr.insalyon.creatis.vip.local; + +import fr.insalyon.creatis.moteur.plugins.workflowsdb.bean.Workflow; +import fr.insalyon.creatis.moteur.plugins.workflowsdb.bean.WorkflowStatus; +import fr.insalyon.creatis.vip.application.client.bean.AppVersion; +import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; +import fr.insalyon.creatis.vip.application.server.business.WorkflowExecutionBusiness; +import fr.insalyon.creatis.vip.application.server.business.simulation.ParameterSweep; +import fr.insalyon.creatis.vip.application.server.business.util.FileUtil; +import fr.insalyon.creatis.vip.core.client.bean.User; +import fr.insalyon.creatis.vip.core.server.business.BusinessException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.util.Date; +import java.util.List; + +/** + * local version to override WebServiceEngine and make all executions stuff + * by LocalBashEngine + */ + +@Service +@Profile("local") +@Primary +public class LocalWorkflowExecutionBusiness extends WorkflowExecutionBusiness { + + private final LocalBashEngine localBashEngine; + + @Autowired + public LocalWorkflowExecutionBusiness(LocalBashEngine localBashEngine) { + super(null, null); + this.localBashEngine = localBashEngine; + } + + @Override + public Workflow launch(String engineEndpoint, AppVersion appVersion, User user, String simulationName, String workflowPath, List parameters) throws BusinessException { + String workflowContent; + try { + workflowContent = FileUtil.read(new File(workflowPath)); + String workflowId = localBashEngine.launch(workflowContent, parameters); + return new Workflow(workflowId, user.getFullName(), WorkflowStatus.Running, new Date(), null, simulationName, appVersion.getApplicationName(), appVersion.getVersion(), "", engineEndpoint, null); + } catch (Exception e) { + throw new BusinessException(e); + } + } + + @Override + public void kill(String addressWS, String workflowID) { + localBashEngine.kill(workflowID); + + } + + @Override + public SimulationStatus getStatus(String addressWS, String workflowID) { + return localBashEngine.getStatus(workflowID); + } + +} diff --git a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/WebServiceEngineLocal.java b/vip-local/src/main/java/fr/insalyon/creatis/vip/local/WebServiceEngineLocal.java deleted file mode 100644 index afd7f8674..000000000 --- a/vip-local/src/main/java/fr/insalyon/creatis/vip/local/WebServiceEngineLocal.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.insalyon.creatis.vip.local; - -import fr.insalyon.creatis.vip.application.client.view.monitor.SimulationStatus; -import fr.insalyon.creatis.vip.application.server.business.simulation.ParameterSweep; -import fr.insalyon.creatis.vip.application.server.business.simulation.WebServiceEngine; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Profile; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Service; - -import javax.xml.rpc.ServiceException; -import java.io.File; -import java.rmi.RemoteException; -import java.util.List; - -/** - * local version to override WebServiceEngine and make all executions stuff - * by LocalBashEngine - */ - -@Service -@Scope("prototype") -@Profile("local") -@Primary -public class WebServiceEngineLocal extends WebServiceEngine { - - @Autowired - private LocalBashEngine localBashEngine; - - private String addressWS; - private String settings; - private List inputs; - private File workflow; - - @Override - public String getAddressWS() { - return addressWS; - } - - @Override - public void setAddressWS(String addressWS) { - this.addressWS = addressWS; - } - - @Override - public String getSettings() { - return settings; - } - - @Override - public void setSettings(String settings) { - this.settings = settings; - } - - @Override - public String getWorkflow() { - return workflow.toString(); - } - - @Override - public void setWorkflow(File workflow) { - this.workflow = workflow; - } - - @Override - public String getInput() { - return inputs.toString(); - } - - @Override - public void setInput(List parameters) { - this.inputs = parameters; - } - - @Override - public String launch(String proxyFileName, String userDN) throws RemoteException, ServiceException { - return localBashEngine.launch(workflow, inputs); - } - - @Override - public String getSimulationId(String launchID) { - return launchID; - } - - @Override - public void kill(String workflowID) throws RemoteException, ServiceException { - localBashEngine.kill(workflowID); - } - - @Override - public SimulationStatus getStatus(String workflowID) throws RemoteException, ServiceException { - return localBashEngine.getStatus(workflowID); - } -} diff --git a/vip-portal/src/main/webapp/documentation/import_application.html b/vip-portal/src/main/webapp/documentation/import_application.html index e8f796089..bfab35eef 100644 --- a/vip-portal/src/main/webapp/documentation/import_application.html +++ b/vip-portal/src/main/webapp/documentation/import_application.html @@ -5,61 +5,8 @@ -

How to import an application

- -

Write your application descriptor with Boutiques

-

Applications are imported into VIP using Boutiques descriptors.

- -

Please do not hesitate to contact us if you need help with writing your application descriptor. - Once the descriptor is ready, VIP admins will import it for you.

- -

We recommend that descriptors are versionned and published to Zenodo, similarly to what has been done for the Gate OpenDose application:

- - -

Note that publising to Zenodo can be done through VIP, once the application is imported with its Boutiques descriptor. In exchange, - you will get a DOI allowing for the proper citation of the application.

- -

We also recommend that you use containers (such as Docker or Singularity) to facilitate application installation and sharing.

- - -

Docker guidelines

-
    -
  • If you are not familiar with Docker, read the docs on the Docker website.
  • -
  • We recommend to build containers from a Dockerfile.
  • -
  • For efficient management of containers in VIP, we recommend that containers use the following images if possible: - -
  • -
  • Compiled applications: avoid using architecture-specific - compilation flags as it will produce non-portable code - (Illegal instruction error messages).
  • -
  • Your application is supposed to work with a regular user (not as root).
  • -
-
- - -

Troubleshooting tips: compiled Matlab functions

- Arguments of compiled Matlab functions will be passed as strings, which may create nasty - bugs at runtime. We recommend that numerical arguments are handled using code such as: -
-                
-                if ~isnumeric(parameter)
-                  parameter = str2double(parameter);
-                  if isnan(parameter)
-                    disp('The parameter value is not a number')
-                    return
-                  end
-                end
-                
-            
-
- - +

See the Application packaging guide + for instructions on how to get your application packaged, so that it can be imported by VIP admins.

+

Please do not hesitate to contact us if you need help with your application packaging.

diff --git a/vip-portal/src/test/java/fr/insalyon/creatis/applicationimporter/GwendiaTemplateTest.java b/vip-portal/src/test/java/fr/insalyon/creatis/applicationimporter/GwendiaTemplateTest.java index e107b2b52..89a37c1a5 100644 --- a/vip-portal/src/test/java/fr/insalyon/creatis/applicationimporter/GwendiaTemplateTest.java +++ b/vip-portal/src/test/java/fr/insalyon/creatis/applicationimporter/GwendiaTemplateTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; /* @@ -26,7 +27,7 @@ public class GwendiaTemplateTest { @ParameterizedTest @ValueSource(strings = {STANDALONE_TEMPLATE}) - public void testTemplateWithNonNullDescription(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException { + public void testTemplateWithNonNullDescription(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException, ParserConfigurationException { String inputDescription = "test input description"; Descriptor gwendiaDesc = testGwendiaTemplate( template, @@ -37,7 +38,7 @@ public void testTemplateWithNonNullDescription(String template) throws IOExcepti @ParameterizedTest @ValueSource(strings = {STANDALONE_TEMPLATE}) - public void testTemplateWithNullDescription(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException { + public void testTemplateWithNullDescription(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException, ParserConfigurationException { // when the description is not in boutiques, it must be an empty string in gwendia Descriptor gwendiaDesc = testGwendiaTemplate( template, @@ -48,7 +49,7 @@ public void testTemplateWithNullDescription(String template) throws IOException, @ParameterizedTest @ValueSource(strings = {STANDALONE_TEMPLATE}) - public void testTemplateWithAIntegerInput(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException { + public void testTemplateWithAIntegerInput(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException, ParserConfigurationException { Descriptor gwendiaDesc = testGwendiaTemplate( template, getIntegerInput(1, 42.)); @@ -57,7 +58,7 @@ public void testTemplateWithAIntegerInput(String template) throws IOException, I @ParameterizedTest @ValueSource(strings = {STANDALONE_TEMPLATE}) - public void testTemplateWithANumberInput(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException { + public void testTemplateWithANumberInput(String template) throws IOException, InvalidBoutiquesDescriptorException, SAXException, ParserConfigurationException { Descriptor gwendiaDesc = testGwendiaTemplate( template, getNumberInput(1, false, 42.)); @@ -79,7 +80,7 @@ protected BoutiquesInput getBasicFileInput(Object id, String description, String true, null, null, null, null, null, defaultValue); } - protected Descriptor testGwendiaTemplate(String templateFile, BoutiquesInput... inputs) throws IOException, SAXException { + protected Descriptor testGwendiaTemplate(String templateFile, BoutiquesInput... inputs) throws IOException, SAXException, ParserConfigurationException { BoutiquesApplication boutiquesApp = new BoutiquesApplication("testApp", "test app desc", "42.43"); for (BoutiquesInput input : inputs) { boutiquesApp.addInput(input);