diff --git a/.github/workflows/wls-wahlvorstand-service_pull-request.yml b/.github/workflows/wls-wahlvorstand-service_pull-request.yml new file mode 100644 index 000000000..de7ea2494 --- /dev/null +++ b/.github/workflows/wls-wahlvorstand-service_pull-request.yml @@ -0,0 +1,14 @@ +name: verify pull request wahlvorstand-service + +on: + pull_request: + paths: + - 'wls-wahlvorstand-service/**' + - '.github/workflows/wls-wahlvorstand-service_pull-request.yml' + +jobs: + verify-pull-request: + uses: + ./.github/workflows/callable-run-mvn-verify.yml + with: + pom-dir: 'wls-wahlvorstand-service' \ No newline at end of file diff --git a/.github/workflows/wls-wahlvorstand-service_push-dev.yml b/.github/workflows/wls-wahlvorstand-service_push-dev.yml new file mode 100644 index 000000000..b0990417a --- /dev/null +++ b/.github/workflows/wls-wahlvorstand-service_push-dev.yml @@ -0,0 +1,18 @@ +name: build push dev wahlvorstand-service + +on: + push: + branches: + - dev + paths: + - 'wls-wahlvorstand-service/**' + - '.github/workflows/wls-wahlvorstand-service_push-dev.yml' + +jobs: + build-github-container-image: + permissions: + packages: write + uses: + ./.github/workflows/callable-create-github-container-image.yml + with: + service: 'wls-wahlvorstand-service' \ No newline at end of file diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ff205b285..2bea765df 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -50,6 +50,7 @@ export default withMermaid({ {text: 'EAI-Service', link: `${PATH_FEATURES}eai-service/`}, {text: 'Basisdaten-Service', link: `${PATH_FEATURES}basisdaten-service/`}, {text: 'Monitoring-Service', link: `${PATH_FEATURES}monitoring-service/`}, + {text: 'Wahlvorstand-Service', link: `${PATH_FEATURES}wahlvorstand-service/`}, ] }, diff --git a/docs/src/features/wahlvorstand-service/index.md b/docs/src/features/wahlvorstand-service/index.md new file mode 100644 index 000000000..e113cf43c --- /dev/null +++ b/docs/src/features/wahlvorstand-service/index.md @@ -0,0 +1,15 @@ +# Wahlvorstand-Service + +Service für Themen im Zusammenhang mit Wahlvorständen und Anwesenheiten. + +## Abhängigkeiten + +Folgende Services werden für den Betrieb benötigt: +- Basisdaten-Service +- EAI-Service +- Infomanagement-Service + +## Daten und Funktionen + +- Abrufen der Wahlvorstände der Wahlbezirke +- Pflege der Anwesenheiten der Wahlvorstände \ No newline at end of file diff --git a/stack/oracle-database/add-user-on-startup.sql b/stack/oracle-database/add-user-on-startup.sql index 45140f888..56f552ea8 100644 --- a/stack/oracle-database/add-user-on-startup.sql +++ b/stack/oracle-database/add-user-on-startup.sql @@ -30,4 +30,8 @@ GRANT CONNECT, RESOURCE, CREATE SESSION TO wls_basisdaten_service; -- add user for wls-monitoring-service CREATE USER wls_monitoring_service IDENTIFIED BY secret QUOTA UNLIMITED ON USERS; -GRANT CONNECT, RESOURCE, CREATE SESSION TO wls_monitoring_service; \ No newline at end of file +GRANT CONNECT, RESOURCE, CREATE SESSION TO wls_monitoring_service; + +-- add user for wls-wahlvorstand-service +CREATE USER wls_wahlvorstand_service IDENTIFIED BY secret QUOTA UNLIMITED ON USERS; +GRANT CONNECT, RESOURCE, CREATE SESSION TO wls_wahlvorstand_service; \ No newline at end of file diff --git a/wls-wahlvorstand-service/.gitignore b/wls-wahlvorstand-service/.gitignore new file mode 100644 index 000000000..ca049981e --- /dev/null +++ b/wls-wahlvorstand-service/.gitignore @@ -0,0 +1,31 @@ +# Covers Maven specific +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +!/.mvn/wrapper/maven-wrapper.jar + +# Covers Eclipse specific: +.settings/ +.classpath +.project + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +.idea +*.iml + +# Covers Netbeans: +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + diff --git a/wls-wahlvorstand-service/Dockerfile b/wls-wahlvorstand-service/Dockerfile new file mode 100644 index 000000000..cc5bed40e --- /dev/null +++ b/wls-wahlvorstand-service/Dockerfile @@ -0,0 +1,3 @@ +FROM registry.access.redhat.com/ubi8/openjdk-17:latest + +COPY target/*.jar /deployments/spring-boot-application.jar \ No newline at end of file diff --git a/wls-wahlvorstand-service/checkstyle.xml b/wls-wahlvorstand-service/checkstyle.xml new file mode 100644 index 000000000..37f7f0e7b --- /dev/null +++ b/wls-wahlvorstand-service/checkstyle.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wls-wahlvorstand-service/pom.xml b/wls-wahlvorstand-service/pom.xml new file mode 100644 index 000000000..251a20ce4 --- /dev/null +++ b/wls-wahlvorstand-service/pom.xml @@ -0,0 +1,435 @@ + + + 4.0.0 + + de.muenchen.oss.wahllokalsystem + wls-wahlvorstand-service + 0.0.1-SNAPSHOT + wls_wahlvorstand_service + + + 17 + ${java.version} + ${java.version} + ${java.version} + UTF-8 + 3.3.0 + 2023.0.2 + 7.4 + 4.4 + 1.11.0 + 2.16.1 + 3.14.0 + 1.12.0 + 1.8.0 + + 3.11.0.3922 + + 0.8.12 + + 1.13.0 + 3.2.5 + 3.13.0 + 3.3.1 + + 1.18.30 + 0.2.0 + 2.5.0 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud.version} + pom + import + + + + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-oauth2-client + + + org.springframework.security + spring-security-oauth2-resource-server + + + org.springframework.security + spring-security-oauth2-jose + + + + + org.springframework.hateoas + spring-hateoas + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.jayway.jsonpath + json-path + + + + + com.h2database + h2 + + + com.mysql + mysql-connector-j + + + org.hibernate.orm + hibernate-core + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-oracle + + + com.oracle.database.jdbc + ojdbc11 + 23.4.0.24.05 + + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + org.apache.httpcomponents.client5 + httpclient5 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${org.springdoc.version} + + + + + jakarta.validation + jakarta.validation-api + + + org.hibernate.validator + hibernate-validator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + io.micrometer + micrometer-tracing + + + io.micrometer + micrometer-tracing-bridge-brave + + + net.logstash.logback + logstash-logback-encoder + ${logstash.encoder} + + + io.micrometer + micrometer-registry-prometheus + + + + + org.projectlombok + lombok + provided + + + + + org.apache.commons + commons-collections4 + ${apache.commons.collections4} + + + org.apache.commons + commons-csv + ${apache.commons.csv} + + + commons-io + commons-io + ${apache.commons.io} + + + org.apache.commons + commons-lang3 + ${apache.commons.lang3} + + + org.apache.commons + commons-text + ${apache.commons.text} + + + commons-validator + commons-validator + ${apache.commons.validator} + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + https://github.com/it-at-m/Wahllokalsystem + scm:git:https://github.com/it-at-m/Wahllokalsystem.git + scm:git:https://github.com/it-at-m/Wahllokalsystem.git + HEAD + + + + + + + src/main/resources + true + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven.resources.plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${org.projectlombok.lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${org.projectlombok.mapstructbinding.version} + + + + + + maven-scm-plugin + ${maven-scm-plugin.version} + + RT-REL-${project.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + ${surefireArgLine} -Dfile.encoding=${project.build.sourceEncoding} + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar.scanner.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + true + ${sonar.jacoco.reportPath} + + surefireArgLine + + + + report + prepare-package + + report + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + de.muenchen.oss + itm-java-codeformat + 1.0.9 + + + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + itm-java-codeformat/java_codestyle_formatter.xml + + + + + + + + + check + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.4.0 + + checkstyle.xml + true + + + + + check + + + + + + com.puppycrawl.tools + checkstyle + 10.17.0 + + + + + + + diff --git a/wls-wahlvorstand-service/runLocal.bat b/wls-wahlvorstand-service/runLocal.bat new file mode 100644 index 000000000..399761028 --- /dev/null +++ b/wls-wahlvorstand-service/runLocal.bat @@ -0,0 +1 @@ +mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local" diff --git a/wls-wahlvorstand-service/runLocal.sh b/wls-wahlvorstand-service/runLocal.sh new file mode 100644 index 000000000..4ecb19456 --- /dev/null +++ b/wls-wahlvorstand-service/runLocal.sh @@ -0,0 +1,2 @@ +#!/bin/bash +mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local" diff --git a/wls-wahlvorstand-service/runLocalNoSecurity.bat b/wls-wahlvorstand-service/runLocalNoSecurity.bat new file mode 100644 index 000000000..e15717119 --- /dev/null +++ b/wls-wahlvorstand-service/runLocalNoSecurity.bat @@ -0,0 +1 @@ +mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local,no-security" diff --git a/wls-wahlvorstand-service/runLocalNoSecurity.sh b/wls-wahlvorstand-service/runLocalNoSecurity.sh new file mode 100644 index 000000000..69535f252 --- /dev/null +++ b/wls-wahlvorstand-service/runLocalNoSecurity.sh @@ -0,0 +1,2 @@ +#!/bin/bash +mvn clean spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=local,no-security" diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/MicroServiceApplication.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/MicroServiceApplication.java new file mode 100644 index 000000000..5a6d58d4a --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/MicroServiceApplication.java @@ -0,0 +1,42 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +/** + * Application class for starting the micro-service. + */ +@Configuration +@ComponentScan( + basePackages = { + "org.springframework.data.jpa.convert.threeten", + "de.muenchen.oss.wahllokalsystem.wahlvorstandservice" + } +) +@EntityScan( + basePackages = { + "org.springframework.data.jpa.convert.threeten", + "de.muenchen.oss.wahllokalsystem.wahlvorstandservice" + } +) +@EnableJpaRepositories( + basePackages = { + "de.muenchen.oss.wahllokalsystem.wahlvorstandservice" + } +) +@EnableAutoConfiguration +public class MicroServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MicroServiceApplication.class, args); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfiguration.java new file mode 100644 index 000000000..222a4f415 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * The class adds a {@link HttpHeaders#CACHE_CONTROL} header to each http response, if the header is + * not already set. + */ +@Configuration +public class CacheControlConfiguration { + + private static final String CACHE_CONTROL_HEADER_VALUES = "no-cache, no-store, must-revalidate"; + + @Bean + public FilterRegistrationBean cacheControlFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new CacheControlFilter()); + registration.addUrlPatterns("/*"); + return registration; + } + + /** + * The concrete implementation of the cache control filter which adds a + * {@link HttpHeaders#CACHE_CONTROL} to a http response, if the header is not already + * set. + */ + public static class CacheControlFilter extends OncePerRequestFilter { + + /** + * The method which adds the {@link HttpHeaders#CACHE_CONTROL} header to the + * {@link HttpServletResponse} given in the parameter, if the header is not + * already set. + * + * Same contract as for {@code super.doFilter}, but guaranteed to be just invoked once per request + * within a single request thread. See + * {@link OncePerRequestFilter#shouldNotFilterAsyncDispatch()} for details. + *

