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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/ForwardedHeaderConfiguration.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/ForwardedHeaderConfiguration.java
new file mode 100644
index 000000000..3a6851345
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/JwtUserInfoAuthenticationConverter.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/JwtUserInfoAuthenticationConverter.java
new file mode 100644
index 000000000..24ccf97bb
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/NoSecurityConfiguration.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/NoSecurityConfiguration.java
new file mode 100644
index 000000000..1a2cb07e0
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SecurityConfiguration.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SecurityConfiguration.java
new file mode 100644
index 000000000..154420841
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfiguration.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfiguration.java
new file mode 100644
index 000000000..291bff004
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfiguration.java
@@ -0,0 +1,43 @@
+package de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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("Auth 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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UnicodeConfiguration.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UnicodeConfiguration.java
new file mode 100644
index 000000000..af653aae0
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.configuration;
+
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UserInfoAuthoritiesService.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UserInfoAuthoritiesService.java
new file mode 100644
index 000000000..88a792994
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcHelper.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcHelper.java
new file mode 100644
index 000000000..d5432db44
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcReader.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcReader.java
new file mode 100644
index 000000000..0ff2b90c3
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcRequest.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcRequest.java
new file mode 100644
index 000000000..05c91544a
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcRequestFilter.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcRequestFilter.java
new file mode 100644
index 000000000..ae7d26071
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcServletInputStream.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/nfcconverter/NfcServletInputStream.java
new file mode 100644
index 000000000..000dd7e84
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/domain/BaseEntity.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/domain/BaseEntity.java
new file mode 100644
index 000000000..511b71c3e
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/domain/TheEntity.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/domain/TheEntity.java
new file mode 100644
index 000000000..c6cae2d2d
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/rest/TheEntityRepository.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/rest/TheEntityRepository.java
new file mode 100644
index 000000000..911466c0a
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/rest/TheEntityRepository.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik
+ * der Landeshauptstadt München, 2024
+ */
+package de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.rest;
+
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_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.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_SERVICE_DELETE_THEENTITY.name())"
+ )
+ void deleteAll(Iterable extends TheEntity> entities);
+
+ /**
+ * Delete all {@link TheEntity} entities.
+ */
+ @Override
+ @CacheEvict(value = CACHE, allEntries = true)
+ @PreAuthorize(
+ "hasAuthority(T(de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.security.AuthoritiesEnum).WLS_AUTH_SERVICE_DELETE_THEENTITY.name())"
+ )
+ void deleteAll();
+
+}
diff --git a/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/AuthUtils.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/AuthUtils.java
new file mode 100644
index 000000000..7984e307c
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/AuthoritiesEnum.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/AuthoritiesEnum.java
new file mode 100644
index 000000000..c678b6d85
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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_AUTH_SERVICE_READ_THEENTITY, WLS_AUTH_SERVICE_WRITE_THEENTITY, WLS_AUTH_SERVICE_DELETE_THEENTITY,
+ // add your authorities here and also add these new authorities to sso-authorisation.json.
+
+}
diff --git a/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/RequestResponseLoggingFilter.java b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/security/RequestResponseLoggingFilter.java
new file mode 100644
index 000000000..eacf26d3e
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/main/resources/application-db-h2.yml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-db-h2.yml
new file mode 100644
index 000000000..f5a573d86
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/application-db-oracle.yml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-db-oracle.yml
new file mode 100644
index 000000000..b90be93fa
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-db-oracle.yml
@@ -0,0 +1,16 @@
+spring:
+ datasource:
+ username: wls_vorfaelleundvorkommnisse_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-vorfaelleundvorkommnisse-service/src/main/resources/application-local.yml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-local.yml
new file mode 100644
index 000000000..c95fdbe4e
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-local.yml
@@ -0,0 +1,2 @@
+server:
+ port: 39156
\ No newline at end of file
diff --git a/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-test.yml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application-test.yml
new file mode 100644
index 000000000..562b25f44
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/application.yml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/application.yml
new file mode 100644
index 000000000..7b717c450
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/banner.txt b/wls-vorfaelleundvorkommnisse-service/src/main/resources/banner.txt
new file mode 100644
index 000000000..2b017dcbe
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/db/migrations/h2/V0_1__createTableTheEntity.sql b/wls-vorfaelleundvorkommnisse-service/src/main/resources/db/migrations/h2/V0_1__createTableTheEntity.sql
new file mode 100644
index 000000000..1f2b83f72
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/db/migrations/oracle/V0_1__createTableTheEntity.sql b/wls-vorfaelleundvorkommnisse-service/src/main/resources/db/migrations/oracle/V0_1__createTableTheEntity.sql
new file mode 100644
index 000000000..1f2b83f72
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/main/resources/logback-spring.xml b/wls-vorfaelleundvorkommnisse-service/src/main/resources/logback-spring.xml
new file mode 100644
index 000000000..06fce8497
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/TestConstants.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/TestConstants.java
new file mode 100644
index 000000000..026e030db
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice;
+
+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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/CacheControlConfigurationTest.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/CacheControlConfigurationTest.java
new file mode 100644
index 000000000..e518925b7
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.configuration;
+
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.TestConstants.SPRING_NO_SECURITY_PROFILE;
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SecurityConfigurationTest.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SecurityConfigurationTest.java
new file mode 100644
index 000000000..3d81fef79
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SecurityConfigurationTest.java
@@ -0,0 +1,67 @@
+package de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.configuration;
+
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfigurationTest.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfigurationTest.java
new file mode 100644
index 000000000..0365665ac
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/SwaggerConfigurationTest.java
@@ -0,0 +1,59 @@
+package de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.configuration;
+
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.TestConstants.SPRING_TEST_PROFILE;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UnicodeConfigurationTest.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UnicodeConfigurationTest.java
new file mode 100644
index 000000000..beebd26eb
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/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.vorfaelleundvorkommnisseservice.configuration;
+
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.TestConstants.SPRING_NO_SECURITY_PROFILE;
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.TestConstants.SPRING_TEST_PROFILE;
+import static de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.TestConstants.TheEntityDto;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.MicroServiceApplication;
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.domain.TheEntity;
+import de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UserInfoAuthoritiesServiceTest.java b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UserInfoAuthoritiesServiceTest.java
new file mode 100644
index 000000000..336f230ca
--- /dev/null
+++ b/wls-vorfaelleundvorkommnisse-service/src/test/java/de/muenchen/oss/wahllokalsystem/vorfaelleundvorkommnisseservice/configuration/UserInfoAuthoritiesServiceTest.java
@@ -0,0 +1,217 @@
+package de.muenchen.oss.wahllokalsystem.vorfaelleundvorkommnisseservice.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