diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6009c23b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "frontend/console"] + path = frontend/console + url = https://github.com/scc-digitalhub/digitalhub-console.git diff --git a/.vscode/launch.json b/.vscode/launch.json index ef5630cb..c009dd8e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,13 @@ "mainClass": "it.smartcommunitylabdhub.core.CoreApplication", "projectName": "core", "vmArgs": "-Dspring.profiles.active=local", + }, + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" } ] } \ No newline at end of file diff --git a/application/pom.xml b/application/pom.xml index 5d96676c..8121afc3 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -76,6 +76,10 @@ org.springframework.boot spring-boot-starter-amqp + + org.springframework.boot + spring-boot-starter-thymeleaf + org.projectlombok @@ -237,6 +241,12 @@ dh-runtime-mlrun ${revision} + + + it.smartcommunitylabdhub + dh-console + ${revision} + diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/ApiVersioningMappingConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/ApiVersioningMappingConfig.java index e75001ff..5b9313f8 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/ApiVersioningMappingConfig.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/ApiVersioningMappingConfig.java @@ -2,14 +2,20 @@ import it.smartcommunitylabdhub.core.config.handlers.VersionedHandlerMapping; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @Configuration -public class ApiVersioningMappingConfig implements WebMvcRegistrations { +public class ApiVersioningMappingConfig { - @Override - public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { - return new VersionedHandlerMapping(); + @Bean + public WebMvcRegistrations webMvcRegistrationsHandlerMapping() { + return new WebMvcRegistrations() { + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return new VersionedHandlerMapping(); + } + }; } } diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java index 9bac326c..dc9d828a 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java +++ b/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityConfig.java @@ -2,6 +2,7 @@ import static org.springframework.security.config.Customizer.withDefaults; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; import java.util.ArrayList; import java.util.Arrays; import java.util.List; diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/WebConfig.java b/application/src/main/java/it/smartcommunitylabdhub/core/config/WebConfig.java deleted file mode 100644 index a6c6dddf..00000000 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/WebConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package it.smartcommunitylabdhub.core.config; - -import java.util.List; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.web.PageableHandlerMethodArgumentResolver; -import org.springframework.data.web.config.EnableSpringDataWebSupport; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@EnableSpringDataWebSupport -public class WebConfig implements WebMvcConfigurer { - - @Override - public void addArgumentResolvers(List argumentResolvers) { - PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); - // Customize resolver properties here if needed - argumentResolvers.add(resolver); - } -} diff --git a/application/src/main/resources/application.yml b/application/src/main/resources/application.yml index 5fc130f5..034da76b 100644 --- a/application/src/main/resources/application.yml +++ b/application/src/main/resources/application.yml @@ -85,8 +85,11 @@ security: password: ${DH_AUTH_BASIC_PASSWORD:} jwt: issuer-uri: ${DH_AUTH_JWT_ISSUER_URI:} - audience: ${DH_AUTH_JWT_AUDIENCE:dhcore} + audience: ${DH_AUTH_JWT_AUDIENCE:${security.oidc.client-id}} claim: ${DH_AUTH_JWT_CLAIM:roles} + oidc: + client-id: ${DH_AUTH_OIDC_CLIENT_ID:} + scope: diff --git a/frontend/.flattened-pom.xml b/frontend/.flattened-pom.xml new file mode 100644 index 00000000..ecec6e8b --- /dev/null +++ b/frontend/.flattened-pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + it.smartcommunitylabdhub + dh-console + 0.0.29 + + + org.projectlombok + lombok + 1.18.30 + compile + true + + + org.springframework.boot + spring-boot + 3.2.0 + compile + + + org.springframework.boot + spring-boot-starter-web + 3.2.0 + compile + + + org.springframework.boot + spring-boot-starter-security + 3.2.0 + compile + + + org.springframework.boot + spring-boot-starter-thymeleaf + 3.2.0 + compile + + + it.smartcommunitylabdhub + dh-commons + 0.0.29 + compile + + + org.springdoc + springdoc-openapi-starter-common + 2.2.0 + compile + + + diff --git a/frontend/console b/frontend/console new file mode 160000 index 00000000..b96889f3 --- /dev/null +++ b/frontend/console @@ -0,0 +1 @@ +Subproject commit b96889f36295bf9c270f74decef1c6ec2f6bbf6b diff --git a/frontend/pom.xml b/frontend/pom.xml new file mode 100644 index 00000000..6bbc230d --- /dev/null +++ b/frontend/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + it.smartcommunitylabdhub + digitalhub-core + ${revision} + ../ + + it.smartcommunitylabdhub + dh-console + console + DHCore console + + + v1.22.19 + v18.17.1 + + + + org.projectlombok + lombok + ${lombok.version} + compile + true + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-security + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + ${spring-boot.version} + + + + it.smartcommunitylabdhub + dh-commons + ${revision} + + + + org.springdoc + springdoc-openapi-starter-common + ${springdoc.version} + + + + + + + + + com.github.eirslett + frontend-maven-plugin + 1.15.0 + + + ${node.version} + ${yarn.version} + ${project.build.directory} + + + + + install-node-and-yarn + + install-node-and-yarn + + + + + frontent-yarn-install + + yarn + + generate-resources + + install + ${project.basedir}/console + + + + + frontend build + + yarn + + prepare-package + + build:production + ${project.basedir}/console + + + + + + + maven-resources-plugin + + + frontend package + + copy-resources + + prepare-package + + ${project.build.outputDirectory}/console + + + ${project.basedir}/console/dist + false + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/main/java/it/smartcommunitylabdhub/console/Keys.java b/frontend/src/main/java/it/smartcommunitylabdhub/console/Keys.java new file mode 100644 index 00000000..7c10bb8e --- /dev/null +++ b/frontend/src/main/java/it/smartcommunitylabdhub/console/Keys.java @@ -0,0 +1,8 @@ +package it.smartcommunitylabdhub.console; + +public class Keys { + + public static final String CONSOLE_CONTEXT = "/console"; + + private Keys() {} +} diff --git a/frontend/src/main/java/it/smartcommunitylabdhub/console/config/ConsoleSecurityConfig.java b/frontend/src/main/java/it/smartcommunitylabdhub/console/config/ConsoleSecurityConfig.java new file mode 100644 index 00000000..7b769bc1 --- /dev/null +++ b/frontend/src/main/java/it/smartcommunitylabdhub/console/config/ConsoleSecurityConfig.java @@ -0,0 +1,68 @@ +package it.smartcommunitylabdhub.console.config; + +import it.smartcommunitylabdhub.commons.config.SecurityProperties; +import it.smartcommunitylabdhub.console.Keys; +import java.util.ArrayList; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class ConsoleSecurityConfig { + + @Autowired + SecurityProperties properties; + + @Value("${security.api.cors.origins}") + private String corsOrigins; + + public RequestMatcher getRequestMatcher() { + return new AntPathRequestMatcher(Keys.CONSOLE_CONTEXT + "/**"); + } + + @Bean("consoleSecurityFilterChain") + public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher(getRequestMatcher()) + .authorizeHttpRequests(auth -> { + auth.anyRequest().permitAll(); + }) + //disable csrf + .csrf(csrf -> csrf.disable()) + // we don't want a session for these endpoints, each request should be evaluated + .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + // allow cors + http.cors(cors -> { + if (StringUtils.hasText(corsOrigins)) { + cors.configurationSource(corsConfigurationSource(corsOrigins)); + } else { + cors.disable(); + } + }); + + return http.build(); + } + + private CorsConfigurationSource corsConfigurationSource(String origins) { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(new ArrayList<>(StringUtils.commaDelimitedListToSet(origins))); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setExposedHeaders(Arrays.asList("authorization", "range")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/frontend/src/main/java/it/smartcommunitylabdhub/console/config/WebConfig.java b/frontend/src/main/java/it/smartcommunitylabdhub/console/config/WebConfig.java new file mode 100644 index 00000000..aaba8c1c --- /dev/null +++ b/frontend/src/main/java/it/smartcommunitylabdhub/console/config/WebConfig.java @@ -0,0 +1,25 @@ +package it.smartcommunitylabdhub.console.config; + +import it.smartcommunitylabdhub.console.Keys; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.resource.EncodedResourceResolver; +import org.springframework.web.servlet.resource.PathResourceResolver; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) { + // user console dist + registry + .addResourceHandler(Keys.CONSOLE_CONTEXT + "/**") + .addResourceLocations("classpath:/console/") + .setCachePeriod(60 * 60 * 24 * 365)/* one year */ + .resourceChain(true) + .addResolver(new EncodedResourceResolver()) + .addResolver(new PathResourceResolver()); + } +} diff --git a/frontend/src/main/java/it/smartcommunitylabdhub/console/controllers/ConsoleController.java b/frontend/src/main/java/it/smartcommunitylabdhub/console/controllers/ConsoleController.java new file mode 100644 index 00000000..7948fe37 --- /dev/null +++ b/frontend/src/main/java/it/smartcommunitylabdhub/console/controllers/ConsoleController.java @@ -0,0 +1,74 @@ +package it.smartcommunitylabdhub.console.controllers; + +import it.smartcommunitylabdhub.commons.config.ApplicationProperties; +import it.smartcommunitylabdhub.commons.config.SecurityProperties; +import it.smartcommunitylabdhub.console.Keys; +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@Controller +public class ConsoleController { + + @Autowired + private ApplicationProperties applicationProperties; + + @Autowired + private SecurityProperties securityProperties; + + public static final String CONSOLE_CONTEXT = Keys.CONSOLE_CONTEXT; + + @GetMapping("/") + public ModelAndView root() { + return new ModelAndView("redirect:" + CONSOLE_CONTEXT + "/"); + } + + // @GetMapping(value = { CONSOLE_CONTEXT, CONSOLE_CONTEXT + "/**" }) + @GetMapping( + value = { + CONSOLE_CONTEXT + "/", + CONSOLE_CONTEXT + "/{path:^(?!\\S+(?:\\.[a-z0-9]{2,}))\\S+$}", + CONSOLE_CONTEXT + "/-/**", + } + ) + public String console(Model model, HttpServletRequest request) { + String requestUrl = ServletUriComponentsBuilder + .fromRequestUri(request) + .replacePath(request.getContextPath()) + .build() + .toUriString(); + + String applicationUrl = StringUtils.hasText(applicationProperties.getEndpoint()) + ? applicationProperties.getEndpoint() + : requestUrl; + + //build config + Map config = new HashMap<>(); + config.put("REACT_APP_APPLICATION_URL", applicationUrl); + config.put("REACT_APP_API_URL", "/api/v1"); + + config.put("REACT_APP_CONTEXT_PATH", CONSOLE_CONTEXT); + config.put("REACT_APP_AUTH", "none"); + if (securityProperties.isBasicAuthEnabled()) { + config.put("REACT_APP_LOGIN_URL", "/auth"); + } + + if (securityProperties.isOidcAuthEnabled()) { + config.put("REACT_APP_ISSUER_URI", securityProperties.getJwt().getIssuerUri()); + config.put("REACT_APP_CLIENT_ID", securityProperties.getOidc().getClientId()); + if (securityProperties.getOidc().getScope() != null) { + config.put("REACT_APP_SCOPE", String.join(" ", securityProperties.getOidc().getScope())); + } + } + + model.addAttribute("config", config); + return "console.html"; + } +} diff --git a/frontend/src/main/resources/templates/console.html b/frontend/src/main/resources/templates/console.html new file mode 100644 index 00000000..bf01851c --- /dev/null +++ b/frontend/src/main/resources/templates/console.html @@ -0,0 +1,27 @@ + + + + + + + dh-console + + + + +
+
+
Loading...
+
+
+ + + + \ No newline at end of file diff --git a/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/ApplicationProperties.java b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/ApplicationProperties.java new file mode 100644 index 00000000..a46911f3 --- /dev/null +++ b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/ApplicationProperties.java @@ -0,0 +1,20 @@ +package it.smartcommunitylabdhub.commons.config; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "application", ignoreUnknownFields = true) +@Getter +@Setter +public class ApplicationProperties { + + private String endpoint; + private String name; + private String description; + private String version; + private List profiles; +} diff --git a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityProperties.java b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java similarity index 74% rename from application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityProperties.java rename to modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java index fd6a9127..b9ec79d9 100644 --- a/application/src/main/java/it/smartcommunitylabdhub/core/config/SecurityProperties.java +++ b/modules/commons/src/main/java/it/smartcommunitylabdhub/commons/config/SecurityProperties.java @@ -1,5 +1,6 @@ -package it.smartcommunitylabdhub.core.config; +package it.smartcommunitylabdhub.commons.config; +import java.util.List; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -19,6 +20,9 @@ public class SecurityProperties { @NestedConfigurationProperty private JwtAuthenticationProperties jwt; + @NestedConfigurationProperty + private OidcAuthenticationProperties oidc; + public boolean isBasicAuthEnabled() { return basic != null && basic.isEnabled(); } @@ -27,6 +31,10 @@ public boolean isJwtAuthEnabled() { return jwt != null && jwt.isEnabled(); } + public boolean isOidcAuthEnabled() { + return oidc != null && oidc.isEnabled(); + } + public boolean isRequired() { return isBasicAuthEnabled() || isJwtAuthEnabled(); } @@ -55,4 +63,16 @@ public boolean isEnabled() { return StringUtils.hasText(issuerUri) && StringUtils.hasText(audience); } } + + @Getter + @Setter + public static class OidcAuthenticationProperties { + + private String clientId; + private List scope; + + public boolean isEnabled() { + return StringUtils.hasText(clientId); + } + } } diff --git a/pom.xml b/pom.xml index c7b755f3..8c99cbd6 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ modules/runtime-dbt modules/runtime-nefertem modules/runtime-mlrun + frontend application