+ * Provides HttpServletRequest and HttpServletResponse arguments instead of the default + * ServletRequest and ServletResponse ones. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String cacheControlHeaderValue = response.getHeader(HttpHeaders.CACHE_CONTROL); + if (StringUtils.isBlank(cacheControlHeaderValue)) { + response.addHeader(HttpHeaders.CACHE_CONTROL, CACHE_CONTROL_HEADER_VALUES); + } + + filterChain.doFilter(request, response); + + } + + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/ForwardedHeaderConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/ForwardedHeaderConfiguration.java new file mode 100644 index 000000000..5bc6c6bc1 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/ForwardedHeaderConfiguration.java @@ -0,0 +1,27 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ForwardedHeaderFilter; + +/** + * This class provides the {@link ForwardedHeaderFilter} to handle the headers of type "Forwarded" + * and "X-Forwarded-*". + */ +@Configuration +public class ForwardedHeaderConfiguration { + + @Bean + public FilterRegistrationBean forwardedHeaderFilter() { + final FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ForwardedHeaderFilter()); + registration.addUrlPatterns("/*"); + return registration; + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/JwtUserInfoAuthenticationConverter.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/JwtUserInfoAuthenticationConverter.java new file mode 100644 index 000000000..bc887132f --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/JwtUserInfoAuthenticationConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Ein custom {@link JwtAuthenticationConverter}, der die Authorities mittels + * {@link UserInfoAuthoritiesService} vom /userinfo Endpoint des OIDC Providers + * bezieht. + */ +public class JwtUserInfoAuthenticationConverter implements Converter { + + private final UserInfoAuthoritiesService userInfoService; + + /** + * Erzeugt eine neue Instanz von {@link JwtUserInfoAuthenticationConverter}. + * + * @param userInfoService ein {@link UserInfoAuthoritiesService} + */ + public JwtUserInfoAuthenticationConverter(UserInfoAuthoritiesService userInfoService) { + this.userInfoService = userInfoService; + } + + @Override + public AbstractAuthenticationToken convert(Jwt source) { + return new JwtAuthenticationToken(source, this.userInfoService.loadAuthorities(source)); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/NoSecurityConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/NoSecurityConfiguration.java new file mode 100644 index 000000000..397ce2d0b --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/NoSecurityConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@Profile("no-security") +@EnableWebSecurity +public class NoSecurityConfiguration { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(customizer -> customizer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + .authorizeHttpRequests(requests -> requests.requestMatchers(AntPathRequestMatcher.antMatcher("/**")) + .permitAll() + .requestMatchers(PathRequest.toH2Console()).permitAll() + .anyRequest() + .permitAll()) + .csrf(AbstractHttpConfigurer::disable); + // @formatter:on + return http.build(); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfiguration.java new file mode 100644 index 000000000..2e3526f82 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +/** + * The central class for configuration of all security aspects. + */ +@Configuration +@Profile("!no-security") +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +@Import(RestTemplateAutoConfiguration.class) +public class SecurityConfiguration { + + @Autowired + private RestTemplateBuilder restTemplateBuilder; + + @Value("${security.oauth2.resource.user-info-uri}") + private String userInfoUri; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((requests) -> requests.requestMatchers( + // allow access to /actuator/info + AntPathRequestMatcher.antMatcher("/actuator/info"), + // allow access to /actuator/health for OpenShift Health Check + AntPathRequestMatcher.antMatcher("/actuator/health"), + // allow access to /actuator/health/liveness for OpenShift Liveness Check + AntPathRequestMatcher.antMatcher("/actuator/health/liveness"), + // allow access to /actuator/health/readiness for OpenShift Readiness Check + AntPathRequestMatcher.antMatcher("/actuator/health/readiness"), + // allow access to /actuator/metrics for Prometheus monitoring in OpenShift + AntPathRequestMatcher.antMatcher("/actuator/metrics"), + AntPathRequestMatcher.antMatcher("/v3/api-docs/**"), + AntPathRequestMatcher.antMatcher("/swagger-ui/**")) + .permitAll()) + .authorizeHttpRequests((requests) -> requests.requestMatchers("/**") + .authenticated()) + .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> httpSecurityOAuth2ResourceServerConfigurer + .jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(new JwtUserInfoAuthenticationConverter( + new UserInfoAuthoritiesService(userInfoUri, restTemplateBuilder))))); + + return http.build(); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfiguration.java new file mode 100644 index 000000000..f2fb7534d --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfiguration.java @@ -0,0 +1,43 @@ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfiguration { + + @Value("${info.application.version:unknown}") + String version; + + @Bean + GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("public-apis") + .pathsToMatch("/**") + .build(); + } + + @Bean + OpenAPI customOpenAPI() { + return new OpenAPI() + .info( + new Info().title("Wahlvorstand Service") + .version(version) + .contact(new Contact().name("Your Name").email("Your E-Mail-Address"))) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components( + new Components() + .addSecuritySchemes("bearerAuth", new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfiguration.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfiguration.java new file mode 100644 index 000000000..12aa36d5b --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter.NfcRequestFilter; +import org.apache.commons.lang3.ArrayUtils; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +/** + *

+ * Beispiel für Konfiguration des NFC Request-Filters + *

+ *
    + *
  • Es werden alle Requests gefiltert, die an URIs unter /* geschickt werden.
  • + *
  • Filter ist in Bean nfcRequestFilter enthalten.
  • + *
  • Es werden nur Requests mit den Content-Types text/plain; application/json + * und text/html gefiltert.
  • + *
+ */ +@Configuration +public class UnicodeConfiguration { + + private static final String NFC_FILTER_NAME = "nfcRequestFilter"; + + private static final String NFC_WHITE_LIST = "text/plain; application/json; application/hal+json; text/html"; + + private static final String[] NFC_URLS = ArrayUtils.toArray("/*"); + + @Bean + public FilterRegistrationBean nfcRequestFilterRegistration(final NfcRequestFilter nfcRequestFilter) { + + final FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(nfcRequestFilter); + registration.setName(NFC_FILTER_NAME); + registration.setOrder(Ordered.LOWEST_PRECEDENCE); + registration.setAsyncSupported(false); + + // + // Setzen der URLs, auf die Filter anzuwenden ist. + // + registration.addUrlPatterns(NFC_URLS); + + // + // Setzen der White-List von ContentTypes für + // + registration.addInitParameter(NfcRequestFilter.CONTENTTYPES_PROPERTY, NFC_WHITE_LIST); + + return registration; + + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesService.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesService.java new file mode 100644 index 000000000..b50197445 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesService.java @@ -0,0 +1,118 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Ticker; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.Cache; +import org.springframework.cache.Cache.ValueWrapper; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.ObjectUtils; +import org.springframework.web.client.RestTemplate; + +/** + * Service, der einen OIDC /userinfo Endpoint aufruft (mit JWT Bearer Auth) und dort die enthaltenen + * "Authorities" extrahiert. + */ +@Slf4j +public class UserInfoAuthoritiesService { + + private static final String NAME_AUTHENTICATION_CACHE = "authentication_cache"; + private static final int AUTHENTICATION_CACHE_ENTRY_SECONDS_TO_EXPIRE = 60; + + private static final String CLAIM_AUTHORITIES = "authorities"; + + private final String userInfoUri; + private final RestTemplate restTemplate; + private final Cache cache; + + /** + * Erzeugt eine neue Instanz. + * + * @param userInfoUri userinfo Endpoint URI + * @param restTemplateBuilder ein {@link RestTemplateBuilder} + */ + public UserInfoAuthoritiesService(String userInfoUri, RestTemplateBuilder restTemplateBuilder) { + this.userInfoUri = userInfoUri; + this.restTemplate = restTemplateBuilder.build(); + this.cache = new CaffeineCache(NAME_AUTHENTICATION_CACHE, + Caffeine.newBuilder() + .expireAfterWrite(AUTHENTICATION_CACHE_ENTRY_SECONDS_TO_EXPIRE, TimeUnit.SECONDS) + .ticker(Ticker.systemTicker()) + .build()); + } + + /** + * Ruft den /userinfo Endpoint und extrahiert {@link GrantedAuthority}s aus dem "authorities" Claim. + * + * @param jwt der JWT + * @return die {@link GrantedAuthority}s gem. Claim "authorities" des /userinfo Endpoints + */ + public Collection loadAuthorities(Jwt jwt) { + ValueWrapper valueWrapper = this.cache.get(jwt.getSubject()); + if (valueWrapper != null) { + // value present in cache + @SuppressWarnings("unchecked") + Collection authorities = (Collection) valueWrapper.get(); + log.debug("Resolved authorities (from cache): {}", authorities); + return authorities; + } + + log.debug("Fetching user-info for token subject: {}", jwt.getSubject()); + final HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue()); + final HttpEntity entity = new HttpEntity<>(headers); + + Collection authorities = new ArrayList<>(); + try { + @SuppressWarnings("unchecked") + Map map = restTemplate.exchange(this.userInfoUri, HttpMethod.GET, entity, + Map.class).getBody(); + + log.debug("Response from user-info Endpoint: {}", map); + if (map.containsKey(CLAIM_AUTHORITIES)) { + authorities = asAuthorities(map.get(CLAIM_AUTHORITIES)); + } + log.debug("Resolved Authorities (from /userinfo Endpoint): {}", authorities); + // store + this.cache.put(jwt.getSubject(), authorities); + } catch (Exception e) { + log.error(String.format("Could not fetch user details from %s - user is granted NO authorities", + this.userInfoUri), e); + } + + return authorities; + } + + private static List asAuthorities(Object object) { + List authorities = new ArrayList<>(); + if (object instanceof Collection collectionWithAuthorities) { + object = collectionWithAuthorities.toArray(new Object[0]); + } + if (ObjectUtils.isArray(object)) { + authorities.addAll( + Stream.of(((Object[]) object)) + .map(Object::toString) + .map(SimpleGrantedAuthority::new) + .toList()); + } + return authorities; + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelper.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelper.java new file mode 100644 index 000000000..98d2f5ad6 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelper.java @@ -0,0 +1,149 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.text.Normalizer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.map.CaseInsensitiveMap; + +/** + * Hilfsklasse für das NFC-Normalisieren + * + * @see Normalizer + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j +public class NfcHelper { + + /** + * Konvertieren eines String in die kanonische Unicode-Normalform (NFC) + * + * @param in Eingabe-String + * @return Normalisierter String. + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static String nfcConverter(final String in) { + if (in == null) { + log.debug("String BEFORE nfc conversion is \"null\"."); + return null; + } + + log.debug("String BEFORE nfc conversion: \"{}\".", in); + log.debug("Length of String BEFORE nfc conversion: {}.", in.length()); + final String nfcConvertedContent = Normalizer.normalize(in, Normalizer.Form.NFC); + log.debug("String AFTER nfc conversion: \"{}\".", nfcConvertedContent); + log.debug("Length of String AFTER nfc conversion: {}.", nfcConvertedContent.length()); + return nfcConvertedContent; + } + + /** + * Konvertieren eines {@link StringBuffer}-Inhalts in die kanonische Unicode-Normalform (NFC) + * + * @param in Eingabe + * @return Normalisierter Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static StringBuffer nfcConverter(final StringBuffer in) { + return new StringBuffer(nfcConverter(in.toString())); + } + + /** + * Konvertieren eines Array von Strings in die kanonische Unicode-Normalform (NFC) + * + * @param original Eingabe-Array + * @return Array mit normalisierten Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static String[] nfcConverter(final String[] original) { + return Arrays.stream(original) + .map(NfcHelper::nfcConverter) + .toArray(String[]::new); + } + + /** + * Konvertieren einer {@link Map} von Strings in die kanonische Unicode-Normalform (NFC). + * + * @param original Eingabe-Map + * @return Map mit normalisierten Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static Map nfcConverter(final Map original) { + final HashMap nfcConverted = new HashMap<>(original.size()); + original.forEach((nfdKey, nfdValueArray) -> nfcConverted.put( + nfcConverter(nfdKey), + nfcConverter(nfdValueArray))); + return nfcConverted; + } + + /** + * Konvertieren eines {@link Cookie}s in die kanonische Unicode-Normalform (NFC). + * + * @param original Cookie + * @return Cookie mit normalisierten Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static Cookie nfcConverter(Cookie original) { + final Cookie nfcCookie = new Cookie(NfcHelper.nfcConverter(original.getName()), NfcHelper.nfcConverter(original.getValue())); + if (original.getDomain() != null) { + nfcCookie.setDomain(NfcHelper.nfcConverter(original.getDomain())); + } + nfcCookie.setPath(NfcHelper.nfcConverter(original.getPath())); + return nfcCookie; + } + + /** + * Konvertieren eines Arrays von {@link Cookie}s in die kanonische Unicode-Normalform (NFC). + * + * @param original Cookies + * @return Cookies mit normalisierten Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static Cookie[] nfcConverter(final Cookie[] original) { + if (original == null) { + return null; + } + return Arrays.stream(original) + .map(NfcHelper::nfcConverter) + .toArray(Cookie[]::new); + } + + /** + * Konvertieren der Header eines {@link HttpServletRequest} von Strings in die kanonische + * Unicode-Normalform (NFC). + * + * @param originalRequest Der {@link HttpServletRequest} zur Extraktion und Konvertierung der + * Header. + * @return Map mit normalisierten Inhalt. + * @see #nfcConverter(String) + * @see Normalizer#normalize(CharSequence, Normalizer.Form) + */ + public static Map> nfcConverterForHeadersFromOriginalRequest(final HttpServletRequest originalRequest) { + final Map> converted = new CaseInsensitiveMap<>(); + Collections.list(originalRequest.getHeaderNames()).forEach(nfdHeaderName -> { + final String nfcHeaderName = NfcHelper.nfcConverter(nfdHeaderName); + final List nfcHeaderEntries = Collections.list(originalRequest.getHeaders(nfdHeaderName)).stream() + .map(NfcHelper::nfcConverter) + .collect(Collectors.toList()); + converted.put(nfcHeaderName, nfcHeaderEntries); + }); + return converted; + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcReader.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcReader.java new file mode 100644 index 000000000..cec541d85 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcReader.java @@ -0,0 +1,106 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import java.io.CharArrayReader; +import java.io.IOException; +import java.io.Reader; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; + +/** + *

+ * Wrapper für Reader der eine NFC-Konvertierung durchführt. + *

+ * + *

+ * Achtung: + *

    + *
  • Bei Java-Readern und -Writern kann gefahrlos eine NFC-Konvertierung + * durchgeführt werden, da dort Zeichen verarbeitet werden.
  • + *
  • Dieser Reader liest bei vor dem Lesen des ersten Zeichens denn vollständig Text des + * gewrappten Readers in einern internen Buffer und führt darauf die NFC-Normalisierung + * durch. Grund ist, dass NFC-Konvertierung kann nicht auf Basis von einzelnen Zeichen + * durchgeführt werden kann. Dies kann zu erhöhter Latenz führen.
  • + *
+ *

+ */ +@Slf4j +public class NfcReader extends Reader { + + private final Reader original; + + private CharArrayReader converted; + + public NfcReader(final Reader original) { + this.original = original; + this.converted = null; + } + + private void convert() { + + if (converted != null) { + return; + } + + log.debug("Converting Reader data to NFC."); + try { + final String nfdContent = IOUtils.toString(original); + final String nfcConvertedContent = NfcHelper.nfcConverter(nfdContent); + converted = new CharArrayReader(nfcConvertedContent.toCharArray()); + + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public int read() throws IOException { + convert(); + return converted.read(); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + convert(); + return converted.read(cbuf, off, len); + } + + @Override + public void close() { + // Nothing to do + } + + @Override + public long skip(long n) throws IOException { + convert(); + return converted.skip(n); + } + + @Override + public boolean ready() throws IOException { + convert(); + return converted.ready(); + } + + @Override + public boolean markSupported() { + convert(); + return converted.markSupported(); + } + + @Override + public void mark(int readAheadLimit) throws IOException { + convert(); + converted.mark(readAheadLimit); + } + + @Override + public void reset() throws IOException { + convert(); + converted.reset(); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequest.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequest.java new file mode 100644 index 000000000..6b07c9441 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequest.java @@ -0,0 +1,212 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.Part; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.IteratorUtils; +import org.apache.commons.io.IOUtils; + +/** + * Wrapper für HttpServletRequest, der NFC-Konvertierung durchführt. + * + * @see java.text.Normalizer + */ +@Slf4j +public class NfcRequest extends HttpServletRequestWrapper implements HttpServletRequest { + + private Map params; + + private Cookie[] cookies; + + private Map> headers; + + @SuppressWarnings("unused") + private Set contentTypes; + + public NfcRequest(final HttpServletRequest request, final Set contentTypes) { + super(request); + this.params = null; + this.cookies = null; + this.headers = null; + this.contentTypes = contentTypes; + } + + private void convert() { + if (params != null) { + return; + } + this.params = NfcHelper.nfcConverter(getRequest().getParameterMap()); + this.cookies = NfcHelper.nfcConverter(getOriginalRequest().getCookies()); + this.headers = NfcHelper.nfcConverterForHeadersFromOriginalRequest(getOriginalRequest()); + } + + @Override + public Cookie[] getCookies() { + convert(); + return this.cookies; + } + + @Override + public String getHeader(final String name) { + convert(); + final List values = headers.get(NfcHelper.nfcConverter(name)); + return (values == null) ? null : values.get(0); + } + + @Override + public Enumeration getHeaders(final String name) { + convert(); + final List values = this.headers.get(NfcHelper.nfcConverter(name)); + return (values == null) ? Collections.emptyEnumeration() : IteratorUtils.asEnumeration(values.iterator()); + } + + @Override + public Enumeration getHeaderNames() { + convert(); + return IteratorUtils.asEnumeration(this.headers.keySet().iterator()); + } + + @Override + public String getPathInfo() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getPathInfo()); + } + + @Override + public String getPathTranslated() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getPathTranslated()); + } + + @Override + public String getContextPath() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getContextPath()); + } + + @Override + public String getQueryString() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getQueryString()); + } + + @Override + public String getRemoteUser() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getRemoteUser()); + } + + @Override + public String getRequestedSessionId() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getRequestedSessionId()); + } + + @Override + public String getRequestURI() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getRequestURI()); + } + + @Override + public StringBuffer getRequestURL() { + convert(); + return NfcHelper.nfcConverter(getOriginalRequest().getRequestURL()); + } + + /** + * {@inheritDoc} + * + * Only the username is converted to nfc. Password won't be touched! + */ + @Override + public void login(String username, String password) throws ServletException { + getOriginalRequest().login(NfcHelper.nfcConverter(username), password); + } + + @Override + public String getParameter(final String name) { + convert(); + final String[] values = this.params.get(NfcHelper.nfcConverter(name)); + return (values == null) ? null : values[0]; + } + + @Override + public Map getParameterMap() { + convert(); + return this.params; + } + + @Override + public Enumeration getParameterNames() { + convert(); + return IteratorUtils.asEnumeration(this.params.keySet().iterator()); + } + + @Override + public String[] getParameterValues(final String name) { + convert(); + return this.params.get(NfcHelper.nfcConverter(name)); + } + + @Override + public BufferedReader getReader() throws IOException { + log.debug("getReader()"); + return new BufferedReader(new NfcReader(getOriginalRequest().getReader())); + } + + @Override + public String getRemoteHost() { + return NfcHelper.nfcConverter(getRequest().getRemoteHost()); + } + + @Override + public Part getPart(final String name) throws IOException, ServletException { + log.debug("getPart({})", name); + return getOriginalRequest().getPart(name); + } + + @Override + public Collection getParts() throws IOException, ServletException { + log.debug("getParts()"); + return getOriginalRequest().getParts(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + final String encoding = getOriginalRequest().getCharacterEncoding(); + + String content = null; + try (final InputStream is = getOriginalRequest().getInputStream()) { + content = new String(IOUtils.toByteArray(is), encoding); + } + + log.debug("Converting InputStream data to NFC."); + final String nfcConvertedContent = NfcHelper.nfcConverter(content); + return new NfcServletInputStream(new ByteArrayInputStream(nfcConvertedContent.getBytes())); + } + + private HttpServletRequest getOriginalRequest() { + return (HttpServletRequest) getRequest(); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequestFilter.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequestFilter.java new file mode 100644 index 000000000..b61cc39b7 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcRequestFilter.java @@ -0,0 +1,104 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + *

+ * Spring-Filter, der eine NFC-Normalisierung aller sicher textuellen Inhalte durchführt. + *

+ * + * Achtung: + *
    + *
  • Alle Datenströme die in Zusammenhang mit Multipart-Requests stehen werden nicht nach NFC + * normalisiert. + * Grund ist, dass hier binäre Datenströme übergeben werden und diese i.d.R. nicht einfacher Text + * sind. + * Falls notwendig bzw. sinnvoll kann bzw. muss die Anwendungslogik oder eine geeignete Bibliothek + * ggf. eine NFC-Normalisierung durchgeführt werden. + *
  • NFC-Normalisierung kann nur auf der Zeichenebene durchgeführt werden und für die + * Konvertierung von + * binären Datenströmen ist die Kenntnis des Datenformats notwendig, was die Kenntnis des + * verwendeten Charsets + * impliziert. Dies lässt die NFC-Normalisierung in einem generischen Filter sinnvoll erscheinen. + *
  • + *
+ * + * @see java.text.Normalizer + * @see HttpServletRequest#getPart(String) + * @see HttpServletRequest#getParts() + */ +@Component +@Slf4j +public class NfcRequestFilter extends OncePerRequestFilter { + + /** + * Name des Properties für Konfiguration der White-List für Content-Types. + * + * @see #getContentTypes() + * @see #setContentTypes(String) + */ + public static final String CONTENTTYPES_PROPERTY = "contentTypes"; + + private final Set contentTypes = new HashSet<>(); + + /** + * @return Das Property contentTypes + */ + public String getContentTypes() { + return String.join("; ", this.contentTypes); + } + + /** + * @param contentTypes Das Property contentTypes + */ + @Autowired(required = false) + public void setContentTypes(final String contentTypes) { + this.contentTypes.clear(); + if (StringUtils.isEmpty(contentTypes)) { + log.info("Disabling context-type filter."); + + } else { + final Set newContentTypes = Arrays.stream(contentTypes.split(";")).map(String::trim) + .collect(Collectors.toSet()); + this.contentTypes.addAll(newContentTypes); + log.info("Enabled content-type filtering to NFC for: {}", getContentTypes()); + + } + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + log.debug("Request-Type={}", request.getClass().getName()); + log.debug("Intercepting request for URI {}", request.getRequestURI()); + + final String contentType = request.getContentType(); + log.debug("ContentType for request with URI: \"{}\"", contentType); + if ((contentTypes != null) && (contentTypes.contains(contentType))) { + log.debug("Processing request {}.", request.getRequestURI()); + filterChain.doFilter(new NfcRequest(request, contentTypes), response); + } else { + log.debug("Skip processing of HTTP request since it's content type \"{}\" is not in whitelist.", contentType); + filterChain.doFilter(request, response); + } + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcServletInputStream.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcServletInputStream.java new file mode 100644 index 000000000..896bbd264 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcServletInputStream.java @@ -0,0 +1,44 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.apache.commons.lang3.NotImplementedException; + +/** + * ServletInputStream, der von einem Puffer ließt. + */ +public class NfcServletInputStream extends ServletInputStream { + + private final ByteArrayInputStream buffer; + + public NfcServletInputStream(final ByteArrayInputStream buffer) { + this.buffer = buffer; + } + + @Override + public int read() throws IOException { + return buffer.read(); + } + + @Override + public boolean isFinished() { + return buffer.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(final ReadListener listener) { + throw new NotImplementedException("Not implemented"); + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/BaseEntity.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/BaseEntity.java new file mode 100644 index 000000000..b336f654a --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/BaseEntity.java @@ -0,0 +1,40 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.domain; + +import static java.sql.Types.VARCHAR; + +import jakarta.persistence.Column; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import java.io.Serializable; +import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.JdbcTypeCode; + +@MappedSuperclass +@NoArgsConstructor +@Getter +@Setter +@ToString +@EqualsAndHashCode +public abstract class BaseEntity implements Cloneable, Serializable { + + private static final long serialVersionUID = 1L; + + @Column(name = "id", length = 36) + @Id + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid2") + @JdbcTypeCode(VARCHAR) + private UUID id; + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/TheEntity.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/TheEntity.java new file mode 100644 index 000000000..af478fefc --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/domain/TheEntity.java @@ -0,0 +1,43 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** + * This class represents a TheEntity. + *

+ * The entity's content will be loaded according to the reference variable. + *

+ */ +@Entity +// Definition of getter, setter, ... +@Getter +@Setter +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class TheEntity extends BaseEntity { + + private static final long serialVersionUID = 1L; + + // ========= // + // Variables // + // ========= // + + @Column(name = "textattribute", nullable = false, length = 8) + @NotNull + @Size(min = 2, max = 8) + private String textAttribute; + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepository.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepository.java new file mode 100644 index 000000000..0fed877f5 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepository.java @@ -0,0 +1,124 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.rest; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.domain.TheEntity; +import java.util.Optional; +import java.util.UUID; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.repository.CrudRepository; +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * Provides a Repository for {@link TheEntity}. This Repository is exported as a REST resource. + *

+ * The Repository handles CRUD Operations. Every Operation is secured and takes care of the tenancy. + * For specific Documentation on how the generated REST point + * behaves, please consider the Spring Data Rest Reference + * here. + *

+ */ +@PreAuthorize("hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_READ_THEENTITY.name())") +public interface TheEntityRepository extends CrudRepository { //NOSONAR + + /** + * Name for the specific cache. + */ + String CACHE = "THEENTITY_CACHE"; + + /** + * Get one specific {@link TheEntity} by its unique id. + * + * @param id The identifier of the {@link TheEntity}. + * @return The {@link TheEntity} with the requested id. + */ + @Override + @Cacheable(value = CACHE, key = "#p0") + Optional findById(UUID id); + + /** + * Create or update a {@link TheEntity}. + *

+ * If the id already exists, the {@link TheEntity} will be overridden, hence update. If the id does + * not already exist, a new {@link TheEntity} will be + * created, hence create. + *

+ * + * @param theEntity The {@link TheEntity} that will be saved. + * @return the saved {@link TheEntity}. + */ + @Override + @CachePut(value = CACHE, key = "#p0.id") + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_WRITE_THEENTITY.name())" + ) + S save(S theEntity); + + /** + * Create or update a collection of {@link TheEntity}. + *

+ * If the id already exists, the {@link TheEntity}s will be overridden, hence update. If the id does + * not already exist, the new {@link TheEntity}s will be + * created, hence create. + *

+ * + * @param entities The {@link TheEntity} that will be saved. + * @return the collection saved {@link TheEntity}. + */ + @Override + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_WRITE_THEENTITY.name())" + ) + Iterable saveAll(Iterable entities); + + /** + * Delete the {@link TheEntity} by a specified id. + * + * @param id the unique id of the {@link TheEntity} that will be deleted. + */ + @Override + @CacheEvict(value = CACHE, key = "#p0") + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_DELETE_THEENTITY.name())" + ) + void deleteById(UUID id); + + /** + * Delete a {@link TheEntity} by entity. + * + * @param entity The {@link TheEntity} that will be deleted. + */ + @Override + @CacheEvict(value = CACHE, key = "#p0.id") + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_DELETE_THEENTITY.name())" + ) + void delete(TheEntity entity); + + /** + * Delete multiple {@link TheEntity} entities by their id. + * + * @param entities The Iterable of {@link TheEntity} that will be deleted. + */ + @Override + @CacheEvict(value = CACHE, allEntries = true) + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_DELETE_THEENTITY.name())" + ) + void deleteAll(Iterable entities); + + /** + * Delete all {@link TheEntity} entities. + */ + @Override + @CacheEvict(value = CACHE, allEntries = true) + @PreAuthorize( + "hasAuthority(T(de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security.AuthoritiesEnum).WLS_WAHLVORSTAND_SERVICE_DELETE_THEENTITY.name())" + ) + void deleteAll(); + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthUtils.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthUtils.java new file mode 100644 index 000000000..e2058956e --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Utilities zu Authentifizierungsdaten. + */ +public class AuthUtils { + + public static final String NAME_UNAUTHENTICATED_USER = "unauthenticated"; + + private static final String TOKEN_USER_NAME = "user_name"; + + private AuthUtils() { + } + + /** + * Extrahiert den Usernamen aus dem vorliegenden Spring Security Context via + * {@link SecurityContextHolder}. + * + * @return der Username or a "unauthenticated", wenn keine {@link Authentication} existiert + */ + public static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof JwtAuthenticationToken jwtAuthenticationToken) { + return (String) jwtAuthenticationToken.getTokenAttributes().getOrDefault(TOKEN_USER_NAME, null); + } else if (authentication instanceof UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) { + return usernamePasswordAuthenticationToken.getName(); + } else { + return NAME_UNAUTHENTICATED_USER; + } + } + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthoritiesEnum.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthoritiesEnum.java new file mode 100644 index 000000000..e3129347d --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/AuthoritiesEnum.java @@ -0,0 +1,19 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * Each possible authority in this project is represented by an enum. The enums are used within the + * {@link PagingAndSortingRepository} in the annotation e.g. + * {@link PreAuthorize}. + */ +public enum AuthoritiesEnum { + WLS_WAHLVORSTAND_SERVICE_READ_THEENTITY, WLS_WAHLVORSTAND_SERVICE_WRITE_THEENTITY, WLS_WAHLVORSTAND_SERVICE_DELETE_THEENTITY, + // add your authorities here and also add these new authorities to sso-authorisation.json. + +} diff --git a/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/RequestResponseLoggingFilter.java b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/RequestResponseLoggingFilter.java new file mode 100644 index 000000000..4a996d9ec --- /dev/null +++ b/wls-wahlvorstand-service/src/main/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/security/RequestResponseLoggingFilter.java @@ -0,0 +1,93 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.security; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.stereotype.Component; + +/** + * This filter logs the username for requests. + */ +@Component +@Order(1) +@Slf4j +public class RequestResponseLoggingFilter implements Filter { + + private static final String REQUEST_LOGGING_MODE_ALL = "all"; + + private static final String REQUEST_LOGGING_MODE_CHANGING = "changing"; + + private static final List CHANGING_METHODS = Arrays.asList("POST", "PUT", "PATCH", "DELETE"); + + /** + * The property or a zero length string if no property is available. + */ + @Value("${security.logging.requests:}") + private String requestLoggingMode; + + /** + * {@inheritDoc} + */ + @Override + public void init(final FilterConfig filterConfig) { + log.debug("Initializing filter: {}", this); + } + + /** + * The method logs the username extracted out of the {@link SecurityContext}, the kind of + * HTTP-Request, the targeted URI and the response http status code. + * + * {@inheritDoc} + */ + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + chain.doFilter(request, response); + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final HttpServletResponse httpResponse = (HttpServletResponse) response; + if (checkForLogging(httpRequest)) { + log.info("User {} executed {} on URI {} with http status {}", + AuthUtils.getUsername(), + httpRequest.getMethod(), + httpRequest.getRequestURI(), + httpResponse.getStatus()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void destroy() { + log.debug("Destructing filter: {}", this); + } + + /** + * The method checks if logging the username should be done. + * + * @param httpServletRequest The request to check for logging. + * @return True if logging should be done otherwise false. + */ + private boolean checkForLogging(HttpServletRequest httpServletRequest) { + return requestLoggingMode.equals(REQUEST_LOGGING_MODE_ALL) + || (requestLoggingMode.equals(REQUEST_LOGGING_MODE_CHANGING) + && CHANGING_METHODS.contains(httpServletRequest.getMethod())); + } + +} diff --git a/wls-wahlvorstand-service/src/main/resources/application-db-h2.yml b/wls-wahlvorstand-service/src/main/resources/application-db-h2.yml new file mode 100644 index 000000000..2b19b5efa --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/application-db-h2.yml @@ -0,0 +1,17 @@ +spring: + h2.console.enabled: true + datasource: + username: sa + password: + url: jdbc:h2:mem:wls-wahlvorstand-service + flyway: + enabled: true + jpa: + database: H2 + hibernate: + ddl-auto: validate + naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + properties: + hibernate: + format_sql: true + show-sql: true \ No newline at end of file diff --git a/wls-wahlvorstand-service/src/main/resources/application-db-oracle.yml b/wls-wahlvorstand-service/src/main/resources/application-db-oracle.yml new file mode 100644 index 000000000..b1945ca66 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/application-db-oracle.yml @@ -0,0 +1,16 @@ +spring: + datasource: + username: wls_wahlvorstand_service + password: secret + url: jdbc:oracle:thin:@//localhost:1521/XEPDB1 + flyway: + enabled: true + jpa: + database: oracle + hibernate: + ddl-auto: validate + naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + properties: + hibernate: + format_sql: true + show-sql: true \ No newline at end of file diff --git a/wls-wahlvorstand-service/src/main/resources/application-local.yml b/wls-wahlvorstand-service/src/main/resources/application-local.yml new file mode 100644 index 000000000..582b3e4ae --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/application-local.yml @@ -0,0 +1,2 @@ +server: + port: 39155 \ No newline at end of file diff --git a/wls-wahlvorstand-service/src/main/resources/application-test.yml b/wls-wahlvorstand-service/src/main/resources/application-test.yml new file mode 100644 index 000000000..562b25f44 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + + # Spring JPA + h2.console.enabled: true + jpa: + database: H2 + hibernate: + # always drop and create the db should be the best + # configuration for local (development) mode. this + # is also the default, that spring offers by convention. + # but here explicite: + ddl-auto: create-drop + naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + # Logging for database operation + show-sql: true + properties: + hibernate: + format_sql: true diff --git a/wls-wahlvorstand-service/src/main/resources/application.yml b/wls-wahlvorstand-service/src/main/resources/application.yml new file mode 100644 index 000000000..7b717c450 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/application.yml @@ -0,0 +1,58 @@ +spring: + application.name: @project.artifactId@ + banner.location: banner.txt + profiles: + group: + local: + - db-h2 + flyway: + locations: + - classpath:db/migrations/{vendor} + h2.console.enabled: false + + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: http://kubernetes.docker.internal:8100/auth/realms/${realm}/protocol/openid-connect/certs + + + +security: + # possible values: none, all, changing (With changing, only changing requests such as POST, PUT, DELETE are logged) + logging.requests: all + oauth2: + resource.user-info-uri: http://kubernetes.docker.internal:8100/auth/realms/${realm}/protocol/openid-connect/userinfo + + +# Define the local keycloak realm here +realm: wls_realm + +server: + shutdown: "graceful" + port: 8080 + error: + include-exception: false + include-stacktrace: never + whitelabel: + enabled: false + +# Config for spring actuator endpoints +management: + server.port: ${server.port} + endpoints: + enabled-by-default: false + web: + exposure: + include: health, info, prometheus, livenessstate, readinessstate + path-mapping: + prometheus: metrics + endpoint: + health.enabled: true + info.enabled: true + prometheus.enabled: true + info: + env: + enabled: true +info.application.name: @project.artifactId@ +info.application.version: @project.version@ diff --git a/wls-wahlvorstand-service/src/main/resources/banner.txt b/wls-wahlvorstand-service/src/main/resources/banner.txt new file mode 100644 index 000000000..2b017dcbe --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/banner.txt @@ -0,0 +1,13 @@ +--------------------------------------------------------------------------------------------------------------------------------------------------- + ____ _ _ _ _ + | _ \ | | | | /\ | | | | + | |_) | __ _ _ __ _ __ __ _ | | __ _ _ __| | __ _ ______ / \ _ __ ___ | |__ ___ | |_ _ _ _ __ ___ + | _ < / _` | | '__| | '__| / _` | | |/ / | | | | / _` | / _` | |______| / /\ \ | '__| / __| | '_ \ / _ \ | __| | | | | | '_ \ / _ \ + | |_) | | (_| | | | | | | (_| | | < | |_| | | (_| | | (_| | / ____ \ | | | (__ | | | | | __/ | |_ | |_| | | |_) | | __/ + |____/ \__,_| |_| |_| \__,_| |_|\_\ \__,_| \__,_| \__,_| /_/ \_\ |_| \___| |_| |_| \___| \__| \__, | | .__/ \___| + __/ | | | + |___/ |_| by CCSE + + Application Name : ${spring.application.name} + Spring Boot Version : ${spring-boot.formatted-version} +--------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/wls-wahlvorstand-service/src/main/resources/db/migrations/h2/V0_1__createTableTheEntity.sql b/wls-wahlvorstand-service/src/main/resources/db/migrations/h2/V0_1__createTableTheEntity.sql new file mode 100644 index 000000000..1f2b83f72 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/db/migrations/h2/V0_1__createTableTheEntity.sql @@ -0,0 +1,5 @@ +CREATE TABLE theEntity +( + id varchar2(36) NOT NULL primary key, + textAttribute varchar2(8) NOT NULL +) \ No newline at end of file diff --git a/wls-wahlvorstand-service/src/main/resources/db/migrations/oracle/V0_1__createTableTheEntity.sql b/wls-wahlvorstand-service/src/main/resources/db/migrations/oracle/V0_1__createTableTheEntity.sql new file mode 100644 index 000000000..1f2b83f72 --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/db/migrations/oracle/V0_1__createTableTheEntity.sql @@ -0,0 +1,5 @@ +CREATE TABLE theEntity +( + id varchar2(36) NOT NULL primary key, + textAttribute varchar2(8) NOT NULL +) \ No newline at end of file diff --git a/wls-wahlvorstand-service/src/main/resources/logback-spring.xml b/wls-wahlvorstand-service/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..296753e7b --- /dev/null +++ b/wls-wahlvorstand-service/src/main/resources/logback-spring.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + %date{yyyy.MM.dd HH:mm:ss.SSS} | ${springAppName} | TraceId: %X{traceId:-} | SpanId: %X{spanId:-}] | %level | [%thread] | %logger{0} | [%file : %line] - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "timestamp" : "%date{yyyy-MM-dd'T'HH:mm:ss.SSS}", + "appName" : "${springAppName}", + "TraceId" : "%mdc{traceId}", + "SpanId" : "%mdc{spanId}", + "X-Span-Export" : "%mdc{X-Span-Export}", + "thread" : "%thread", + "level" : "%level", + "logger": "%logger", + "location" : { + "fileName" : "%file", + "line" : "%line" + }, + "message": "%message" + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/TestConstants.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/TestConstants.java new file mode 100644 index 000000000..9405ca499 --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/TestConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.hateoas.RepresentationModel; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TestConstants { + + public static final String SPRING_TEST_PROFILE = "test"; + + public static final String SPRING_NO_SECURITY_PROFILE = "no-security"; + + @NoArgsConstructor + @Getter + @Setter + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + public static class TheEntityDto extends RepresentationModel { + + private String textAttribute; + + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfigurationTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfigurationTest.java new file mode 100644 index 000000000..07f102d3c --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/CacheControlConfigurationTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_NO_SECURITY_PROFILE; +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_TEST_PROFILE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.MicroServiceApplication; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest( + classes = { MicroServiceApplication.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.datasource.url=jdbc:h2:mem:testexample;DB_CLOSE_ON_EXIT=FALSE", + "refarch.gracefulshutdown.pre-wait-seconds=0" + } +) +@ActiveProfiles(profiles = { SPRING_TEST_PROFILE, SPRING_NO_SECURITY_PROFILE }) +class CacheControlConfigurationTest { + + private static final String ENTITY_ENDPOINT_URL = "/theEntities"; + + private static final String EXPECTED_CACHE_CONTROL_HEADER_VALUES = "no-cache, no-store, must-revalidate"; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Test + @Disabled + void testForCacheControlHeadersForEntityEndpoint() { + ResponseEntity response = testRestTemplate.exchange(ENTITY_ENDPOINT_URL, HttpMethod.GET, null, String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL)); + assertEquals(EXPECTED_CACHE_CONTROL_HEADER_VALUES, response.getHeaders().getCacheControl()); + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfigurationTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfigurationTest.java new file mode 100644 index 000000000..ff19e429e --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SecurityConfigurationTest.java @@ -0,0 +1,67 @@ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_TEST_PROFILE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.MicroServiceApplication; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest(classes = MicroServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@AutoConfigureObservability +@ActiveProfiles(profiles = { SPRING_TEST_PROFILE }) +class SecurityConfigurationTest { + + @Autowired + MockMvc api; + + @Test + void accessSecuredResourceRootThenUnauthorized() throws Exception { + api.perform(get("/")) + .andExpect(status().isUnauthorized()); + } + + @Test + void accessSecuredResourceActuatorThenUnauthorized() throws Exception { + api.perform(get("/actuator")) + .andExpect(status().isUnauthorized()); + } + + @Test + void accessUnsecuredResourceActuatorHealthThenOk() throws Exception { + api.perform(get("/actuator/health")) + .andExpect(status().isOk()); + } + + @Test + void accessUnsecuredResourceActuatorInfoThenOk() throws Exception { + api.perform(get("/actuator/info")) + .andExpect(status().isOk()); + } + + @Test + void accessUnsecuredResourceActuatorMetricsThenOk() throws Exception { + api.perform(get("/actuator/metrics")) + .andExpect(status().isOk()); + } + + @Test + void accessUnsecuredResourceV3ApiDocsThenOk() throws Exception { + api.perform(get("/v3/api-docs")) + .andExpect(status().isOk()); + } + + @Test + void accessUnsecuredResourceSwaggerUiThenOk() throws Exception { + api.perform(get("/swagger-ui/index.html")) + .andExpect(status().isOk()); + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfigurationTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfigurationTest.java new file mode 100644 index 000000000..3b27b898b --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/SwaggerConfigurationTest.java @@ -0,0 +1,59 @@ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_TEST_PROFILE; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.MicroServiceApplication; +import lombok.Data; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@SpringBootTest(classes = MicroServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@AutoConfigureObservability +@ActiveProfiles(profiles = { SPRING_TEST_PROFILE }) +class SwaggerConfigurationTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Value("${info.application.version}") + String version; + + @Test + void versionIsSetInDoc() throws Exception { + val request = MockMvcRequestBuilders.get("/v3/api-docs/public-apis").contentType(MediaType.APPLICATION_JSON); + + val response = mockMvc.perform(request).andReturn(); + + val openApiDoc = objectMapper.readValue(response.getResponse().getContentAsString(), OpenApiDoc.class); + + Assertions.assertThat(openApiDoc.getInfo().getVersion()).isNotNull(); + Assertions.assertThat(openApiDoc.getInfo().getVersion()).isEqualTo(version); + } + + @Data + private static class OpenApiDoc { + + private Info info; + + @Data + private static class Info { + private String version; + } + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfigurationTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfigurationTest.java new file mode 100644 index 000000000..b53d0e121 --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UnicodeConfigurationTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_NO_SECURITY_PROFILE; +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_TEST_PROFILE; +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.TheEntityDto; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.MicroServiceApplication; +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.domain.TheEntity; +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.rest.TheEntityRepository; +import java.net.URI; +import java.util.UUID; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest( + classes = { MicroServiceApplication.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.datasource.url=jdbc:h2:mem:testexample;DB_CLOSE_ON_EXIT=FALSE", + "refarch.gracefulshutdown.pre-wait-seconds=0" + } +) +@ActiveProfiles(profiles = { SPRING_TEST_PROFILE, SPRING_NO_SECURITY_PROFILE }) +class UnicodeConfigurationTest { + + private static final String ENTITY_ENDPOINT_URL = "/theEntities"; + + /** + * Decomposed string: String "Ä-é" represented with unicode letters "A◌̈-e◌́" + */ + private static final String TEXT_ATTRIBUTE_DECOMPOSED = "\u0041\u0308-\u0065\u0301"; + + /** + * Composed string: String "Ä-é" represented with unicode letters "Ä-é". + */ + private static final String TEXT_ATTRIBUTE_COMPOSED = "\u00c4-\u00e9"; + + @Autowired + private TestRestTemplate testRestTemplate; + + @Autowired + private TheEntityRepository theEntityRepository; + + @Test + @Disabled + void testForNfcNormalization() { + // Persist entity with decomposed string. + final TheEntityDto theEntityDto = new TheEntityDto(); + theEntityDto.setTextAttribute(TEXT_ATTRIBUTE_DECOMPOSED); + assertEquals(TEXT_ATTRIBUTE_DECOMPOSED.length(), theEntityDto.getTextAttribute().length()); + final TheEntityDto response = testRestTemplate.postForEntity(URI.create(ENTITY_ENDPOINT_URL), theEntityDto, TheEntityDto.class).getBody(); + + // Check whether response contains a composed string. + assertEquals(TEXT_ATTRIBUTE_COMPOSED, response.getTextAttribute()); + assertEquals(TEXT_ATTRIBUTE_COMPOSED.length(), response.getTextAttribute().length()); + + // Extract uuid from self link. + final UUID uuid = UUID.fromString(StringUtils.substringAfterLast(response.getRequiredLink("self").getHref(), "/")); + + // Check persisted entity contains a composed string via JPA repository. + final TheEntity theEntity = theEntityRepository.findById(uuid).orElse(null); + assertEquals(TEXT_ATTRIBUTE_COMPOSED, theEntity.getTextAttribute()); + assertEquals(TEXT_ATTRIBUTE_COMPOSED.length(), theEntity.getTextAttribute().length()); + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesServiceTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesServiceTest.java new file mode 100644 index 000000000..b1362736f --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/UserInfoAuthoritiesServiceTest.java @@ -0,0 +1,217 @@ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.client.RestTemplate; + +@ExtendWith(MockitoExtension.class) +class UserInfoAuthoritiesServiceTest { + + private static final String RESPONSEBODY_MAP_KEY_CLAIM_AUTHORITIES = "authorities"; + + @Mock + RestTemplate restTemplate; + + @Mock + ResponseEntity responseEntity; + + @Mock + Jwt jwt; + + String userInfoUri = "http://localhost:8080/userinfo"; + + UserInfoAuthoritiesService unitUnderTest; + + @BeforeEach + void setUp() { + val restTemplateBuilder = new RestTemplateBuilder(new RestTemplateCustomizer[0]) { + @Override + public RestTemplate build() { + return restTemplate; + } + }; + + unitUnderTest = new UserInfoAuthoritiesService(userInfoUri, restTemplateBuilder); + } + + @Nested + class LoadAuthorities { + + @Test + void buildAuthoritiesFromTemplateResponseWithCollection() { + val jwtTokenValue = "myTokenValue"; + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + val authority1 = "auth1"; + val authority2 = "auth2"; + val authority3 = "auth3"; + val claimAuthorityValues = List.of(authority1, authority2, authority3); + + val responseEntityBody = new HashMap(); + responseEntityBody.put(RESPONSEBODY_MAP_KEY_CLAIM_AUTHORITIES, claimAuthorityValues); + + Mockito.when(jwt.getSubject()).thenReturn("subject"); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenReturn(responseEntity); + Mockito.when(responseEntity.getBody()).thenReturn(responseEntityBody); + + val expectedAuthorities = List.of(new SimpleGrantedAuthority(authority1), new SimpleGrantedAuthority(authority2), + new SimpleGrantedAuthority(authority3)); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + Assertions.assertThat(authorities).hasSize(claimAuthorityValues.size()); + Assertions.assertThat(authorities).containsAll(expectedAuthorities); + } + + @Test + void buildAuthoritiesFromTemplateResponseWithArray() { + val jwtTokenValue = "myTokenValue"; + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + val authority1 = "auth1"; + val authority2 = "auth2"; + val authority3 = "auth3"; + val claimAuthorityValues = new String[] { authority1, authority2, authority3 }; + + val responseEntityBody = new HashMap(); + responseEntityBody.put(RESPONSEBODY_MAP_KEY_CLAIM_AUTHORITIES, claimAuthorityValues); + + Mockito.when(jwt.getSubject()).thenReturn("subject"); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenReturn(responseEntity); + Mockito.when(responseEntity.getBody()).thenReturn(responseEntityBody); + + val expctedAuthorities = List.of(new SimpleGrantedAuthority(authority1), new SimpleGrantedAuthority(authority2), + new SimpleGrantedAuthority(authority3)); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + Assertions.assertThat(authorities).hasSize(claimAuthorityValues.length); + Assertions.assertThat(authorities).containsAll(expctedAuthorities); + } + + @Test + void buildAuthoritiesFromTemplateResponseWithUnhandledDataStructure() { + val jwtTokenValue = "myTokenValue"; + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + val claimAuthorityValues = "list;of;claims;as;CSV;that;is;not;supported"; + + val responseEntityBody = new HashMap(); + responseEntityBody.put(RESPONSEBODY_MAP_KEY_CLAIM_AUTHORITIES, claimAuthorityValues); + + Mockito.when(jwt.getSubject()).thenReturn("subject"); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenReturn(responseEntity); + Mockito.when(responseEntity.getBody()).thenReturn(responseEntityBody); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + Assertions.assertThat(authorities).isEmpty(); + } + + @Test + void buildAuthoritiesFromTemplateResponseWithoutAuthoritiesClaim() { + val jwtTokenValue = "myTokenValue"; + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + val responseEntityBody = new HashMap(); + + Mockito.when(jwt.getSubject()).thenReturn("subject"); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenReturn(responseEntity); + Mockito.when(responseEntity.getBody()).thenReturn(responseEntityBody); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + Assertions.assertThat(authorities).isEmpty(); + } + + @Test + void errorWhileLoadingViaTemplate() { + val jwtTokenValue = "myTokenValue"; + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + Mockito.when(jwt.getSubject()).thenReturn("subject"); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenThrow(new RuntimeException("sth happend")); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + Assertions.assertThat(authorities).isEmpty(); + } + + @Test + void loadedAuthoritiesAsPlacedInCache() { + val jwtSubject = "subject"; + val jwtTokenValue = "myTokenValue"; + val jwtForCachMethodCall = Mockito.mock(Jwt.class); + + val expectedRequestHeaders = new HttpHeaders(); + expectedRequestHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwtTokenValue); + val expectedRequestEntity = new HttpEntity<>(expectedRequestHeaders); + + val authority1 = "auth1"; + val authority2 = "auth2"; + val authority3 = "auth3"; + val claimAuthorityValues = List.of(authority1, authority2, authority3); + + val responseEntityBody = new HashMap(); + responseEntityBody.put(RESPONSEBODY_MAP_KEY_CLAIM_AUTHORITIES, claimAuthorityValues); + + Mockito.when(jwt.getSubject()).thenReturn(jwtSubject); + Mockito.when(jwtForCachMethodCall.getSubject()).thenReturn(jwtSubject); + Mockito.when(jwt.getTokenValue()).thenReturn(jwtTokenValue); + Mockito.when(restTemplate.exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class)).thenReturn(responseEntity); + Mockito.when(responseEntity.getBody()).thenReturn(responseEntityBody); + + val expectedAuthorities = List.of(new SimpleGrantedAuthority(authority1), new SimpleGrantedAuthority(authority2), + new SimpleGrantedAuthority(authority3)); + + val authorities = unitUnderTest.loadAuthorities(jwt); + + val authoritiesThatShouldComeFromCache = unitUnderTest.loadAuthorities(jwtForCachMethodCall); + + Assertions.assertThat(authorities).hasSize(claimAuthorityValues.size()); + Assertions.assertThat(authorities).containsAll(expectedAuthorities); + Assertions.assertThat(authoritiesThatShouldComeFromCache).isSameAs(authorities); + + Mockito.verify(restTemplate, Mockito.times(1)).exchange(userInfoUri, HttpMethod.GET, expectedRequestEntity, Map.class); + } + + } +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcConverterTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcConverterTest.java new file mode 100644 index 000000000..44377ac94 --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcConverterTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.collections4.list.UnmodifiableList; +import org.apache.commons.collections4.map.UnmodifiableMap; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class NfcConverterTest { + + private static final String NAME_NFD = "aM\u0302ao\u0308a"; + + private static final String VALUE_NFD = "b M\u0302 b o\u0308 b"; + + private static final String VALUE2_NFD = "c M\u0302 c o\u0308 c"; + + private static final String NAME_NFC = Normalizer.normalize(NAME_NFD, Normalizer.Form.NFC); + + private static final String VALUE_NFC = Normalizer.normalize(VALUE_NFD, Normalizer.Form.NFC); + + @SuppressWarnings("unused") + private static final String VALUE2_NFC = Normalizer.normalize(VALUE2_NFD, Normalizer.Form.NFC); + + // Für Stellen der API an denen Strings bestimmten Regeln genügen müssen. + public static final String TOKEN = "token"; + + private static final Charset UTF8 = StandardCharsets.UTF_8; + + @Mock + private HttpServletRequest req; + + @Mock + private HttpServletResponse resp; + + @Mock + private FilterChain chain; + + private final NfcRequestFilter filter = new NfcRequestFilter(); + + // + // Test, das Request mit konfigriertem ContentType auf NFC normalisiert wird. + // + @Test + void testFilterIfContenttypeInWhitelist() throws ServletException, IOException { + mockRequest("text/plain"); + + filter.setContentTypes("text/plain;text/html;application/json"); + + filter.doFilter(req, resp, chain); + + // + // Check + // + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); + Mockito.verify(chain, Mockito.times(1)).doFilter(reqCaptor.capture(), Mockito.any(ServletResponse.class)); + + assertEquals(VALUE_NFC, reqCaptor.getValue().getParameter(NAME_NFC)); + assertEquals(VALUE_NFC, reqCaptor.getValue().getHeader(NAME_NFC)); + assertEquals(VALUE_NFC, reqCaptor.getValue().getCookies()[0].getValue()); + assertEquals(VALUE_NFC, IOUtils.toString(reqCaptor.getValue().getReader())); + + // + // Prüfen, das Multipart-Requests nicht angefasst werden. + // + assertArrayEquals(VALUE_NFD.getBytes(UTF8), IOUtils.toByteArray(reqCaptor.getValue().getPart(NAME_NFD).getInputStream())); + } + + // + // Test, das Request nicht konfigriertem ContentType auf unverändert bleibt, d.h. nicht + // auf NFC normalisiert wird. + // + @Test + void testSkipFilterIfContenttypeNotInWhitelist() throws ServletException, IOException { + mockRequest("application/postscript"); + + filter.setContentTypes("text/plain;text/html"); + + filter.doFilter(req, resp, chain); + + // + // Check + // + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(HttpServletRequest.class); + Mockito.verify(chain, Mockito.times(1)).doFilter(reqCaptor.capture(), Mockito.any(ServletResponse.class)); + + assertEquals(VALUE_NFD, reqCaptor.getValue().getParameter(NAME_NFD)); + assertEquals(VALUE_NFD, reqCaptor.getValue().getHeader(NAME_NFD)); + assertEquals(VALUE_NFD, reqCaptor.getValue().getCookies()[0].getValue()); + assertEquals(VALUE_NFD, IOUtils.toString(reqCaptor.getValue().getReader())); + + // + // Prüfen, das Multipart-Requests nicht angefasst werden. + // + assertArrayEquals(VALUE_NFD.getBytes(UTF8), IOUtils.toByteArray(reqCaptor.getValue().getPart(NAME_NFD).getInputStream())); + } + + private void mockRequest(final String contentType) throws IOException, ServletException { + Mockito.when(req.getContentType()).thenReturn(contentType); + Mockito.when(req.getRequestURI()).thenReturn("/index.html?type=" + contentType); + + Map baseMapParams = new HashMap<>(); + baseMapParams.put(NAME_NFD, new String[] { VALUE_NFD, VALUE2_NFD }); + final Map params = UnmodifiableMap.unmodifiableMap(baseMapParams); + Mockito.when(req.getParameter(NAME_NFD)).thenReturn(params.get(NAME_NFD)[0]); + Mockito.when(req.getParameterMap()).thenReturn(params); + Map baseMapHeaders = new HashMap<>(); + baseMapHeaders.put(NAME_NFD, VALUE_NFD); + final Map headers = UnmodifiableMap.unmodifiableMap(baseMapHeaders); + Mockito.when(req.getHeaderNames()).thenReturn(Collections.enumeration(headers.keySet())); + Mockito.when(req.getHeader(NAME_NFD)).thenReturn(headers.get(NAME_NFD)); + List baseListvalues = new ArrayList<>(); + baseListvalues.add(VALUE_NFD); + final UnmodifiableList values = new UnmodifiableList<>(baseListvalues); + Mockito.when(req.getHeaders(NAME_NFD)).thenReturn(Collections.enumeration(values)); + Mockito.when(req.getCookies()).thenReturn(mockCookies()); + + Mockito.when(req.getReader()).thenReturn(new BufferedReader(new StringReader(VALUE_NFD))); + + final Collection parts = mockParts(); + Mockito.when(req.getPart(NAME_NFD)).thenReturn(parts.iterator().next()); + } + + private Cookie[] mockCookies() { + final Cookie[] cookies = new Cookie[1]; + cookies[0] = new Cookie(TOKEN, VALUE_NFD); + cookies[0].setDomain(VALUE_NFD); + cookies[0].setPath(VALUE_NFD); + return cookies; + } + + private List mockParts() throws IOException { + Part part = Mockito.mock(Part.class); + Mockito.when(part.getInputStream()).thenReturn(new ByteArrayInputStream(VALUE_NFD.getBytes(UTF8))); + List baseListParts = new ArrayList<>(); + baseListParts.add(part); + final UnmodifiableList parts = new UnmodifiableList<>(baseListParts); + return parts; + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelperTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelperTest.java new file mode 100644 index 000000000..c47f2d504 --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/configuration/nfcconverter/NfcHelperTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.configuration.nfcconverter; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.servlet.http.Cookie; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class NfcHelperTest { + + private static final String FIRST_NFD = "\u017f\u0307"; + + private static final String FIRST_NFC = "\u1e9b"; + + private static final String SECOND_NFD = "\u006f\u0302"; + + private static final String SECOND_NFC = "\u00f4"; + + private static final String THIRD_NFD = "\u0073\u0323\u0307"; + + private static final String THIRD_NFC = "\u1e69"; + + private static final String[] NFD_INPUT = new String[] { FIRST_NFD, SECOND_NFD, THIRD_NFD }; + + private static final String[] NFC_OUTPUT_EXPECTED = new String[] { FIRST_NFC, SECOND_NFC, THIRD_NFC }; + + @Test + void nfcConverterString() { + assertEquals(FIRST_NFC, NfcHelper.nfcConverter(FIRST_NFD)); + assertEquals(FIRST_NFC.length(), NfcHelper.nfcConverter(FIRST_NFD).length()); + + assertEquals(SECOND_NFC, NfcHelper.nfcConverter(SECOND_NFD)); + assertEquals(SECOND_NFC.length(), NfcHelper.nfcConverter(SECOND_NFD).length()); + + assertEquals(THIRD_NFC, NfcHelper.nfcConverter(THIRD_NFD)); + assertEquals(THIRD_NFC.length(), NfcHelper.nfcConverter(THIRD_NFD).length()); + + assertNull(NfcHelper.nfcConverter((String) null)); + } + + @Test + void nfcConverterStringBuffer() { + assertEquals(FIRST_NFC, NfcHelper.nfcConverter(new StringBuffer(FIRST_NFD)).toString()); + assertEquals(FIRST_NFC.length(), NfcHelper.nfcConverter(new StringBuffer(FIRST_NFD)).length()); + + assertEquals(SECOND_NFC, NfcHelper.nfcConverter(new StringBuffer(SECOND_NFD)).toString()); + assertEquals(SECOND_NFC.length(), NfcHelper.nfcConverter(new StringBuffer(SECOND_NFD)).length()); + + assertEquals(THIRD_NFC, NfcHelper.nfcConverter(new StringBuffer(THIRD_NFD)).toString()); + assertEquals(THIRD_NFC.length(), NfcHelper.nfcConverter(new StringBuffer(THIRD_NFD)).length()); + } + + @Test + void nfcConverterStringArray() { + assertArrayEquals(NFC_OUTPUT_EXPECTED, NfcHelper.nfcConverter(NFD_INPUT)); + assertEquals(NFC_OUTPUT_EXPECTED.length, NfcHelper.nfcConverter(NFD_INPUT).length); + } + + @Test + void nfcConverterMapOfStrings() { + final Map nfdInput = new HashMap<>(); + nfdInput.put(FIRST_NFD, NFD_INPUT); + nfdInput.put(SECOND_NFD, NFD_INPUT); + nfdInput.put(THIRD_NFD, NFD_INPUT); + + final Map result = NfcHelper.nfcConverter(nfdInput); + assertEquals(3, result.size()); + assertArrayEquals(NFC_OUTPUT_EXPECTED, result.get(FIRST_NFC)); + assertArrayEquals(NFC_OUTPUT_EXPECTED, result.get(SECOND_NFC)); + assertArrayEquals(NFC_OUTPUT_EXPECTED, result.get(THIRD_NFC)); + } + + @Test + void nfcConverterCookie() { + final Cookie nfcCookie = NfcHelper.nfcConverter(createNfdCookie()); + + assertEquals(NfcConverterTest.TOKEN, nfcCookie.getName()); + assertEquals(Arrays.toString(NFC_OUTPUT_EXPECTED), nfcCookie.getValue()); + assertEquals(THIRD_NFC, nfcCookie.getDomain()); + assertEquals(THIRD_NFC, nfcCookie.getPath()); + } + + @Test + void nfcConverterCookieWithoutDomain() { + final Cookie cookieToConvert = createNfdCookie(); + cookieToConvert.setDomain(null); + final Cookie nfcCookie = NfcHelper.nfcConverter(cookieToConvert); + + assertEquals(NfcConverterTest.TOKEN, nfcCookie.getName()); + assertEquals(Arrays.toString(NFC_OUTPUT_EXPECTED), nfcCookie.getValue()); + assertNull(nfcCookie.getDomain()); + assertEquals(THIRD_NFC, nfcCookie.getPath()); + } + + @Test + void nfcConverterCookieArray() { + final Cookie[] nfdCookies = Collections.nCopies(3, createNfdCookie()).toArray(new Cookie[3]); + final Cookie[] nfcCookies = NfcHelper.nfcConverter(nfdCookies); + Arrays.asList(nfcCookies).forEach(nfcCookie -> { + assertEquals(NfcConverterTest.TOKEN, nfcCookie.getName()); + assertEquals(Arrays.toString(NFC_OUTPUT_EXPECTED), nfcCookie.getValue()); + assertEquals(THIRD_NFC, nfcCookie.getDomain()); + assertEquals(THIRD_NFC, nfcCookie.getPath()); + }); + } + + @Test + void nfcConverterCookieArrayNullReturnNull() { + assertNull(NfcHelper.nfcConverter((Cookie[]) null)); + } + + private static Cookie createNfdCookie() { + final Cookie nfdCookie = new Cookie(NfcConverterTest.TOKEN, Arrays.toString(NFD_INPUT)); + nfdCookie.setDomain(THIRD_NFD); + nfdCookie.setPath(THIRD_NFD); + return nfdCookie; + } + +} diff --git a/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepositoryTest.java b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepositoryTest.java new file mode 100644 index 000000000..5471d2b6b --- /dev/null +++ b/wls-wahlvorstand-service/src/test/java/de/muenchen/oss/wahllokalsystem/wahlvorstandservice/rest/TheEntityRepositoryTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik + * der Landeshauptstadt München, 2024 + */ +package de.muenchen.oss.wahllokalsystem.wahlvorstandservice.rest; + +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_NO_SECURITY_PROFILE; +import static de.muenchen.oss.wahllokalsystem.wahlvorstandservice.TestConstants.SPRING_TEST_PROFILE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.MicroServiceApplication; +import de.muenchen.oss.wahllokalsystem.wahlvorstandservice.domain.TheEntity; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest( + classes = { MicroServiceApplication.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.datasource.url=jdbc:h2:mem:wls;DB_CLOSE_ON_EXIT=FALSE", + "refarch.gracefulshutdown.pre-wait-seconds=0" + } +) +@ActiveProfiles(profiles = { SPRING_TEST_PROFILE, SPRING_NO_SECURITY_PROFILE }) +class TheEntityRepositoryTest { + + @Autowired + private TheEntityRepository repository; + + @Test + @Transactional(propagation = Propagation.REQUIRED, noRollbackFor = Exception.class) + void testSave() { + + // Implement your logic here by replacing and/or extending the code + + // initialize + TheEntity original = new TheEntity(); + original.setTextAttribute("test"); + + // persist + original = repository.save(original); + + // check + TheEntity persisted = repository.findById(original.getId()).orElse(null); + assertNotNull(persisted); + assertEquals(original, persisted); + + } + +}