diff --git a/fachstelle-server/pom.xml b/fachstelle-server/pom.xml index e780a1f751b3ab224868fcc2fcb47e0d6913f5ed..17859ec73f2cdbda1c2515a4b273c2d938345b27 100644 --- a/fachstelle-server/pom.xml +++ b/fachstelle-server/pom.xml @@ -25,8 +25,8 @@ --> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns="http://maven.apache.org/POM/4.0.0" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -62,6 +62,43 @@ <openapi.version>2.3.0</openapi.version> </properties> + <repositories> + <repository> + <id>central</id> + <name>Maven Central</name> + <url>https://repo1.maven.org/maven2/</url> + </repository> + <repository> + <id>nexus</id> + <name>Ozg nexus</name> + <url>https://nexus.ozg-sh.de/repository/ozg-releases</url> + </repository> + <repository> + <id>shibboleth-releases</id> + <name>Shibboleth Releases Repository</name> + <url>https://build.shibboleth.net/maven/releases/</url> + <releases> + <enabled>true</enabled> + <checksumPolicy>warn</checksumPolicy> + </releases> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <id>shibboleth-thirdparty</id> + <name>Shibboleth Thirdparty Repository</name> + <url>https://build.shibboleth.net/maven/thirdparty/</url> + <releases> + <enabled>true</enabled> + <checksumPolicy>fail</checksumPolicy> + </releases> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + </repositories> + <dependencies> <!-- Spring --> <dependency> @@ -113,6 +150,14 @@ <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-saml2-service-provider</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> + </dependency> <dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> diff --git a/fachstelle-server/src/main/helm/templates/ingress.yaml b/fachstelle-server/src/main/helm/templates/ingress.yaml index 2393a91265ca5c749cb24ee629e38674764d45aa..4c3a6ebabaf68b8f2d82c7a044c53f241172786b 100644 --- a/fachstelle-server/src/main/helm/templates/ingress.yaml +++ b/fachstelle-server/src/main/helm/templates/ingress.yaml @@ -51,6 +51,62 @@ spec: name: fachstelle-server port: number: 8080 + - path: /registrierung + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /saml2 + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /login + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /preregister + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /register + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /success + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /static + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - path: /webjars + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 host: {{ include "app.baseDomain" . }} tls: - hosts: diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SecurityConfiguration.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SecurityConfiguration.java index 800ed89cbf81a82c6cf49d5b0d9168cb5bcc5af9..d036ab0fbf6a1e1b4b429c40397edccc7e22051f 100644 --- a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SecurityConfiguration.java +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SecurityConfiguration.java @@ -30,13 +30,23 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.config.Customizer; 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.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; +import de.ozgcloud.fachstelle.security.FachstelleLogoutSuccessHandler; +import de.ozgcloud.fachstelle.security.InMemoryUserDetailService; +import de.ozgcloud.fachstelle.security.SecurityProvider; +import de.ozgcloud.fachstelle.security.UrlAuthenticationSuccessHandler; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -45,26 +55,60 @@ import lombok.extern.log4j.Log4j2; @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfiguration { + + private final InMemoryUserDetailService userDetailsService; + private final UrlAuthenticationSuccessHandler urlAuthenticationSuccessHandler; private final FachstellenProperties properties; + private final SpringJwtProperties springJwtProperties; + + @Bean + public SecurityProvider securityProvider() { + return new SecurityProvider(); + } @Bean public AuthenticationTrustResolver authenticationTrustResolver() { return new AuthenticationTrustResolverImpl(); } + @Bean + public Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationRepository registrations) { + var registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations); + var authenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver(registrationResolver); + authenticationRequestResolver.setAuthnRequestCustomizer(context -> context.getAuthnRequest().setForceAuthn(true)); + + return authenticationRequestResolver; + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/*", "/index**", "/success", "/actuator/**", "/error", "/favicon.ico", "/fonts/**", "/webjars/**", "/api/**", "/api", - "/api/environment") - .permitAll() - .requestMatchers("/preregister", "/register").authenticated() - .anyRequest().denyAll()); + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/registrierung", "/static/**", "/webjars/**", "/actuator/**", "/error", "/fonts/**", "/webjars/**", + "/api/environment") + .permitAll() + .requestMatchers("/api/**", "/preregister", "/register", "/success").authenticated() + .anyRequest().denyAll()); + + http.oauth2ResourceServer(this::setOAuth2ResourceServer); + http.saml2Login(samlLogin -> samlLogin.successHandler(urlAuthenticationSuccessHandler)) + .saml2Logout(Customizer.withDefaults()) + .logout(logoutConfigurer -> logoutConfigurer.logoutSuccessHandler(getLogoutSuccessHandler())); return http.build(); } + private void setOAuth2ResourceServer(OAuth2ResourceServerConfigurer<HttpSecurity> configurer) { + configurer.jwt().jwkSetUri(springJwtProperties.getJwkSetUri()); + } + + FachstelleLogoutSuccessHandler getLogoutSuccessHandler() { + var handler = new FachstelleLogoutSuccessHandler(userDetailsService); + handler.setDefaultTargetUrl(properties.getLogoutSuccessUrl()); + + return handler; + } + @Bean public CorsFilter corsFilter() { var source = new UrlBasedCorsConfigurationSource(); @@ -73,11 +117,10 @@ public class SecurityConfiguration { config.setAllowedOrigins(List.of(properties.getCors())); config.setAllowedMethods(List.of("POST", "OPTIONS", "GET", "HEAD")); config.setAllowedHeaders( - List.of(HttpHeaders.ORIGIN, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT, HttpHeaders.AUTHORIZATION, "X-Requested-With", "X-Client")); + List.of(HttpHeaders.ORIGIN, HttpHeaders.CONTENT_TYPE, HttpHeaders.ACCEPT, HttpHeaders.AUTHORIZATION, "X-Requested-With", "X-Client")); config.setExposedHeaders(List.of(HttpHeaders.LOCATION, HttpHeaders.CONTENT_DISPOSITION)); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } - } diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SpringJwtProperties.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SpringJwtProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..3740ffaa0362b23155c69f17d22bb25c9535dc19 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/SpringJwtProperties.java @@ -0,0 +1,21 @@ +package de.ozgcloud.fachstelle; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = SpringJwtProperties.PREFIX) +public class SpringJwtProperties { + + static final String PREFIX = "spring.security.oauth2.resourceserver.jwt"; + + /** + * Jwt jwk set uri + */ + private String jwkSetUri = null; +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationController.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationController.java new file mode 100644 index 0000000000000000000000000000000000000000..5c70ac93dce4f527797e575923616517fe8da76b --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationController.java @@ -0,0 +1,110 @@ +package de.ozgcloud.fachstelle.registration; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.NoSuchElementException; +import java.util.UUID; + +import jakarta.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import de.ozgcloud.fachstelle.FachstellenProperties; +import de.ozgcloud.fachstelle.security.InMemoryUserDetailService; +import de.ozgcloud.fachstelle.security.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Controller +@RequiredArgsConstructor +public class FachstelleRegistrationController { + + public static final String OK = "ok"; + private final InMemoryUserDetailService userDetailsService; + private final FachstellenProperties properties; + private final FachstelleRegistrationService registrationService; + + @GetMapping(value = "/registrierung", produces = "text/html; charset=utf-8") + public String startRegistration(final Model model) { + model.addAttribute("loginUrl", properties.getLoginRedirectUrl()); + return "index"; + } + + @GetMapping(value = "/preregister", produces = "text/html; charset=utf-8") + public String preregister(final Model model, Authentication auth) { + var user = getUser(auth); + addRegistrationKey(user); + + model.addAttribute("organizationName", user.getCompanyName()); + model.addAttribute("organizationLegalForm", user.getLegalFormText()); + model.addAttribute("organizationRegisterType", user.getRegisterType()); + model.addAttribute("organizationRegisterNumber", user.getRegisterNumber()); + model.addAttribute("organizationEmail", user.getEmailAddress()); + model.addAttribute("organizationAddress", user.getAddress()); + + var registration = new Registration(user.getRegistrationKey()); + model.addAttribute("registration", registration); + + return "preregister"; + } + + void addRegistrationKey(final User user) { + user.setRegistrationKey(UUID.randomUUID().toString()); + user.setRegistrationKeyExpiresAt(Instant.now().plus(10, ChronoUnit.MINUTES)); + } + + @GetMapping(value = "/success", produces = "text/html; charset=utf-8") + public String success(final Model model, Authentication auth, HttpServletRequest request) { + var user = getUser(auth); + model.addAttribute("name", user); + userDetailsService.logout(user); + request.getSession().invalidate(); + + return "success"; + } + + @PostMapping("/register") + public String register(@ModelAttribute Registration registration, Model model, final Authentication auth) { + var user = getUser(auth); + + var res = canRegister(user, registration.key()); + if (OK.equals(res) && registrationService.register(user)) { + return "success"; + } else { + model.addAttribute("errorMessageKey", res); + return "error"; + } + } + + private User getUser(Authentication auth) { + try { + return userDetailsService.loadUserByUsername(auth.getName()); + } catch (NoSuchElementException | NullPointerException e) { + LOG.error("Error loading user.", e); + throw new SecurityException(e); + } + } + + String canRegister(User user, String registrationKey) { + boolean regKeyMatch = StringUtils.isNotEmpty(user.getRegistrationKey()) && user.getRegistrationKey().equals(registrationKey); + if (!regKeyMatch) { + LOG.error("Registration key invalid"); + return "error_registration_key_does_not_exist"; + } + boolean regKeyNotExpired = Instant.now().isBefore(user.getRegistrationKeyExpiresAt()); + if (!regKeyNotExpired) { + LOG.error("Registration key expired"); + return "error_registration_key_expired"; + } + + return OK; + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteService.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..1442335e3f16b49bee17f304bf1d9d5b354f786d --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteService.java @@ -0,0 +1,47 @@ +package de.ozgcloud.fachstelle.registration; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; + +import de.ozgcloud.fachstelle.security.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class FachstelleRegistrationRemoteService { + + static final String REGISTER_FACHSTELLE_URI = "api/fachstellen"; + + private final RestClient restClient; + private final FachstelleRegistrationRequestMapper requestMapper; + + boolean register(User user) { + var request = requestMapper.toFachstelleRegistrationRequest(user); + + try { + var response = restClient.post() + .uri(REGISTER_FACHSTELLE_URI) + .contentType(MediaType.APPLICATION_JSON) + .body(request) + .retrieve() + .toBodilessEntity(); + + if (response.getStatusCode().is2xxSuccessful()) { + LOG.info("Successfully registered Fachstelle {} with id {}", request.getFirmenName(), request.getMukId()); + return true; + } + + LOG.error("Failed to register Fachstelle {} with id {}. Status code: {}", request.getFirmenName(), request.getMukId(), + response.getStatusCode()); + } catch (RestClientException e) { + LOG.error("Error sending registration. {}", e.getMessage(), e); + } + + return false; + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapper.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b62bd52a279ac869ee92b635c05db1bfc2ea9b1a --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapper.java @@ -0,0 +1,20 @@ +package de.ozgcloud.fachstelle.registration; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import de.ozgcloud.fachstelle.proxy.FachstellenproxyGrpcFachstelleRegistrationRequest; +import de.ozgcloud.fachstelle.security.User; + +@Mapper +interface FachstelleRegistrationRequestMapper { + @Mapping(target = "mukId", source = "id") + @Mapping(target = "firmenName", source = "companyName") + @Mapping(target = "rechtsform", source = "legalForm") + @Mapping(target = "rechtsformText", source = "legalFormText") + @Mapping(target = "registerNummer", source = "registerNumber") + @Mapping(target = "registerArt", source = "registerType") + @Mapping(target = "emailAdresse", source = "emailAddress") + @Mapping(target = "anschrift", source = "address") + FachstellenproxyGrpcFachstelleRegistrationRequest toFachstelleRegistrationRequest(User user); +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationService.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationService.java new file mode 100644 index 0000000000000000000000000000000000000000..1e055c06ebedda790acc7317c03df15b8d360822 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationService.java @@ -0,0 +1,15 @@ +package de.ozgcloud.fachstelle.registration; + +import de.ozgcloud.fachstelle.security.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FachstelleRegistrationService { + private final FachstelleRegistrationRemoteService remoteService; + + public boolean register(User user) { + return remoteService.register(user); + } +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/Registration.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/Registration.java new file mode 100644 index 0000000000000000000000000000000000000000..01731fdc5616efd2895a42973e5056a4ff9f2833 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/registration/Registration.java @@ -0,0 +1,5 @@ +package de.ozgcloud.fachstelle.registration; + +record Registration(String key) { + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/CurrentUserService.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/CurrentUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..791fac9aff9c086234c941ec125ccddebba84519 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/CurrentUserService.java @@ -0,0 +1,40 @@ +package de.ozgcloud.fachstelle.security; + +import java.util.Optional; + +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.stereotype.Service; + +import de.ozgcloud.fachstelle.common.errorhandling.SamlTokenNotFoundException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CurrentUserService { + + private final AuthenticationTrustResolver trustResolver; + + public Authentication getAuthentication() { + return findAuthentication().orElseThrow(() -> new IllegalStateException("No authenticated User found")); + } + + private Optional<Authentication> findAuthentication() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).filter(this::isTrusted); + } + + private boolean isTrusted(Authentication authentication) { + return !trustResolver.isAnonymous(authentication); + } + + public String getSamlToken() { + if (getAuthentication() instanceof Saml2Authentication samlAuth) { + return samlAuth.getSaml2Response(); + } + + throw new SamlTokenNotFoundException(); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/DefaultRole.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/DefaultRole.java new file mode 100644 index 0000000000000000000000000000000000000000..b2b9991e516fb5b6ae333c5cb8467046cee2d148 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/DefaultRole.java @@ -0,0 +1,14 @@ +package de.ozgcloud.fachstelle.security; + +import org.springframework.security.core.GrantedAuthority; + +public class DefaultRole implements GrantedAuthority { + + public static final String ROLE = "ROLE_USER"; + + @Override + public String getAuthority() { + return ROLE; + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandler.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..52486d790bc5f31058796415b092246bf755c5bd --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandler.java @@ -0,0 +1,31 @@ +package de.ozgcloud.fachstelle.security; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class FachstelleLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler { + + private final UserDetailsService userDetailService; + + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws IOException, ServletException { + if (Objects.nonNull(authentication) && authentication.getPrincipal() instanceof User user) { + ((InMemoryUserDetailService) userDetailService).logout(user); + } + + super.handle(request, response, authentication); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailService.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailService.java new file mode 100644 index 0000000000000000000000000000000000000000..3cc7b980b2fd5af58108d9d5c43d90071e52feb9 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailService.java @@ -0,0 +1,59 @@ +package de.ozgcloud.fachstelle.security; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import lombok.extern.log4j.Log4j2; + +@Service +@Log4j2 +public class InMemoryUserDetailService implements UserDetailsService { + + private final ConcurrentHashMap<String, User> usersMap = new ConcurrentHashMap<>(); + + public InMemoryUserDetailService() { + LOG.debug("Init InMemoryUserDetailService"); + } + + public void addUser(final User user) { + LOG.debug("Adding user: {}", user); + user.setExpirationDate(Instant.now().plus(4, ChronoUnit.HOURS)); + + usersMap.put(user.getId(), user); + + LOG.debug("users map: {}", usersMap); + } + + void setUser(final User user) { + usersMap.put(user.getId(), user); + } + + User getUser(String id) { + return usersMap.get(id); + } + + @Override + public User loadUserByUsername(String username) { + var optionalEntry = usersMap.entrySet().stream().filter(entry -> username.equals(entry.getValue().getUsername())).findFirst(); + + return optionalEntry.map(Map.Entry::getValue).orElseThrow(); + } + + @Scheduled(fixedRate = 4, timeUnit = TimeUnit.HOURS) + void userCleanUp() { + var expiredEntries = usersMap.entrySet().stream().filter(entry -> !entry.getValue().isCredentialsNonExpired()).toList(); + expiredEntries.forEach(expiredEntry -> logout(expiredEntry.getValue())); + } + + public void logout(User user) { + usersMap.remove(user.getId()); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithm.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithm.java new file mode 100644 index 0000000000000000000000000000000000000000..292481eaccfd213e0c6f1cd2a34d68f95f93a832 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithm.java @@ -0,0 +1,41 @@ +package de.ozgcloud.fachstelle.security; + +import org.opensaml.xmlsec.algorithm.SignatureAlgorithm; + +import lombok.NoArgsConstructor; +import lombok.NonNull; + +@NoArgsConstructor +public final class SHA256withRSAAndMGF1SignatureAlgorithm implements SignatureAlgorithm { + + static final String RSA_ALGORITHM_ID = "RSA"; + static final String RSA_SHA256_MGF1_ALGORITHM_URL = "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1"; + static final String RSA_SHA256_MGF1_ALGORITHM_ID = "SHA256withRSAandMGF1"; + static final String SHA256_ALGORITHM_ID = "SHA-256"; + + @NonNull + public String getKey() { + return RSA_ALGORITHM_ID; + } + + @NonNull + public String getURI() { + return RSA_SHA256_MGF1_ALGORITHM_URL; + } + + @NonNull + public AlgorithmType getType() { + return AlgorithmType.Signature; + } + + @NonNull + public String getJCAAlgorithmID() { + return RSA_SHA256_MGF1_ALGORITHM_ID; + } + + @NonNull + public String getDigest() { + return SHA256_ALGORITHM_ID; + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Decrypter.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Decrypter.java new file mode 100644 index 0000000000000000000000000000000000000000..172e3bf1a9c7278d57dfdd78a89c7c3b3560c955 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Decrypter.java @@ -0,0 +1,130 @@ +package de.ozgcloud.fachstelle.security; + +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.Arrays; +import java.util.List; + +import jakarta.annotation.PostConstruct; + +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.config.InitializationService; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +public class Saml2Decrypter { + + @Getter + @Setter + private Decrypter decrypter; + + @Value("${spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location}") + private Resource decryptionPrivateKeyLocation; + + @Value("${spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location}") + private Resource decryptionCertificateLocation; + + private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + + @PostConstruct + void init() throws InitializationException { + InitializationService.initialize(); + + var decryptionX509Credential = getDecryptionCredential(); + var cred = CredentialSupport.getSimpleCredential(decryptionX509Credential.getCertificate(), decryptionX509Credential.getPrivateKey()); + + var resolver = new CollectionKeyInfoCredentialResolver(List.of(cred)); + var setupDecrypter = new Decrypter(null, resolver, encryptedKeyResolver); + setupDecrypter.setRootInNewDocument(true); + + decrypter = setupDecrypter; + } + + Attribute getDecryptedAttribute(String samlResponse, String attributeName) { + var parsedResponse = Saml2Parser.parse(samlResponse); + + decryptResponseElements(parsedResponse); + + return getAttributes(parsedResponse).stream().filter(attribute -> attributeName.equals(attribute.getName())).findFirst().orElseThrow(); + } + + private void decryptResponseElements(Response response) { + response.getEncryptedAssertions().stream() + .map(this::decryptAssertion) + .forEach(assertion -> response.getAssertions().add(assertion)); + } + + private Assertion decryptAssertion(EncryptedAssertion assertion) { + try { + return decrypter.decrypt(assertion); + } catch (Exception ex) { + LOG.error("failed to decrypt assertion {}", assertion); + throw new Saml2Exception(ex); + } + } + + private List<Attribute> getAttributes(Response response) { + var attributeStatement = (AttributeStatement) response.getAssertions().getFirst().getStatements().get(1); + + return attributeStatement.getAttributes(); + } + + private Saml2X509Credential getDecryptionCredential() { + var privateKey = readPrivateKey(decryptionPrivateKeyLocation); + var certificate = readCertificateFromResource(decryptionCertificateLocation); + + return new Saml2X509Credential(privateKey, certificate, Saml2X509Credential.Saml2X509CredentialType.DECRYPTION); + } + + private RSAPrivateKey readPrivateKey(Resource location) { + Assert.state(location != null, "No private key location specified"); + Assert.state(location.exists(), () -> "Private key location '" + location + "' does not exist"); + + try (var inputStream = location.getInputStream()) { + return RsaKeyConverters.pkcs8().convert(inputStream); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private X509Certificate readCertificateFromResource(Resource location) { + Assert.state(location != null, "No certificate location specified"); + Assert.state(location.exists(), () -> "Certificate location '" + location + "' does not exist"); + + try (var inputStream = location.getInputStream()) { + return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); + } catch (IOException | CertificateException e) { + throw new IllegalArgumentException(e); + } + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Parser.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Parser.java new file mode 100644 index 0000000000000000000000000000000000000000..c02667a6c8a935b1d76f74a18aff5cdf9706df3b --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/Saml2Parser.java @@ -0,0 +1,62 @@ +package de.ozgcloud.fachstelle.security; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.springframework.security.saml2.Saml2Exception; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.xml.BasicParserPool; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; + +@Log4j2 +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Saml2Parser { + + static Response parse(String samlResponse) { + try { + var inputStream = new ByteArrayInputStream(samlResponse.getBytes(StandardCharsets.UTF_8)); + var document = getParserPool().parse(inputStream); + + return (Response) getResponseUnmarshaller().unmarshall(document.getDocumentElement()); + } catch (ComponentInitializationException | XMLParserException | UnmarshallingException e) { + LOG.error("failed to parse samlResponse {}", samlResponse); + throw new Saml2Exception("Failed to parse samlResponse", e); + } + } + + static ParserPool getParserPool() throws ComponentInitializationException { + var parserPool = new BasicParserPool(); + parserPool.setBuilderFeatures(getXmlFeatureMap()); + parserPool.setBuilderAttributes(new HashMap<>()); + parserPool.initialize(); + + return parserPool; + } + + private static Map<String, Boolean> getXmlFeatureMap() { + final Map<String, Boolean> features = new HashMap<>(); + features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE); + features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE); + features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE); + features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE); + features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE); + + return features; + } + + static ResponseUnmarshaller getResponseUnmarshaller() { + return (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandler.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7f656161a12378385d209e4461bed8ccdb1d53dc --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandler.java @@ -0,0 +1,30 @@ +package de.ozgcloud.fachstelle.security; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; + +@ControllerAdvice +public class SecurityExceptionHandler { + + public static final String DEFAULT_ERROR_VIEW = "error"; + + @ExceptionHandler(value = SecurityException.class) + public ModelAndView handleSecurityException(HttpServletRequest request, SecurityException exception) { + ModelAndView mav = new ModelAndView(); + mav.addObject("exception", exception.getMessage()); + mav.addObject("url", request.getRequestURL()); + mav.setViewName(DEFAULT_ERROR_VIEW); + + clearSessionCookie(request); + + return mav; + } + + void clearSessionCookie(HttpServletRequest request) { + request.getSession().invalidate(); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityProvider.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..93753a6aa460f0c37afa36bc955af3907d4ad560 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/SecurityProvider.java @@ -0,0 +1,28 @@ +package de.ozgcloud.fachstelle.security; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.xmlsec.algorithm.AlgorithmRegistry; +import org.springframework.beans.factory.InitializingBean; + +public class SecurityProvider implements InitializingBean { + + @Override + public void afterPropertiesSet() { + Security.addProvider(new BouncyCastleProvider()); + + registerSignatureAlgorithms(); + } + + private void registerSignatureAlgorithms() { + var algorithmRegistry = ConfigurationService.get(AlgorithmRegistry.class); + if (algorithmRegistry != null) { + algorithmRegistry.register(new SHA256withRSAAndMGF1SignatureAlgorithm()); + + ConfigurationService.register(AlgorithmRegistry.class, algorithmRegistry); + } + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandler.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..09daf3e224312ccc9c3ebc1a5fbf61a2501d5f4b --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandler.java @@ -0,0 +1,91 @@ +package de.ozgcloud.fachstelle.security; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.WebAttributes; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Service; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Service +public class UrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final InMemoryUserDetailService userDetailService; + private final UserMapper userMapper; + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private final Map<String, String> roleTargetUrlMap = new HashMap<>(); + + public UrlAuthenticationSuccessHandler(final InMemoryUserDetailService userDetailService, final UserMapper userMapper) { + super(); + this.userDetailService = userDetailService; + this.userMapper = userMapper; + roleTargetUrlMap.put(DefaultRole.ROLE, "/preregister"); + } + + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) + throws IOException { + handle(request, response, authentication); + if (authentication instanceof Saml2Authentication saml2Authentication) { + LOG.debug("Adding User with SamlResponse: {}", saml2Authentication.getSaml2Response()); + var user = userMapper.map(saml2Authentication); + LOG.info("Mapped SamlResponse to user: {}", user); + userDetailService.addUser(user); + } else { + throw new SecurityException("Unsupported authentication type"); + } + clearAuthenticationAttributes(request); + } + + protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) + throws IOException { + final String targetUrl = determineTargetUrl(authentication); + + if (response.isCommitted()) { + LOG.debug("Response has already been committed. Unable to redirect to {}", targetUrl); + return; + } + + redirectStrategy.sendRedirect(request, response, targetUrl); + } + + protected String determineTargetUrl(final Authentication authentication) { + final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); + for (final GrantedAuthority grantedAuthority : authorities) { + String authorityName = grantedAuthority.getAuthority(); + if (roleTargetUrlMap.containsKey(authorityName)) { + return roleTargetUrlMap.get(authorityName); + } + } + + throw new IllegalStateException("Invalid role! User is missing role " + DefaultRole.ROLE); + } + + /** + * Removes temporary authentication-related data which may have been stored in the session during the authentication process. + */ + protected final void clearAuthenticationAttributes(final HttpServletRequest request) { + final HttpSession session = request.getSession(false); + + if (session == null) { + return; + } + + session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/User.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/User.java new file mode 100644 index 0000000000000000000000000000000000000000..834fefa3aee717cfc7b813630ad6c01e89172f7c --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/User.java @@ -0,0 +1,71 @@ +package de.ozgcloud.fachstelle.security; + +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Builder(toBuilder = true) +@Getter +@ToString +public class User implements UserDetails { + + private String id; + private String username; + private String companyName; + private String legalForm; + private String legalFormText; + private String registerNumber; + private String registerType; + private String emailAddress; + private String address; + private String password; + private String trustLevel; + @Setter + private String registrationKey; + @Setter + private Instant registrationKeyExpiresAt; + @Setter(AccessLevel.PACKAGE) + private Instant expirationDate; + private transient List<Map.Entry<String, List<Object>>> unknownAttributes; + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return List.of(new DefaultRole()); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return expirationDate != null && expirationDate.isAfter(Instant.now()); + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserAttributeProvider.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserAttributeProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..8dcc91914522f3a324362d0fa2d5b91570d98700 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserAttributeProvider.java @@ -0,0 +1,106 @@ +package de.ozgcloud.fachstelle.security; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +import org.apache.commons.lang3.ArrayUtils; +import org.opensaml.core.xml.XMLObject; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Component +@RequiredArgsConstructor +class UserAttributeProvider { + + static final String SAML_XML_STRASSE_NODE_NAME = "Strasse"; + static final String SAML_XML_HAUSNUMMER_NODE_NAME = "Hausnummer"; + static final String SAML_XML_PLZ_NODE_NAME = "PLZ"; + static final String SAML_XML_ORT_NODE_NAME = "Ort"; + static final String SAML_XML_LAND_NODE_NAME = "Land"; + + static final String MUK_FIRMENNAME_KEY = "Firmenname"; + static final String MUK_RECHTSFORM_KEY = "Rechtsform"; + static final String MUK_RECHTSFORM_TEXT_KEY = "RechtsformText"; + static final String MUK_REGISTERNUMMER_KEY = "Registernummer"; + static final String MUK_REGISTERART_KEY = "Registerart"; + static final String MUK_EMAIL_ADRESSE_KEY = "EMailAdresse"; + static final String MUK_ADRESSE_KEY = "Unternehmensanschrift"; + static final String MUK_VERTRAUENSNIVEAU_KEY = "ElsterVertrauensniveauAuthentifizierung"; + private static final String[] MUK_KNOWN_ATTRIBUTES = new String[] { MUK_FIRMENNAME_KEY, MUK_RECHTSFORM_KEY, MUK_RECHTSFORM_TEXT_KEY, + MUK_REGISTERNUMMER_KEY, MUK_REGISTERART_KEY, MUK_EMAIL_ADRESSE_KEY, MUK_ADRESSE_KEY, MUK_VERTRAUENSNIVEAU_KEY }; + + private final Saml2Decrypter saml2Decrypter; + + String getCompanyName(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_FIRMENNAME_KEY); + } + + String getLegalForm(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_RECHTSFORM_KEY); + } + + String getLegalFormText(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_RECHTSFORM_TEXT_KEY); + } + + String getRegisterNumber(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_REGISTERNUMMER_KEY); + } + + String getRegisterType(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_REGISTERART_KEY); + } + + String getEmailAddress(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_EMAIL_ADRESSE_KEY); + } + + String getTrustLevel(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getFirstAttribute(MUK_VERTRAUENSNIVEAU_KEY); + } + + String getAddress(String samlResponse) { + try { + var addressNode = saml2Decrypter.getDecryptedAttribute(samlResponse, MUK_ADRESSE_KEY); + var addressPartNodes = addressNode.getAttributeValues().getFirst().getOrderedChildren(); + + if (addressPartNodes != null && !addressPartNodes.isEmpty()) { + return getAddressByXMLNodes(addressPartNodes); + } + } catch (IllegalArgumentException | Saml2Exception | NoSuchElementException e) { + LOG.error("Failed parsing company address from SamlResponse: {}", samlResponse); + } + + return null; + } + + private static String getAddressByXMLNodes(final List<XMLObject> addressPartNodes) { + var addressBuilder = new StringBuilder(); + + for (XMLObject node : addressPartNodes) { + var nodeName = node.getElementQName().getLocalPart(); + var textContent = Objects.requireNonNull(node.getDOM()).getTextContent().trim(); + + addressBuilder.append(textContent); + if (SAML_XML_STRASSE_NODE_NAME.equals(nodeName) || SAML_XML_PLZ_NODE_NAME.equals(nodeName)) { + addressBuilder.append(" "); + } else if (SAML_XML_HAUSNUMMER_NODE_NAME.equals(nodeName) || SAML_XML_ORT_NODE_NAME.equals(nodeName)) { + addressBuilder.append(", "); + } + } + + return addressBuilder.toString().trim(); + } + + List<Map.Entry<String, List<Object>>> getUnknownAttributes(DefaultSaml2AuthenticatedPrincipal principal) { + return principal.getAttributes().entrySet().stream().filter(entry -> !ArrayUtils.contains(MUK_KNOWN_ATTRIBUTES, entry.getKey())).toList(); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserMapper.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..4d80722847c5d43dd5f6d1a9d8d35269227199d0 --- /dev/null +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/security/UserMapper.java @@ -0,0 +1,33 @@ +package de.ozgcloud.fachstelle.security; + +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class UserMapper { + + private final UserAttributeProvider userAttributeProvider; + + User map(Saml2Authentication authentication) { + var principal = (DefaultSaml2AuthenticatedPrincipal) authentication.getPrincipal(); + + return new User.UserBuilder() + .id(principal.getName()) + .username(principal.getName()) + .companyName(userAttributeProvider.getCompanyName(principal)) + .legalForm(userAttributeProvider.getLegalForm(principal)) + .legalFormText(userAttributeProvider.getLegalFormText(principal)) + .registerNumber(userAttributeProvider.getRegisterNumber(principal)) + .registerType(userAttributeProvider.getRegisterType(principal)) + .emailAddress(userAttributeProvider.getEmailAddress(principal)) + .address(userAttributeProvider.getAddress(authentication.getSaml2Response())) + .trustLevel(userAttributeProvider.getTrustLevel(principal)) + .unknownAttributes(userAttributeProvider.getUnknownAttributes(principal)) + .build(); + } + +} diff --git a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteService.java b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteService.java index 4ba5760478249e140dda96b02534ce06e58cb38b..c7c9436971548a8d71f9a5e01a4c990171d11059 100644 --- a/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteService.java +++ b/fachstelle-server/src/main/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteService.java @@ -38,33 +38,34 @@ import lombok.extern.log4j.Log4j2; @Component class VorgangRemoteService { - public static final String DUMMY_SAML_TOKEN = "--dummy-saml-token--"; - static final String FIND_VORGANG_URI = "/api/vorgang/{vorgangId}/{samlToken}"; - private final RestClient fachstellenProxyClient; - private final FachstellenProxyProperties fachstellenProxyProperties; - private final VorgangMapper vorgangMapper; + public static final String DUMMY_SAML_TOKEN = "--dummy-saml-token--"; + static final String FIND_VORGANG_URI = "/api/vorgang/{vorgangId}/{samlToken}"; - Vorgang findVorgang(String id, String collaborationManagerAddress) { - var response = fachstellenProxyClient.get().uri(buildFindVorgangUri(id)) - .accept(MediaType.APPLICATION_JSON) - .header(fachstellenProxyProperties.getCollaborationManagerAddressHeader(), collaborationManagerAddress) - .retrieve() - .toEntity(CollaborationGrpcFindVorgangResponse.class) - .getBody(); + private final RestClient fachstellenProxyClient; + private final FachstellenProxyProperties fachstellenProxyProperties; + private final VorgangMapper vorgangMapper; - if (response == null) { - throw new IllegalStateException("Received null vorgang response"); - } + Vorgang findVorgang(String id, String collaborationManagerAddress) { + var response = fachstellenProxyClient.get().uri(buildFindVorgangUri(id)) + .accept(MediaType.APPLICATION_JSON) + .header(fachstellenProxyProperties.getCollaborationManagerAddressHeader(), collaborationManagerAddress) + .retrieve() + .toEntity(CollaborationGrpcFindVorgangResponse.class) + .getBody(); - return buildVorgangResponse(response); - } + if (response == null) { + throw new IllegalStateException("Received null vorgang response"); + } - String buildFindVorgangUri(String vorgangId) { - return new UriTemplate(FIND_VORGANG_URI).expand(vorgangId, DUMMY_SAML_TOKEN).toString(); - } + return buildVorgangResponse(response); + } - private Vorgang buildVorgangResponse(CollaborationGrpcFindVorgangResponse response) { - return vorgangMapper.toVorgang(response.getVorgang()); - } + String buildFindVorgangUri(String vorgangId) { + return new UriTemplate(FIND_VORGANG_URI).expand(vorgangId, DUMMY_SAML_TOKEN).toString(); + } + + private Vorgang buildVorgangResponse(CollaborationGrpcFindVorgangResponse response) { + return vorgangMapper.toVorgang(response.getVorgang()); + } } diff --git a/fachstelle-server/src/main/resources/application.yml b/fachstelle-server/src/main/resources/application.yml index 215712d6a66aa7dedaced163341a5dcabe7f94ad..e5b2415565bfce8b9352f1318f623f39f6ba35a8 100644 --- a/fachstelle-server/src/main/resources/application.yml +++ b/fachstelle-server/src/main/resources/application.yml @@ -27,6 +27,11 @@ spring: application: name: Fachstellenbeteiligung security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${ozgcloud.oauth2.issuer-uri} + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs saml2: relyingparty: registration: @@ -35,6 +40,8 @@ spring: assertingparty: singlesignon: sign-request: true + mvc: + static-path-pattern: /static/** logging: level: @@ -47,7 +54,8 @@ logging: ozgcloud: oauth2: auth-server-url: ${keycloak.auth-server-url} - realm: ${keycloak.realm} + realm: fachstelle resource: ${keycloak.resource} + issuer-uri: ${ozgcloud.oauth2.auth-server-url}/realms/${ozgcloud.oauth2.realm} fachstellen-proxy: collaboration-manager-address-header: X-Grpc-Address \ No newline at end of file diff --git a/fachstelle-server/src/main/resources/static/index.html b/fachstelle-server/src/main/resources/static/index.html index 5aa76a97c1b523cc6083fe32bb430631939f6fd1..39c3f85470782463286b60a969d9357035ded278 100644 --- a/fachstelle-server/src/main/resources/static/index.html +++ b/fachstelle-server/src/main/resources/static/index.html @@ -24,23 +24,24 @@ <html lang="de"> <head> <meta charset="utf-8"/> - <link href="/favicon.ico" rel="icon"/> + <link href="static/favicon.ico" rel="icon"/> <meta content="width=device-width, initial-scale=1" name="viewport"/> <meta content="#000000" name="theme-color"/> <meta content="Seite zur Registrierung als Fachstelle auf Nachfrage einer Behörde" name="description" /> - <link href="App.css" rel="stylesheet"/> + <link href="static/App.css" rel="stylesheet"/> <link href="webjars/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet"/> <title>Fachstellen Registrierung</title> </head> <body> +<h1>debug</h1> <noscript>You need to enable JavaScript to run this app.</noscript> <div class="container content-box"> <div class="container App-header" id="ozg-header"> - <img alt="ozg cloud logo" class="App-logo" src="ozg_cloud_logo.png"/> + <img alt="ozg cloud logo" class="App-logo" src="static/ozg_cloud_logo.png"/> <span id="app-text">Fachstellenregistrierung</span> <hr/> </div> diff --git a/fachstelle-server/src/main/resources/templates/fragments/header.html b/fachstelle-server/src/main/resources/templates/fragments/header.html index ae9615c95c2bf78c492b2cf014a97e784ca600a7..94e3b04981376cd5c47e4bea555b3b73ab480089 100644 --- a/fachstelle-server/src/main/resources/templates/fragments/header.html +++ b/fachstelle-server/src/main/resources/templates/fragments/header.html @@ -1,18 +1,18 @@ <head> <meta content="text/html; charset=utf-8" http-equiv="content-type"/> - <link href="favicon.ico" rel="icon"/> + <link href="static/favicon.ico" rel="icon"/> <meta content="width=device-width, initial-scale=1" name="viewport"/> <meta content="#000000" name="theme-color"/> <meta content="Seite zur Registrierung als Fachstelle auf Nachfrage einer Behörde" name="description"/> - <link href="ozg_cloud_logo.png" rel="apple-touch-icon"/> - <link href="App.css" rel="stylesheet"/> + <link href="static/ozg_cloud_logo.png" rel="apple-touch-icon"/> + <link href="static/App.css" rel="stylesheet"/> <link href="webjars/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <title>Fachstellen Registrierung</title> </head> <div class="container App-header" id="ozg-header"> - <img alt="ozg cloud logo" class="App-logo" id="app-logo" src="ozg_cloud_logo.png"/> + <img alt="ozg cloud logo" class="App-logo" id="app-logo" src="static/ozg_cloud_logo.png"/> <span class="sub-title-text" id="app-text" th:text="#{messages.fachstellenregistrierung}"> Fachstellenregistrierung </span> diff --git a/fachstelle-server/src/test/helm/ingress_test.yaml b/fachstelle-server/src/test/helm/ingress_test.yaml index 28fb86bc74490570e3c95d8895540672a61dd7a9..5bbfb6c24feae31c85cc0d449c9790201fba976e 100644 --- a/fachstelle-server/src/test/helm/ingress_test.yaml +++ b/fachstelle-server/src/test/helm/ingress_test.yaml @@ -114,6 +114,66 @@ tests: name: fachstelle-server port: number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /registrierung + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /saml2 + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /login + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /preregister + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /register + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 + - contains: + path: spec.rules[0].http.paths + content: + path: /success + pathType: Prefix + backend: + service: + name: fachstelle-server + port: + number: 8080 - it: should fail template when baseUrl not set set: diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/SecurityConfigurationTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/SecurityConfigurationTest.java index cdeb76e4d55240ef71010ff26ed53915235f3a78..078190301fdcc0d63d7107158d420619991b4800 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/SecurityConfigurationTest.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/SecurityConfigurationTest.java @@ -23,103 +23,151 @@ */ package de.ozgcloud.fachstelle; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import de.ozgcloud.fachstelle.security.FachstelleLogoutSuccessHandler; +import de.ozgcloud.fachstelle.security.InMemoryUserDetailService; +import de.ozgcloud.fachstelle.security.UrlAuthenticationSuccessHandler; @ExtendWith(MockitoExtension.class) class SecurityConfigurationTest { - @Mock - FachstellenProperties fachstellenProperties; + @InjectMocks + private SecurityConfiguration securityConfiguration; + + @Mock + private InMemoryUserDetailService userDetailsService; + @Mock + private UrlAuthenticationSuccessHandler urlAuthenticationSuccessHandler; + + @Mock + private FachstellenProperties fachstellenProperties; + @Mock + private SpringJwtProperties springJwtProperties; + + @Test + void shouldCreateSecurityProvider() { + var securityProvider = securityConfiguration.securityProvider(); + + assertThat(securityProvider).isNotNull(); + } + + @Test + void shouldCreateAuthenticationTrustResolver() { + var authenticationTrustResolver = securityConfiguration.authenticationTrustResolver(); + + assertThat(authenticationTrustResolver).isNotNull(); + } + + @Nested + class TestHttpSecurity { + + private HttpSecurity httpSecurity; + + @BeforeEach + void init() throws Exception { + httpSecurity = mock(HttpSecurity.class); + when(httpSecurity.authorizeHttpRequests(any())).thenReturn(httpSecurity); + when(httpSecurity.saml2Login(any())).thenReturn(httpSecurity); + when(httpSecurity.saml2Logout(any())).thenReturn(httpSecurity); + } + + @Test + void shouldSetupFilterChainAuthorize() throws Exception { + securityConfiguration.filterChain(httpSecurity); + + verify(httpSecurity).authorizeHttpRequests(any()); + } + + @Disabled + @Test + void shouldSetupFilterChainSaml2Login() throws Exception { + securityConfiguration.filterChain(httpSecurity); - private SecurityConfiguration securityConfiguration; + verify(httpSecurity).saml2Login(any()); + } - @BeforeEach - void setUp() { - securityConfiguration = new SecurityConfiguration(fachstellenProperties); - } + @Disabled + @Test + void shouldSetupFilterChainSaml2Logout() throws Exception { + securityConfiguration.filterChain(httpSecurity); - @Test - void shouldCreateAuthenticationTrustResolver() { - var authenticationTrustResolver = securityConfiguration.authenticationTrustResolver(); + verify(httpSecurity).saml2Logout(any()); + } - assertThat(authenticationTrustResolver).isNotNull(); - } + } - @Nested - class TestHttpSecurity { + @Nested + class TestAuthenticationRequestResolver { - private HttpSecurity httpSecurity; + @Mock + private RelyingPartyRegistrationRepository registrations; - @BeforeEach - void init() throws Exception { - httpSecurity = mock(HttpSecurity.class); - when(httpSecurity.authorizeHttpRequests(any())).thenReturn(httpSecurity); - } + @Test + void shouldCreateSaml2AuthenticationRequestResolver() { + var authResolver = securityConfiguration.authenticationRequestResolver(registrations); - @Test - void shouldSetupFilterChainAuthorize() throws Exception { - securityConfiguration.filterChain(httpSecurity); + assertThat(authResolver).isInstanceOf(Saml2AuthenticationRequestResolver.class); + } - verify(httpSecurity).authorizeHttpRequests(any()); - } + } - @Disabled - @Test - void shouldSetupFilterChainSaml2Login() throws Exception { - securityConfiguration.filterChain(httpSecurity); + @Nested + class TestSamlFilterChain { - verify(httpSecurity).saml2Login(any()); - } + @Mock + HttpSecurity security; - @Disabled - @Test - void shouldSetupFilterChainSaml2Logout() throws Exception { - securityConfiguration.filterChain(httpSecurity); + @BeforeEach + void setUp() throws Exception { + when(security.authorizeHttpRequests(any())).thenReturn(security); + when(security.saml2Login(any())).thenReturn(security); + when(security.saml2Logout(any())).thenReturn(security); + } - verify(httpSecurity).saml2Logout(any()); - } + @Disabled + @Test + void shouldSetSamlLogin() throws Exception { + securityConfiguration.filterChain(security); - } + verify(security).saml2Login(any()); + } - @Nested - class TestSamlFilterChain { + @Disabled + @Test + void shouldSetSamlLogout() throws Exception { + securityConfiguration.filterChain(security); - @Mock - HttpSecurity security; + verify(security).saml2Logout(any()); + } - @BeforeEach - void setUp() throws Exception { - when(security.authorizeHttpRequests(any())).thenReturn(security); - when(security.saml2Login(any())).thenReturn(security); - when(security.saml2Logout(any())).thenReturn(security); - } + } - @Disabled - @Test - void shouldSetSamlLogin() throws Exception { - securityConfiguration.filterChain(security); + @Nested + class TestLogoutHandler { - verify(security).saml2Login(any()); - } + @Test + void shouldCreateLogoutHandler() { + when(fachstellenProperties.getLogoutSuccessUrl()).thenReturn("http://localhost:8080/logout"); - @Disabled - @Test - void shouldSetSamlLogout() throws Exception { - securityConfiguration.filterChain(security); + var handler = securityConfiguration.getLogoutSuccessHandler(); - verify(security).saml2Logout(any()); - } + assertThat(handler).isInstanceOf(FachstelleLogoutSuccessHandler.class); + } - } + } } \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/attachment/AttachmentControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/attachment/AttachmentControllerITCase.java index 5203d17fc9f73e8ebbe1810dc882ad689043cd8a..7509e4a54febda1cff20a738119affdba0740734 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/attachment/AttachmentControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/attachment/AttachmentControllerITCase.java @@ -47,17 +47,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class AttachmentControllerITCase { @@ -115,9 +117,9 @@ class AttachmentControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(AttachmentController.PATH + "?eingangId=" + EingangTestFactory.ID) - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(AttachmentController.PATH + "?eingangId=" + EingangTestFactory.ID) + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/command/CommandControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/command/CommandControllerITCase.java index 4dfb10ec6a529b8f3180f20aa547c567b97554ba..e14b64142d4da0845852bef0470cf21e1d16c71d 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/command/CommandControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/command/CommandControllerITCase.java @@ -46,81 +46,83 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class CommandControllerITCase { - @Autowired - private MockMvc mockMvc; - - @SneakyThrows - @Test - void shouldReturnStatusOk() { - performRequest().andExpect(status().isOk()); - } - - @SneakyThrows - @Test - void shouldHaveId() { - performRequest().andExpect(jsonPath("$.id").value(CommandTestFactory.ID)); - } - - @SneakyThrows - @Test - void shouldHaveCreatedAt() { - performRequest().andExpect(jsonPath("$.createdAt").exists()); - } - - @SneakyThrows - @Test - void shouldHaveFinishedAt() { - performRequest().andExpect(jsonPath("$.finishedAt").exists()); - } - - @SneakyThrows - @Test - void shouldHaveStatus() { - performRequest().andExpect(jsonPath("$.status").value(CommandStatus.FINISHED.toString())); - } - - @SneakyThrows - @Test - void shouldHaveVorgangId() { - performRequest().andExpect(jsonPath("$.vorgangId").value("TestVorgangId")); - } - - @SneakyThrows - @Test - void shouldHaveOrder() { - performRequest().andExpect(jsonPath("$.order").value("TestOrder")); - } - - @SneakyThrows - @Test - void shouldHaveErrorMessage() { - performRequest().andExpect(jsonPath("$.errorMessage").value("")); - } - - @SneakyThrows - @Test - void shouldHaveLinks() { - performRequest().andExpect(jsonPath("$._links").exists()); - } - - @SneakyThrows - private ResultActions performRequest() { - return mockMvc.perform(get(CommandController.PATH + "/" + CommandTestFactory.ID).contentType(MediaType.APPLICATION_JSON) - .characterEncoding(Charset.defaultCharset()).with(csrf().asHeader())); - } + @Autowired + private MockMvc mockMvc; + + @SneakyThrows + @Test + void shouldReturnStatusOk() { + performRequest().andExpect(status().isOk()); + } + + @SneakyThrows + @Test + void shouldHaveId() { + performRequest().andExpect(jsonPath("$.id").value(CommandTestFactory.ID)); + } + + @SneakyThrows + @Test + void shouldHaveCreatedAt() { + performRequest().andExpect(jsonPath("$.createdAt").exists()); + } + + @SneakyThrows + @Test + void shouldHaveFinishedAt() { + performRequest().andExpect(jsonPath("$.finishedAt").exists()); + } + + @SneakyThrows + @Test + void shouldHaveStatus() { + performRequest().andExpect(jsonPath("$.status").value(CommandStatus.FINISHED.toString())); + } + + @SneakyThrows + @Test + void shouldHaveVorgangId() { + performRequest().andExpect(jsonPath("$.vorgangId").value("TestVorgangId")); + } + + @SneakyThrows + @Test + void shouldHaveOrder() { + performRequest().andExpect(jsonPath("$.order").value("TestOrder")); + } + + @SneakyThrows + @Test + void shouldHaveErrorMessage() { + performRequest().andExpect(jsonPath("$.errorMessage").value("")); + } + + @SneakyThrows + @Test + void shouldHaveLinks() { + performRequest().andExpect(jsonPath("$._links").exists()); + } + + @SneakyThrows + private ResultActions performRequest() { + return mockMvc.perform(get(CommandController.PATH + "/" + CommandTestFactory.ID).contentType(MediaType.APPLICATION_JSON) + .characterEncoding(Charset.defaultCharset()).with(csrf().asHeader())); + } } \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/historie/HistorieControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/historie/HistorieControllerITCase.java index 9a1d874491becce6a2bd6f5e8bf8c11ff758b6ac..cefc03c6e56df87170e7e2afacccf28f20bc8ed3 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/historie/HistorieControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/historie/HistorieControllerITCase.java @@ -48,17 +48,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class HistorieControllerITCase { @@ -134,9 +136,9 @@ class HistorieControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(HistorieController.PATH + "?vorgangId=" + VorgangTestFactory.ID) - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(HistorieController.PATH + "?vorgangId=" + VorgangTestFactory.ID) + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarByVorgangControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarByVorgangControllerITCase.java index a32e9fd3c14285788140e01395f2442653abb4bb..649b932a2461ab8b28fe77aadc9f71c7f01e7df1 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarByVorgangControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarByVorgangControllerITCase.java @@ -47,17 +47,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class KommentarByVorgangControllerITCase { @@ -133,9 +135,9 @@ class KommentarByVorgangControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(KommentarController.KommentarByVorgangController.PATH + "/" + VorgangTestFactory.ID + "/kommentars") - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(KommentarController.KommentarByVorgangController.PATH + "/" + VorgangTestFactory.ID + "/kommentars") + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarControllerITCase.java index 9598b4787caabd9fc9e9d8a619c716d02569f35e..d79f9f01334e2b0aa90a0627489675cfb3e67e19 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/kommentar/KommentarControllerITCase.java @@ -47,17 +47,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class KommentarControllerITCase { @@ -124,9 +126,9 @@ class KommentarControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(KommentarController.PATH + "/" + KommentarTestFactory.ID) - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(KommentarController.PATH + "/" + KommentarTestFactory.ID) + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } @@ -185,9 +187,9 @@ class KommentarControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(KommentarController.PATH + "/" + KommentarTestFactory.ID + "/attachments") - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(KommentarController.PATH + "/" + KommentarTestFactory.ID + "/attachments") + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..66fde07fcec09f51ae36dd62fecfaf5b40e570a2 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerITCase.java @@ -0,0 +1,47 @@ +package de.ozgcloud.fachstelle.registration; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc(printOnlyOnFailure = false) +@TestPropertySource(properties = { + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" +}) +class FachstelleRegistrationControllerITCase { + + @Autowired + private MockMvc mockMvc; + + @Test + void whenCallIndexPage_ThenReturnPage() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/registrierung") + .header("Accept-Language", "de")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Registrierung als Fachstelle"))); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ce2a95e161c95668f0e243cb6d731f8fefd614a2 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationControllerTest.java @@ -0,0 +1,356 @@ +package de.ozgcloud.fachstelle.registration; + +import static de.ozgcloud.fachstelle.security.UserTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.ui.Model; + +import de.ozgcloud.fachstelle.FachstellenProperties; +import de.ozgcloud.fachstelle.security.InMemoryUserDetailService; +import de.ozgcloud.fachstelle.security.User; + +@ExtendWith(MockitoExtension.class) +class FachstelleRegistrationControllerTest { + + @InjectMocks + @Spy + private FachstelleRegistrationController fachstelleRegistrationController; + @Mock + private InMemoryUserDetailService userDetailService; + @Mock + private FachstellenProperties fachstellenProperties; + @Mock + private FachstelleRegistrationService registrationService; + + @Nested + class TestStartRegistration { + + @Mock + private Model model; + + @BeforeEach + void setUp() { + when(fachstellenProperties.getLoginRedirectUrl()).thenReturn("/login"); + } + + @Test + void shouldCreateIndexPage() { + var res = fachstelleRegistrationController.startRegistration(model); + + assertThat(res).isEqualTo("index"); + } + + @Test + void shouldSetLoginUrl() { + fachstelleRegistrationController.startRegistration(model); + + verify(model).addAttribute("loginUrl", "/login"); + } + + } + + @Nested + class TestRegistrationPage { + + @Mock + private Model model; + + @Mock + private Authentication authentication; + + @BeforeEach + void setUp() { + when(authentication.getName()).thenReturn("user"); + when(userDetailService.loadUserByUsername("user")).thenReturn(create()); + } + + @Test + void shouldCreateRegisterPage() { + var res = fachstelleRegistrationController.preregister(model, authentication); + + assertThat(res).isEqualTo("preregister"); + } + + @Test + void shouldSetOrganizationName() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationName", COMPANY_NAME); + } + + @Test + void shouldSetOrganizationLegalForm() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationLegalForm", LEGAL_FORM_TEXT); + } + + @Test + void shouldSetOrganizationRegisterType() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationRegisterType", REGISTER_TYPE); + } + + @Test + void shouldSetOrganizationRegisterNumber() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationRegisterNumber", REGISTER_NUMBER); + } + + @Test + void shouldSetOrganizationEmail() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationEmail", EMAIL_ADDRESS); + } + + @Test + void shouldSetOrganizationAddress() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute("organizationAddress", ADDRESS); + } + + @Test + void shouldSetRegistration() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(model).addAttribute(eq("registration"), any(Registration.class)); + } + + @Test + void shouldSetRegistrationKey() { + fachstelleRegistrationController.preregister(model, authentication); + + verify(fachstelleRegistrationController).addRegistrationKey(any(User.class)); + } + + } + + @Nested + class TestSettingRegistrationKey { + + User user = create(); + + @Test + void shouldSetRegistrationKey() { + fachstelleRegistrationController.addRegistrationKey(user); + + assertThat(user.getRegistrationKey()).isNotNull(); + } + + @Test + void shouldSetRegistrationExpirationDate() { + fachstelleRegistrationController.addRegistrationKey(user); + + assertThat(user.getRegistrationKeyExpiresAt()).isBetween(Instant.now().plus(598, ChronoUnit.SECONDS), + Instant.now().plus(602, ChronoUnit.SECONDS)); + } + + } + + @Nested + class TestSuccessPage { + + @Mock + private Model model; + + @Mock + private Authentication authentication; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpSession session; + + @BeforeEach + void setUp() { + when(authentication.getName()).thenReturn("user"); + when(userDetailService.loadUserByUsername("user")).thenReturn(create()); + when(request.getSession()).thenReturn(session); + } + + @Test + void shouldCreateSuccessPage() { + var res = fachstelleRegistrationController.success(model, authentication, request); + + assertThat(res).isEqualTo("success"); + } + + @Test + void shouldSetUserName() { + fachstelleRegistrationController.success(model, authentication, request); + + verify(model).addAttribute(eq("name"), any(User.class)); + } + + @Test + void shouldInvalidateSession() { + fachstelleRegistrationController.success(model, authentication, request); + + verify(session).invalidate(); + } + + @Test + void shouldLogout() { + fachstelleRegistrationController.success(model, authentication, request); + + verify(userDetailService).logout(any(User.class)); + } + + } + + @Nested + class TestRegistration { + + @Mock + private Model model; + + @Mock + private Authentication authentication; + + @Mock + private Registration registration; + + @BeforeEach + void setUp() { + when(authentication.getName()).thenReturn("user"); + var user = create(); + user.setRegistrationKey("key"); + user.setRegistrationKeyExpiresAt(Instant.now().plus(10, ChronoUnit.MINUTES)); + when(userDetailService.loadUserByUsername("user")).thenReturn(user); + when(registration.key()).thenReturn("key"); + } + + @Test + void shouldRegister() { + fachstelleRegistrationController.register(registration, model, authentication); + + verify(registrationService).register(any(User.class)); + } + + @Test + void shouldReturnSuccessOnRegistration() { + when(registrationService.register(any(User.class))).thenReturn(true); + + var res = fachstelleRegistrationController.register(registration, model, authentication); + + assertThat(res).isEqualTo("success"); + } + + @Test + void shouldNotRegister() { + when(registration.key()).thenReturn("otherKey"); + var res = fachstelleRegistrationController.register(registration, model, authentication); + + assertThat(res).isEqualTo("error"); + } + + @Test + void shouldSetErrorOnRegistrationKeyError() { + when(registration.key()).thenReturn("otherKey"); + + fachstelleRegistrationController.register(registration, model, authentication); + + verify(model).addAttribute("errorMessageKey", "error_registration_key_does_not_exist"); + } + + @Test + void shouldSetErrorOnRegistrationError() { + when(registrationService.register(any(User.class))).thenReturn(false); + + var res = fachstelleRegistrationController.register(registration, model, authentication); + + assertThat(res).isEqualTo("error"); + } + + } + + @Nested + class TestRegistrationTimeout { + + @Mock + private Model model; + + @Mock + private Authentication authentication; + + @Mock + private Registration registration; + + @BeforeEach + void setUp() { + when(authentication.getName()).thenReturn("user"); + var user = create(); + user.setRegistrationKey("key"); + user.setRegistrationKeyExpiresAt(Instant.now().minus(10, ChronoUnit.MINUTES)); + when(userDetailService.loadUserByUsername("user")).thenReturn(user); + when(registration.key()).thenReturn("key"); + } + + @Test + void shouldSetErrorOnRegistrationExpired() { + fachstelleRegistrationController.register(registration, model, authentication); + + verify(model).addAttribute("errorMessageKey", "error_registration_key_expired"); + } + + } + + @Nested + class TestCanRegister { + + User user; + + @BeforeEach + void setUp() { + user = create(); + user.setRegistrationKey("key"); + user.setRegistrationKeyExpiresAt(Instant.now().plus(10, ChronoUnit.MINUTES)); + } + + @Test + void shouldReturnOk() { + var res = fachstelleRegistrationController.canRegister(user, "key"); + + assertThat(res).isEqualTo("ok"); + } + + @Test + void shouldReturnExpiredErrorKey() { + user.setRegistrationKeyExpiresAt(Instant.now().minus(10, ChronoUnit.MINUTES)); + + var res = fachstelleRegistrationController.canRegister(user, "key"); + + assertThat(res).isEqualTo("error_registration_key_expired"); + } + + @Test + void shouldReturnNotFoundErrorKey() { + var res = fachstelleRegistrationController.canRegister(user, "otherHey"); + + assertThat(res).isEqualTo("error_registration_key_does_not_exist"); + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteServiceTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..496f664384460e6adeab4fe6a1d6213d6910f74c --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRemoteServiceTest.java @@ -0,0 +1,92 @@ +package de.ozgcloud.fachstelle.registration; + +import static de.ozgcloud.fachstelle.registration.FachstelleRegistrationRemoteService.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClient; + +import de.ozgcloud.fachstelle.security.User; +import de.ozgcloud.fachstelle.security.UserTestFactory; + +@ExtendWith(MockitoExtension.class) +class FachstelleRegistrationRemoteServiceTest { + + @Spy + @InjectMocks + private FachstelleRegistrationRemoteService service; + + @Mock + private RestClient restClient; + + @Mock + private FachstelleRegistrationRequestMapper requestMapper; + + @Nested + class TestRegister { + private static final User user = UserTestFactory.create(); + + private RestClient.ResponseSpec responseSpec; + + @BeforeEach + void setUp() { + var request = GrpcFachstelleRegistrationRequestTestFactory.create(); + var uriSpec = mock(RestClient.RequestBodyUriSpec.class); + var bodySpec = mock(RestClient.RequestBodySpec.class); + responseSpec = mock(RestClient.ResponseSpec.class); + + when(requestMapper.toFachstelleRegistrationRequest(user)).thenReturn(request); + when(restClient.post()).thenReturn(uriSpec); + when(uriSpec.uri(REGISTER_FACHSTELLE_URI)).thenReturn(bodySpec); + when(bodySpec.contentType(MediaType.APPLICATION_JSON)).thenReturn(bodySpec); + when(bodySpec.body(request)).thenReturn(bodySpec); + when(bodySpec.retrieve()).thenReturn(responseSpec); + } + + @Test + void shouldCallRestClient() { + when(responseSpec.toBodilessEntity()).thenReturn(new ResponseEntity<>(HttpStatus.OK)); + + service.register(user); + + verify(restClient).post(); + } + + @Test + void shouldCallRequestMapper() { + when(responseSpec.toBodilessEntity()).thenReturn(new ResponseEntity<>(HttpStatus.OK)); + + service.register(user); + + verify(requestMapper).toFachstelleRegistrationRequest(user); + } + + @Test + void shouldRegisterSuccessfully() { + when(responseSpec.toBodilessEntity()).thenReturn(new ResponseEntity<>(HttpStatus.OK)); + + var result = service.register(user); + assertTrue(result); + } + + @Test + void shouldHaveRegistrationError() { + when(responseSpec.toBodilessEntity()).thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + var result = service.register(user); + assertFalse(result); + } + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapperTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bb11baf9a7655ed079f676f47b3bb7f80475ab15 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationRequestMapperTest.java @@ -0,0 +1,24 @@ +package de.ozgcloud.fachstelle.registration; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mapstruct.factory.Mappers; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.ozgcloud.fachstelle.security.UserTestFactory; + +@ExtendWith(MockitoExtension.class) +class FachstelleRegistrationRequestMapperTest { + @InjectMocks + private final FachstelleRegistrationRequestMapper mapper = Mappers.getMapper(FachstelleRegistrationRequestMapper.class); + + @Test + void shouldMap() { + var request = mapper.toFachstelleRegistrationRequest(UserTestFactory.create()); + + assertThat(request).usingRecursiveComparison().isEqualTo(GrpcFachstelleRegistrationRequestTestFactory.create()); + } +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationServiceTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ec79fb58b3c9285163b33b1e44be90bc04b9e96b --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/FachstelleRegistrationServiceTest.java @@ -0,0 +1,48 @@ +package de.ozgcloud.fachstelle.registration; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.ozgcloud.fachstelle.security.User; +import de.ozgcloud.fachstelle.security.UserTestFactory; + +@ExtendWith(MockitoExtension.class) +class FachstelleRegistrationServiceTest { + @InjectMocks + private FachstelleRegistrationService service; + + @Mock + private FachstelleRegistrationRemoteService remoteService; + + @Nested + class TestRegister { + private static final User user = UserTestFactory.create(); + + @BeforeEach + void init() { + when(remoteService.register(user)).thenReturn(true); + } + + @Test + void shouldCallRemoteService() { + service.register(user); + + verify(remoteService).register(user); + } + + @Test + void shouldReturnRegistrationResult() { + var result = service.register(user); + + assertTrue(result); + } + } +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/GrpcFachstelleRegistrationRequestTestFactory.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/GrpcFachstelleRegistrationRequestTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..d8219522d3883707db017c2d63b4f360538aafe9 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/registration/GrpcFachstelleRegistrationRequestTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.fachstelle.registration; + +import de.ozgcloud.fachstelle.proxy.FachstellenproxyGrpcFachstelleRegistrationRequest; +import de.ozgcloud.fachstelle.security.UserTestFactory; + +public class GrpcFachstelleRegistrationRequestTestFactory { + public static FachstellenproxyGrpcFachstelleRegistrationRequest create() { + return new FachstellenproxyGrpcFachstelleRegistrationRequest() + .mukId(UserTestFactory.USER_ID) + .firmenName(UserTestFactory.COMPANY_NAME) + .rechtsform(UserTestFactory.LEGAL_FORM) + .rechtsformText(UserTestFactory.LEGAL_FORM_TEXT) + .registerNummer(UserTestFactory.REGISTER_NUMBER) + .registerArt(UserTestFactory.REGISTER_TYPE) + .emailAdresse(UserTestFactory.EMAIL_ADDRESS) + .anschrift(UserTestFactory.ADDRESS); + } +} diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/representation/RepresentationControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/representation/RepresentationControllerITCase.java index a0fb15ef71c0afe60c48a693c327d05655140e75..0a18ccc0fe062ab081d00607f8d1cbb08c50af11 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/representation/RepresentationControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/representation/RepresentationControllerITCase.java @@ -47,17 +47,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class RepresentationControllerITCase { @@ -115,9 +117,9 @@ class RepresentationControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(RepresentationController.PATH + "?eingangId=" + EingangTestFactory.ID) - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(RepresentationController.PATH + "?eingangId=" + EingangTestFactory.ID) + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/resource/OzgcloudResourceControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/resource/OzgcloudResourceControllerITCase.java index 1988cc22cac0d477f9558467e58f78ce6bf4e7f4..2617195b04ea167311c410e6c414eae32e6ebf5d 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/resource/OzgcloudResourceControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/resource/OzgcloudResourceControllerITCase.java @@ -51,87 +51,89 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class OzgcloudResourceControllerITCase { - @Autowired - private MockMvc mockMvc; + @Autowired + private MockMvc mockMvc; - @Nested - class TestGetOzgcloudResource { + @Nested + class TestGetOzgcloudResource { - private static final String COLLABORATION_MANAGER_ADDRESS = VorgangWithCollaborationManagerAddressTestFactory.COLLABORATION_MANAGER_ADDRESS; - private static final String VORGANG_ID = VorgangTestFactory.ID; - private static final String VALID_URI = COLLABORATION_MANAGER_ADDRESS + "/vorgangs/" + VORGANG_ID; - private static final String INVALID_URI = COLLABORATION_MANAGER_ADDRESS + ":123/" + VORGANG_ID; + private static final String COLLABORATION_MANAGER_ADDRESS = VorgangWithCollaborationManagerAddressTestFactory.COLLABORATION_MANAGER_ADDRESS; + private static final String VORGANG_ID = VorgangTestFactory.ID; + private static final String VALID_URI = COLLABORATION_MANAGER_ADDRESS + "/vorgangs/" + VORGANG_ID; + private static final String INVALID_URI = COLLABORATION_MANAGER_ADDRESS + ":123/" + VORGANG_ID; - @Test - void shouldReturnStatusOk() throws Exception { - var response = doRequest(VALID_URI); + @Test + void shouldReturnStatusOk() throws Exception { + var response = doRequest(VALID_URI); - response.andExpect(status().isOk()); - } + response.andExpect(status().isOk()); + } - @Test - void shouldHaveVorgangLink() throws Exception { - var response = doRequest(VALID_URI); + @Test + void shouldHaveVorgangLink() throws Exception { + var response = doRequest(VALID_URI); - response.andExpect(jsonPath("$._links.vorgang.href").value(StringEndsWith.endsWith( - "/api/vorgangs/" + VORGANG_ID + "?" + PARAM_COLLABORATION_MANAGER_ADDRESS + "=" + COLLABORATION_MANAGER_ADDRESS))); - } + response.andExpect(jsonPath("$._links.vorgang.href").value(StringEndsWith.endsWith( + "/api/vorgangs/" + VORGANG_ID + "?" + PARAM_COLLABORATION_MANAGER_ADDRESS + "=" + COLLABORATION_MANAGER_ADDRESS))); + } - @Test - void shouldHaveSelfLink() throws Exception { - var encodedUri = URLEncoder.encode(VALID_URI, StandardCharsets.UTF_8); + @Test + void shouldHaveSelfLink() throws Exception { + var encodedUri = URLEncoder.encode(VALID_URI, StandardCharsets.UTF_8); - var response = doRequest(VALID_URI); + var response = doRequest(VALID_URI); - response.andExpect( - jsonPath("$._links.self.href").value(StringEndsWith.endsWith(OzgcloudResourceController.PATH + "?" + PARAM_URI + "=" + encodedUri))); - } + response.andExpect( + jsonPath("$._links.self.href").value(StringEndsWith.endsWith(OzgcloudResourceController.PATH + "?" + PARAM_URI + "=" + encodedUri))); + } - @Test - void shouldReturnStatusNotFound() throws Exception { - var response = doRequest(INVALID_URI); + @Test + void shouldReturnStatusNotFound() throws Exception { + var response = doRequest(INVALID_URI); - response.andExpect(status().isNotFound()); - } + response.andExpect(status().isNotFound()); + } - @Test - void shouldReturnBadRequestOnNoRequestParam() throws Exception { - var response = doRequestWithQueryString(""); + @Test + void shouldReturnBadRequestOnNoRequestParam() throws Exception { + var response = doRequestWithQueryString(""); - response.andExpect(status().isBadRequest()); - } + response.andExpect(status().isBadRequest()); + } - @Test - void shouldReturnBadRequestOnEmptyUri() throws Exception { - var response = doRequestWithQueryString("?" + PARAM_URI + "="); + @Test + void shouldReturnBadRequestOnEmptyUri() throws Exception { + var response = doRequestWithQueryString("?" + PARAM_URI + "="); - response.andExpect(status().isBadRequest()); - } + response.andExpect(status().isBadRequest()); + } - @SneakyThrows - private ResultActions doRequest(String uri) { - return doRequestWithQueryString("?" + PARAM_URI + "=" + uri); - } + @SneakyThrows + private ResultActions doRequest(String uri) { + return doRequestWithQueryString("?" + PARAM_URI + "=" + uri); + } - @SneakyThrows - private ResultActions doRequestWithQueryString(String queryString) { - return mockMvc.perform(get(OzgcloudResourceController.PATH + queryString)); - } + @SneakyThrows + private ResultActions doRequestWithQueryString(String queryString) { + return mockMvc.perform(get(OzgcloudResourceController.PATH + queryString)); + } - } + } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/CurrentUserServiceTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/CurrentUserServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f88895389a11d10a5de9fad62a84b87dfa24fa22 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/CurrentUserServiceTest.java @@ -0,0 +1,94 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +import de.ozgcloud.fachstelle.common.errorhandling.SamlTokenNotFoundException; + +@ExtendWith(MockitoExtension.class) +class CurrentUserServiceTest { + + @Spy + @InjectMocks + private CurrentUserService service; + + @Mock + private AuthenticationTrustResolver trustResolver; + + @Nested + class TestGetAuthentication { + + @Test + void shouldReturnAuthentication() { + try (var securityContextHolder = mockStatic(SecurityContextHolder.class)) { + var authentication = mock(Saml2Authentication.class); + var context = mock(SecurityContext.class); + when(context.getAuthentication()).thenReturn(authentication); + securityContextHolder.when(SecurityContextHolder::getContext).thenReturn(context); + + assertThat(service.getAuthentication()).isEqualTo(authentication); + } + } + + @Test + void shouldThrowExceptionOnUntrustedAuthentication() { + try (var securityContextHolder = mockStatic(SecurityContextHolder.class)) { + var authentication = mock(AnonymousAuthenticationToken.class); + var context = mock(SecurityContext.class); + when(trustResolver.isAnonymous(authentication)).thenReturn(true); + when(context.getAuthentication()).thenReturn(authentication); + securityContextHolder.when(SecurityContextHolder::getContext).thenReturn(context); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(service::getAuthentication); + } + } + + @Test + void shouldThrowExceptionOnNullAuthentication() { + try (var securityContextHolder = mockStatic(SecurityContextHolder.class)) { + var context = mock(SecurityContext.class); + when(context.getAuthentication()).thenReturn(null); + securityContextHolder.when(SecurityContextHolder::getContext).thenReturn(context); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(service::getAuthentication); + } + } + + } + + @Nested + class TestGetSamlToken { + + @Test + void shouldReturnSamlToken() { + var authentication = mock(Saml2Authentication.class); + when(authentication.getSaml2Response()).thenReturn(UserTestFactory.SAML_TOKEN); + doReturn(authentication).when(service).getAuthentication(); + + assertThat(service.getSamlToken()).isEqualTo(UserTestFactory.SAML_TOKEN); + } + + @Test + void shouldThrowExceptionOnNonSamlAuthentication() { + var authentication = mock(AnonymousAuthenticationToken.class); + doReturn(authentication).when(service).getAuthentication(); + + assertThatExceptionOfType(SamlTokenNotFoundException.class).isThrownBy(service::getSamlToken); + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandlerTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandlerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..57b557920691f55ecf0b8ca043e8ea7a9b504f2f --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/FachstelleLogoutSuccessHandlerTest.java @@ -0,0 +1,45 @@ +package de.ozgcloud.fachstelle.security; + +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; + +@ExtendWith(MockitoExtension.class) +class FachstelleLogoutSuccessHandlerTest { + + @Spy + @InjectMocks + private FachstelleLogoutSuccessHandler fachstelleLogoutSuccessHandler; + @Mock + private InMemoryUserDetailService userDetailService; + + @Test + void shouldCallLogout() throws ServletException, IOException { + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(UserTestFactory.create()); + + fachstelleLogoutSuccessHandler.onLogoutSuccess(mock(HttpServletRequest.class), mock(HttpServletResponse.class), authentication); + + verify(userDetailService).logout(any(User.class)); + } + + @Test + void shouldNotCallLogout() throws ServletException, IOException { + fachstelleLogoutSuccessHandler.onLogoutSuccess(mock(HttpServletRequest.class), mock(HttpServletResponse.class), mock(Authentication.class)); + + verify(userDetailService, never()).logout(any(User.class)); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailServiceTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ef543bcdf5e04cfb21e0d2dbf88cfbf598c54a5 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/InMemoryUserDetailServiceTest.java @@ -0,0 +1,115 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +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.junit.jupiter.MockitoExtension; + +import com.google.common.testing.FakeTicker; + +@ExtendWith(MockitoExtension.class) +class InMemoryUserDetailServiceTest { + + private final InMemoryUserDetailService userDetailService = new InMemoryUserDetailService(); + private User user; + + @Nested + class TestAddingUser { + + @BeforeEach + void setup() { + user = UserTestFactory.create(); + userDetailService.addUser(user); + } + + @Test + void shouldAddUser() { + assertThat(userDetailService.getUser(UserTestFactory.USER_ID)).isNotNull(); + } + + @Test + void shouldLoadUser() { + userDetailService.addUser(user); + + assertThat(userDetailService.loadUserByUsername(UserTestFactory.USER_NAME)).isNotNull(); + } + + } + + @Nested + class TestSetUser { + + @BeforeEach + void setup() { + user = UserTestFactory.create(); + userDetailService.setUser(user); + } + + @Test + void shouldSetUser() { + assertThat(userDetailService.getUser(UserTestFactory.USER_ID)).isNotNull(); + } + + } + + @Nested + class TestExpiringCodeCache { + + FakeTicker ticker = new FakeTicker(); + + @BeforeEach + void setup() { + user = UserTestFactory.create(); + + userDetailService.addUser(user); + + ticker.advance(2, TimeUnit.SECONDS); + } + + } + + @Nested + class TestUserMapCleanUp { + + private User expiredUser; + private User validUser; + + @BeforeEach + void setup() { + validUser = UserTestFactory.create(); + userDetailService.setUser(validUser); + expiredUser = UserTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .build(); + } + + @Test + void shouldRemoveExpiredUser() { + userDetailService.userCleanUp(); + + assertThat(userDetailService.getUser(expiredUser.getId())).isNull(); + } + + @Test + void shouldNotRemoveValidUser() { + userDetailService.userCleanUp(); + + assertThat(userDetailService.getUser(validUser.getId())).isNotNull(); + } + + @Test + void shouldRemoveOnLogout() { + userDetailService.logout(validUser); + + assertThat(userDetailService.getUser(validUser.getId())).isNull(); + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithmTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithmTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b37b9e4c86befd46819d69f43be1cae673849394 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SHA256withRSAAndMGF1SignatureAlgorithmTest.java @@ -0,0 +1,46 @@ +package de.ozgcloud.fachstelle.security; + +import static de.ozgcloud.fachstelle.security.SHA256withRSAAndMGF1SignatureAlgorithm.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.opensaml.xmlsec.algorithm.AlgorithmDescriptor; + +class SHA256withRSAAndMGF1SignatureAlgorithmTest { + + @Test + void shouldGetKey() { + var algorithm = new SHA256withRSAAndMGF1SignatureAlgorithm(); + + assertThat(algorithm.getKey()).isEqualTo(RSA_ALGORITHM_ID); + } + + @Test + void shouldGetURI() { + var algorithm = new SHA256withRSAAndMGF1SignatureAlgorithm(); + + assertThat(algorithm.getURI()).isEqualTo(RSA_SHA256_MGF1_ALGORITHM_URL); + } + + @Test + void shouldGetType() { + var algorithm = new SHA256withRSAAndMGF1SignatureAlgorithm(); + + assertThat(algorithm.getType()).isEqualTo(AlgorithmDescriptor.AlgorithmType.Signature); + } + + @Test + void shouldGetJCAAlgorithmID() { + var algorithm = new SHA256withRSAAndMGF1SignatureAlgorithm(); + + assertThat(algorithm.getJCAAlgorithmID()).isEqualTo(RSA_SHA256_MGF1_ALGORITHM_ID); + } + + @Test + void shouldGetDigest() { + var algorithm = new SHA256withRSAAndMGF1SignatureAlgorithm(); + + assertThat(algorithm.getDigest()).isEqualTo(SHA256_ALGORITHM_ID); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2DecrypterTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2DecrypterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cccb4fcc9f01f2bec0111e17177267cee3fd6043 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2DecrypterTest.java @@ -0,0 +1,130 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FileUtils; +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.junit.jupiter.MockitoExtension; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.xmlsec.encryption.support.DecryptionException; +import org.springframework.core.io.ClassPathResource; + +@ExtendWith(MockitoExtension.class) +class Saml2DecrypterTest { + + private Saml2Decrypter saml2Decrypter; + private String samlResponse; + + @BeforeEach + void init() throws NoSuchFieldException, IllegalAccessException, InitializationException { + saml2Decrypter = new Saml2Decrypter(); + + var privateKeyLocationField = Saml2Decrypter.class.getDeclaredField("decryptionPrivateKeyLocation"); + privateKeyLocationField.setAccessible(true); + privateKeyLocationField.set(saml2Decrypter, new ClassPathResource("mujina-test.key")); + + var certificateLocationField = Saml2Decrypter.class.getDeclaredField("decryptionCertificateLocation"); + certificateLocationField.setAccessible(true); + certificateLocationField.set(saml2Decrypter, new ClassPathResource("mujina-test.crt")); + + saml2Decrypter.init(); + } + + @Nested + class TestInit { + + @Test + void shouldHaveDecrypter() { + assertThat(saml2Decrypter.getDecrypter()).isNotNull(); + } + + } + + @Nested + class TestGetDecryptedAttribute { + + private static final String ATTRIBUTE_NAME = "TestAttributeName"; + + @Mock + private UnmarshallerFactory unmarshallerFactory; + + @Mock + private ResponseUnmarshaller responseUnmarshaller; + + @Mock + private XMLObjectProviderRegistry providerRegistry; + + @Mock + private Response response; + + @Mock + private EncryptedAssertion encryptedAssertion; + + @Mock + private Decrypter decrypter; + + @Mock + private AttributeStatement attributeStatement; + + @Mock + private Attribute attribute; + + @Mock + private Assertion assertion; + + @BeforeEach + void init() throws IOException, UnmarshallingException, DecryptionException { + samlResponse = FileUtils.readFileToString(new File("src/test/resources/SamlResponse.xml"), Charset.defaultCharset()); + + when(decrypter.decrypt(encryptedAssertion)).thenReturn(assertion); + when(attribute.getName()).thenReturn(ATTRIBUTE_NAME); + when(assertion.getStatements()).thenReturn(List.of(attributeStatement, attributeStatement)); + when(attributeStatement.getAttributes()).thenReturn(Collections.singletonList(attribute)); + when(response.getEncryptedAssertions()).thenReturn(Collections.singletonList(encryptedAssertion)); + when(response.getAssertions()).thenReturn(new ArrayList<>(Collections.singletonList(assertion))); + + when(responseUnmarshaller.unmarshall(any())).thenReturn(response); + when(unmarshallerFactory.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME)).thenReturn(responseUnmarshaller); + when(providerRegistry.getUnmarshallerFactory()).thenReturn(unmarshallerFactory); + + saml2Decrypter.setDecrypter(decrypter); + } + + @Test + void shouldDecryptAttribute() { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(XMLObjectProviderRegistry.class)).thenReturn(providerRegistry); + + var attribute = saml2Decrypter.getDecryptedAttribute(samlResponse, ATTRIBUTE_NAME); + + assertThat(attribute).isNotNull(); + } + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2ParserTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2ParserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..18d5efb9e7a24586309eda9a196991855095a264 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/Saml2ParserTest.java @@ -0,0 +1,115 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.commons.io.FileUtils; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.w3c.dom.Element; + +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; + +@ExtendWith(MockitoExtension.class) +class Saml2ParserTest { + + @Nested + class TestGetParserPool { + + @Test + void shouldHaveParserPool() throws ComponentInitializationException { + assertThat(Saml2Parser.getParserPool()).isNotNull(); + } + + } + + @Nested + class TestGetResponseUnmarshaller { + + @Mock + private UnmarshallerFactory unmarshallerFactory; + + @Mock + private ResponseUnmarshaller responseUnmarshaller; + + @Mock + private XMLObjectProviderRegistry providerRegistry; + + @Test + void shouldHaveResponseUnmarshaller() { + when(unmarshallerFactory.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME)).thenReturn(responseUnmarshaller); + when(providerRegistry.getUnmarshallerFactory()).thenReturn(unmarshallerFactory); + + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(XMLObjectProviderRegistry.class)).thenReturn(providerRegistry); + + assertThat(Saml2Parser.getResponseUnmarshaller()).isNotNull(); + } + } + + } + + @Nested + class TestParse { + + @Mock + private UnmarshallerFactory unmarshallerFactory; + + @Mock + private ResponseUnmarshaller responseUnmarshaller; + + @Mock + private XMLObjectProviderRegistry providerRegistry; + + private String samlResponse; + + @BeforeEach + void init() throws IOException, UnmarshallingException { + samlResponse = FileUtils.readFileToString(new File("src/test/resources/SamlResponse.xml"), Charset.defaultCharset()); + + when(responseUnmarshaller.unmarshall(any())).thenReturn(mock(Response.class)); + when(unmarshallerFactory.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME)).thenReturn(responseUnmarshaller); + when(providerRegistry.getUnmarshallerFactory()).thenReturn(unmarshallerFactory); + } + + @Test + void shouldParseSamlToken() { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(XMLObjectProviderRegistry.class)).thenReturn(providerRegistry); + + assertThat(Saml2Parser.parse(samlResponse)).isNotNull(); + } + } + + @Test + void shouldCreateXmlDocument() throws UnmarshallingException { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(XMLObjectProviderRegistry.class)).thenReturn(providerRegistry); + + var xmlElementArgumentCaptor = ArgumentCaptor.forClass(Element.class); + + Saml2Parser.parse(samlResponse); + + verify(responseUnmarshaller).unmarshall(xmlElementArgumentCaptor.capture()); + assertThat(xmlElementArgumentCaptor.getValue().getTagName()).isEqualTo("saml2p:Response"); + } + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandlerTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandlerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6993ad343c02d2c744ec9efe191888fb39e9c85a --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityExceptionHandlerTest.java @@ -0,0 +1,72 @@ +package de.ozgcloud.fachstelle.security; + +import static de.ozgcloud.fachstelle.security.SecurityExceptionHandler.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SecurityExceptionHandlerTest { + + SecurityExceptionHandler exceptionHandler = new SecurityExceptionHandler(); + @Mock + HttpServletRequest request; + @Mock + SecurityException exception; + @Mock + HttpSession session; + + @BeforeEach + void setUp() { + when(request.getSession()).thenReturn(session); + } + + @Test + void shouldCreateNotFoundView() { + var res = exceptionHandler.handleSecurityException(request, exception); + + assertThat(res).isNotNull(); + } + + @Test + void shouldHaveMessage() { + when(exception.getMessage()).thenReturn("message"); + + var res = exceptionHandler.handleSecurityException(request, exception); + + assertThat(res.getModel()).containsEntry("exception", "message"); + } + + @Test + void shouldHaveUrl() { + var val = new StringBuffer("http://localhost:8080/"); + when(request.getRequestURL()).thenReturn(val); + + var res = exceptionHandler.handleSecurityException(request, exception); + + assertThat(res.getModel()).containsEntry("url", val); + } + + @Test + void shouldHaveViewName() { + var res = exceptionHandler.handleSecurityException(request, exception); + + assertThat(res.getViewName()).isEqualTo(DEFAULT_ERROR_VIEW); + } + + @Test + void shouldInvalidateSession() { + exceptionHandler.handleSecurityException(request, exception); + + verify(session).invalidate(); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityProviderTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityProviderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6d8a6df9de3d9e57ff16a3641e970982441088b2 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/SecurityProviderTest.java @@ -0,0 +1,76 @@ +package de.ozgcloud.fachstelle.security; + +import static de.ozgcloud.fachstelle.security.SHA256withRSAAndMGF1SignatureAlgorithm.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.xmlsec.algorithm.AlgorithmRegistry; + +public class SecurityProviderTest { + + private static final String BOUNCY_CASTLE_PROVIDER_ID = "BC"; + + private final SecurityProvider securityProvider = new SecurityProvider(); + + @Nested + class TestAfterPropertiesSet { + + @BeforeEach + void init() { + Security.removeProvider(BOUNCY_CASTLE_PROVIDER_ID); + } + + @Test + void shouldNotAddBouncyCastleProvider() { + assertThat(Security.getProvider(BOUNCY_CASTLE_PROVIDER_ID)).isNull(); + } + + @Test + void shouldAddBouncyCastleProvider() { + securityProvider.afterPropertiesSet(); + + assertThat(Security.getProvider(BOUNCY_CASTLE_PROVIDER_ID)).isInstanceOf(BouncyCastleProvider.class); + } + + @Test + void shouldHandleAlgorithmRegistryIsNull() { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(AlgorithmRegistry.class)).thenReturn(null); + + securityProvider.afterPropertiesSet(); + + assertThat(ConfigurationService.get(AlgorithmRegistry.class)).isNull(); + } + } + + @Test + void shouldNotRegisterSignatureAlgorithms() { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(AlgorithmRegistry.class)).thenReturn(new AlgorithmRegistry()); + + assertThat(ConfigurationService.get(AlgorithmRegistry.class).get(RSA_SHA256_MGF1_ALGORITHM_URL)).isNull(); + } + } + + @Test + void shouldRegisterSignatureAlgorithms() { + try (var configService = mockStatic(ConfigurationService.class)) { + configService.when(() -> ConfigurationService.get(AlgorithmRegistry.class)).thenReturn(new AlgorithmRegistry()); + + securityProvider.afterPropertiesSet(); + + assertThat(ConfigurationService.get(AlgorithmRegistry.class).get(RSA_SHA256_MGF1_ALGORITHM_URL)).isInstanceOf( + SHA256withRSAAndMGF1SignatureAlgorithm.class); + } + } + + } + +} diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandlerTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandlerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a0179b2bae4501b002aa39063d47cf385ab4078f --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UrlAuthenticationSuccessHandlerTest.java @@ -0,0 +1,174 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +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.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +@ExtendWith(MockitoExtension.class) +class UrlAuthenticationSuccessHandlerTest { + + private static final String HTTP_TEST = "http://test"; + + private UrlAuthenticationSuccessHandler successHandler; + + @Mock + private InMemoryUserDetailService userService; + + @Mock + private UserMapper userMapper; + + @Nested + class TestOnSuccess { + + UrlAuthenticationSuccessHandler handler; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private Saml2Authentication authentication; + + @BeforeEach + void setup() { + handler = spy(new UrlAuthenticationSuccessHandler(userService, userMapper)); + Collection<? extends GrantedAuthority> grantedAuthorities = List.of(new DefaultRole()); + doReturn(grantedAuthorities).when(authentication).getAuthorities(); + } + + @Test + void shouldCallHandle() throws IOException { + handler.onAuthenticationSuccess(request, response, authentication); + + verify(handler).handle(any(), any(), any()); + } + + @Test + void shouldCallClearAuthenticationAttributes() throws IOException { + handler.onAuthenticationSuccess(request, response, authentication); + + verify(handler).clearAuthenticationAttributes(any()); + } + + } + + @Nested + class TestWithAuthentication { + + @Mock + private Authentication authentication; + + @Test + void shouldDetermineTargetUrlForRole() { + Collection<? extends GrantedAuthority> grantedAuthorities = List.of(new DefaultRole()); + doReturn(grantedAuthorities).when(authentication).getAuthorities(); + + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + + var url = successHandler.determineTargetUrl(authentication); + + assertThat(url).isEqualTo("/preregister"); + } + + @Test + void shouldDetermineTargetUrlMissingRole() { + Collection<? extends GrantedAuthority> grantedAuthorities = List.of(); + doReturn(grantedAuthorities).when(authentication).getAuthorities(); + + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> successHandler.determineTargetUrl(authentication)) + .withMessage("Invalid role! User is missing role ROLE_USER"); + } + + } + + @Nested + class TestClearLoginSession { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpSession session; + + @Test + void shouldClearSession() { + when(request.getSession(anyBoolean())).thenReturn(session); + + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + + successHandler.clearAuthenticationAttributes(request); + + verify(session).removeAttribute(any()); + } + + @Test + void shouldHandleNullSession() { + assertThatNoException().isThrownBy(() -> { + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + successHandler.clearAuthenticationAttributes(request); + }); + + } + + } + + @Nested + class TestHandle { + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private Authentication authentication; + + @BeforeEach + void setup() { + Collection<? extends GrantedAuthority> grantedAuthorities = List.of(new DefaultRole()); + doReturn(grantedAuthorities).when(authentication).getAuthorities(); + } + + @Test + void shouldHandle() throws IOException { + when(request.getContextPath()).thenReturn(HTTP_TEST); + when(response.isCommitted()).thenReturn(Boolean.FALSE); + when(response.encodeRedirectURL(anyString())).thenReturn(HTTP_TEST + "/preregister"); + + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + successHandler.handle(request, response, authentication); + + verify(response).encodeRedirectURL(HTTP_TEST + "/preregister"); + verify(response).sendRedirect(startsWith(HTTP_TEST + "/preregister")); + } + + @Test + void shouldHandleCommitted() throws IOException { + when(response.isCommitted()).thenReturn(Boolean.TRUE); + successHandler = new UrlAuthenticationSuccessHandler(userService, userMapper); + successHandler.handle(request, response, authentication); + + verify(response, never()).sendRedirect(anyString()); + } + + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserAttributeProviderTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserAttributeProviderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..13ad3360b608ee0b9f6a207687fc25f5ead4dfe7 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserAttributeProviderTest.java @@ -0,0 +1,145 @@ +package de.ozgcloud.fachstelle.security; + +import static de.ozgcloud.fachstelle.security.UserAttributeProvider.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.Attribute; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.w3c.dom.Element; + +@ExtendWith(MockitoExtension.class) +class UserAttributeProviderTest { + + private static final String UNKNOWN_ATTRIBUTE_KEY = "UnknownAttributeKey"; + + @Spy + @InjectMocks + private UserAttributeProvider provider; + + @Mock + private Saml2Decrypter saml2Decrypter; + + private DefaultSaml2AuthenticatedPrincipal principal; + + @BeforeEach + void setup() { + Map<String, List<Object>> attributes = new HashMap<>(); + attributes.put(UserAttributeProvider.MUK_FIRMENNAME_KEY, List.of(UserTestFactory.COMPANY_NAME)); + attributes.put(UserAttributeProvider.MUK_RECHTSFORM_KEY, List.of(UserTestFactory.LEGAL_FORM)); + attributes.put(UserAttributeProvider.MUK_RECHTSFORM_TEXT_KEY, List.of(UserTestFactory.LEGAL_FORM_TEXT)); + attributes.put(UserAttributeProvider.MUK_REGISTERNUMMER_KEY, List.of(UserTestFactory.REGISTER_NUMBER)); + attributes.put(UserAttributeProvider.MUK_REGISTERART_KEY, List.of(UserTestFactory.REGISTER_TYPE)); + attributes.put(UserAttributeProvider.MUK_EMAIL_ADRESSE_KEY, List.of(UserTestFactory.EMAIL_ADDRESS)); + attributes.put(UserAttributeProvider.MUK_ADRESSE_KEY, List.of(UserTestFactory.ADDRESS)); + attributes.put(UserAttributeProvider.MUK_VERTRAUENSNIVEAU_KEY, List.of(UserTestFactory.TRUST_LEVEL)); + attributes.put(UNKNOWN_ATTRIBUTE_KEY, List.of(UserTestFactory.USER_ID)); + + principal = new DefaultSaml2AuthenticatedPrincipal(UserTestFactory.USER_ID, attributes); + } + + @Test + void shouldGetCompanyName() { + assertThat(provider.getCompanyName(principal)).isEqualTo(UserTestFactory.COMPANY_NAME); + } + + @Test + void shouldGetLegalForm() { + assertThat(provider.getLegalForm(principal)).isEqualTo(UserTestFactory.LEGAL_FORM); + } + + @Test + void shouldGetLegalFormText() { + assertThat(provider.getLegalFormText(principal)).isEqualTo(UserTestFactory.LEGAL_FORM_TEXT); + } + + @Test + void shouldGetRegisterNumber() { + assertThat(provider.getRegisterNumber(principal)).isEqualTo(UserTestFactory.REGISTER_NUMBER); + } + + @Test + void shouldGetRegisterType() { + assertThat(provider.getRegisterType(principal)).isEqualTo(UserTestFactory.REGISTER_TYPE); + } + + @Test + void shouldGetEmailAddress() { + assertThat(provider.getEmailAddress(principal)).isEqualTo(UserTestFactory.EMAIL_ADDRESS); + } + + @Test + void shouldGetAddress() { + var addressNode = mock(Attribute.class); + var strasseNode = createMockXmlObject(SAML_XML_STRASSE_NODE_NAME, UserTestFactory.STREET); + var hausnummerNode = createMockXmlObject(SAML_XML_HAUSNUMMER_NODE_NAME, UserTestFactory.HOUSE_NUMBER); + var plzNode = createMockXmlObject(SAML_XML_PLZ_NODE_NAME, UserTestFactory.POSTAL_CODE); + var ortNode = createMockXmlObject(SAML_XML_ORT_NODE_NAME, UserTestFactory.CITY); + var landNode = createMockXmlObject(SAML_XML_LAND_NODE_NAME, UserTestFactory.COUNTRY); + var attributeValue = mock(XMLObject.class); + + when(attributeValue.getOrderedChildren()).thenReturn(List.of(strasseNode, hausnummerNode, plzNode, ortNode, landNode)); + when(addressNode.getAttributeValues()).thenReturn(List.of(attributeValue)); + when(saml2Decrypter.getDecryptedAttribute(anyString(), anyString())).thenReturn(addressNode); + + assertThat(provider.getAddress("")).isEqualTo(UserTestFactory.ADDRESS); + } + + @Test + void shouldHaveNullAddress() { + var addressNode = mock(Attribute.class); + var attributeValue = mock(XMLObject.class); + + when(saml2Decrypter.getDecryptedAttribute(anyString(), anyString())).thenReturn(addressNode); + when(attributeValue.getOrderedChildren()).thenReturn(Collections.emptyList()); + when(addressNode.getAttributeValues()).thenReturn(List.of(attributeValue)); + + assertThat(provider.getAddress("")).isNull(); + } + + @Test + void shouldHaveNullAddressDueToException() { + when(saml2Decrypter.getDecryptedAttribute(anyString(), anyString())).thenThrow(new Saml2Exception("Decryption error")); + + assertThat(provider.getAddress("")).isNull(); + } + + @Test + void shouldTrustLevel() { + assertThat(provider.getTrustLevel(principal)).isEqualTo(UserTestFactory.TRUST_LEVEL); + } + + @Test + void shouldGetUnknownAttributes() { + assertThat(provider.getUnknownAttributes(principal)).hasSize(1); + } + + private XMLObject createMockXmlObject(String nodeName, String textContent) { + var node = mock(XMLObject.class); + var element = mock(Element.class); + var elementQName = mock(QName.class); + when(elementQName.getLocalPart()).thenReturn(nodeName); + when(node.getElementQName()).thenReturn(elementQName); + when(element.getTextContent()).thenReturn(textContent); + when(node.getDOM()).thenReturn(element); + + return node; + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserMapperTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a276c8267e3e08f57d71fa83b6a20de39a1369d1 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserMapperTest.java @@ -0,0 +1,125 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +@ExtendWith(MockitoExtension.class) +class UserMapperTest { + + @Spy + @InjectMocks + private UserMapper userMapper; + + @Mock + private UserAttributeProvider userAttributeProvider; + + private Saml2Authentication auth; + + @BeforeEach + void setup() { + auth = mock(Saml2Authentication.class); + var principal = new DefaultSaml2AuthenticatedPrincipal(UserTestFactory.USER_ID, Collections.emptyMap()); + when(auth.getPrincipal()).thenReturn(principal); + when(auth.getSaml2Response()).thenReturn(""); + + when(userAttributeProvider.getCompanyName(principal)).thenReturn(UserTestFactory.COMPANY_NAME); + when(userAttributeProvider.getLegalForm(principal)).thenReturn(UserTestFactory.LEGAL_FORM); + when(userAttributeProvider.getLegalFormText(principal)).thenReturn(UserTestFactory.LEGAL_FORM_TEXT); + when(userAttributeProvider.getRegisterNumber(principal)).thenReturn(UserTestFactory.REGISTER_NUMBER); + when(userAttributeProvider.getRegisterType(principal)).thenReturn(UserTestFactory.REGISTER_TYPE); + when(userAttributeProvider.getEmailAddress(principal)).thenReturn(UserTestFactory.EMAIL_ADDRESS); + when(userAttributeProvider.getAddress("")).thenReturn(UserTestFactory.ADDRESS); + when(userAttributeProvider.getTrustLevel(principal)).thenReturn(UserTestFactory.TRUST_LEVEL); + when(userAttributeProvider.getUnknownAttributes(principal)).thenReturn(Collections.emptyList()); + } + + @Test + void shouldGetId() { + var user = userMapper.map(auth); + + assertThat(user.getId()).isEqualTo(UserTestFactory.USER_ID); + } + + @Test + void shouldGetUsername() { + var user = userMapper.map(auth); + + assertThat(user.getUsername()).isEqualTo(UserTestFactory.USER_NAME); + } + + @Test + void shouldGetCompanyName() { + var user = userMapper.map(auth); + + assertThat(user.getCompanyName()).isEqualTo(UserTestFactory.COMPANY_NAME); + } + + @Test + void shouldGetLegalForm() { + var user = userMapper.map(auth); + + assertThat(user.getLegalForm()).isEqualTo(UserTestFactory.LEGAL_FORM); + } + + @Test + void shouldGetLegalFormText() { + var user = userMapper.map(auth); + + assertThat(user.getLegalFormText()).isEqualTo(UserTestFactory.LEGAL_FORM_TEXT); + } + + @Test + void shouldGetRegisterNumber() { + var user = userMapper.map(auth); + + assertThat(user.getRegisterNumber()).isEqualTo(UserTestFactory.REGISTER_NUMBER); + } + + @Test + void shouldGetRegisterType() { + var user = userMapper.map(auth); + + assertThat(user.getRegisterType()).isEqualTo(UserTestFactory.REGISTER_TYPE); + } + + @Test + void shouldGetEmailAddress() { + var user = userMapper.map(auth); + + assertThat(user.getEmailAddress()).isEqualTo(UserTestFactory.EMAIL_ADDRESS); + } + + @Test + void shouldGetAddress() { + var user = userMapper.map(auth); + + assertThat(user.getAddress()).isEqualTo(UserTestFactory.ADDRESS); + } + + @Test + void shouldGetTrustLevel() { + var user = userMapper.map(auth); + + assertThat(user.getTrustLevel()).isEqualTo(UserTestFactory.TRUST_LEVEL); + } + + @Test + void shouldGetUnknownAttributes() { + var user = userMapper.map(auth); + + assertThat(user.getUnknownAttributes()).isEmpty(); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cf7ad0ae5b93d41395e5e5ef1b05a7baca2cdd5d --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTest.java @@ -0,0 +1,17 @@ +package de.ozgcloud.fachstelle.security; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class UserTest { + + @Test + void getAuthorities() { + var user = UserTestFactory.create(); + + assertThat(user.getAuthorities()).isNotNull(); + assertThat(user.getAuthorities().iterator().next().getAuthority()).isEqualTo(DefaultRole.ROLE); + } + +} \ No newline at end of file diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTestFactory.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..43b6a17f65f611b9a697d0db9fa5d8fbb685fff1 --- /dev/null +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/security/UserTestFactory.java @@ -0,0 +1,45 @@ +package de.ozgcloud.fachstelle.security; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +public class UserTestFactory { + + public static final String USER_ID = UUID.randomUUID().toString(); + public static final String USER_NAME = USER_ID; + public static final String COMPANY_NAME = "Pauls Unternehmen"; + public static final String LEGAL_FORM = "123"; + public static final String LEGAL_FORM_TEXT = "AG"; + public static final String REGISTER_NUMBER = "123"; + public static final String REGISTER_TYPE = "ABC"; + public static final String EMAIL_ADDRESS = "paul.panter@test.com"; + public static final String STREET = "Musterstraße"; + public static final String HOUSE_NUMBER = "1"; + public static final String POSTAL_CODE = "11011"; + public static final String CITY = "Berlin"; + public static final String COUNTRY = "DE"; + public static final String ADDRESS = String.format("%s %s, %s %s, %s", STREET, HOUSE_NUMBER, POSTAL_CODE, CITY, COUNTRY); + public static final String TRUST_LEVEL = "substantial"; + public static final String SAML_TOKEN = "samlToken"; + + public static User create() { + return createBuilder().build(); + } + + static User.UserBuilder createBuilder() { + return new User.UserBuilder() + .id(USER_ID) + .username(USER_NAME) + .companyName(COMPANY_NAME) + .legalForm(LEGAL_FORM) + .legalFormText(LEGAL_FORM_TEXT) + .registerNumber(REGISTER_NUMBER) + .registerType(REGISTER_TYPE) + .emailAddress(EMAIL_ADDRESS) + .address(ADDRESS) + .trustLevel(TRUST_LEVEL) + .expirationDate(Instant.now().plus(4, ChronoUnit.HOURS)); + } + +} diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangControllerITCase.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangControllerITCase.java index 25cdc6be9e2d212f70e12a0b0052985db07e7fd3..0ed6913cbddbf71737d7f805e35b7994b986e914 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangControllerITCase.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangControllerITCase.java @@ -51,17 +51,19 @@ import lombok.SneakyThrows; @SpringBootTest @AutoConfigureMockMvc(addFilters = false, printOnlyOnFailure = false) @TestPropertySource(properties = { - "ozgcloud.fachstelle.logout-success-url=http://logout", - "ozgcloud.fachstelle.login-redirect-url=http://login", - "ozgcloud.fachstelle.cors=http://login;http://saml-idp", - "ozgcloud.fachstellen-proxy.base-url=http://proxy", - "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", - "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", - "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" + "ozgcloud.fachstelle.logout-success-url=http://logout", + "ozgcloud.fachstelle.login-redirect-url=http://login", + "ozgcloud.fachstelle.cors=http://login;http://saml-idp", + "ozgcloud.fachstellen-proxy.base-url=http://proxy", + "keycloak.auth-server-url=http://keycloak", + "keycloak.realm=fachstelle", + "spring.security.saml2.relyingparty.registration.muk.entity-id=http://mock-idp", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.signing.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].private-key-location=classpath:/mujina-test.key", + "spring.security.saml2.relyingparty.registration.muk.decryption.credentials[0].certificate-location=classpath:/mujina-test.crt", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.singlesignon.sign-request=false", + "spring.security.saml2.relyingparty.registration.muk.assertingparty.metadata-uri=classpath:/metadata.xml" }) class VorgangControllerITCase { @@ -144,10 +146,10 @@ class VorgangControllerITCase { @SneakyThrows private ResultActions performRequest() { return mockMvc.perform( - get(VorgangController.PATH + "/" + VorgangTestFactory.ID + "?" + PARAM_COLLABORATION_MANAGER_ADDRESS + "=" - + COLLABORATION_MANAGER_ADDRESS) - .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) - .with(csrf().asHeader())); + get(VorgangController.PATH + "/" + VorgangTestFactory.ID + "?" + PARAM_COLLABORATION_MANAGER_ADDRESS + "=" + + COLLABORATION_MANAGER_ADDRESS) + .contentType(MediaType.APPLICATION_JSON).characterEncoding(Charset.defaultCharset()) + .with(csrf().asHeader())); } } diff --git a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteServiceTest.java b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteServiceTest.java index f12a8ae760314c74eb3e9fe27d53883fbb813bd5..36b8bd4123f14609bf394c7e77155a2a7ed44ccc 100644 --- a/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteServiceTest.java +++ b/fachstelle-server/src/test/java/de/ozgcloud/fachstelle/vorgang/VorgangRemoteServiceTest.java @@ -23,10 +23,11 @@ */ package de.ozgcloud.fachstelle.vorgang; -import static de.ozgcloud.fachstelle.vorgang.VorgangRemoteService.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - +import de.ozgcloud.fachstelle.FachstellenProxyProperties; +import de.ozgcloud.fachstelle.common.errorhandling.SamlTokenNotFoundException; +import de.ozgcloud.fachstelle.proxy.CollaborationGrpcFindVorgangResponse; +import de.ozgcloud.fachstelle.security.CurrentUserService; +import de.ozgcloud.fachstelle.security.UserTestFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -41,103 +42,104 @@ import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; import org.springframework.web.util.UriTemplate; -import de.ozgcloud.fachstelle.FachstellenProxyProperties; -import de.ozgcloud.fachstelle.proxy.CollaborationGrpcFindVorgangResponse; +import static de.ozgcloud.fachstelle.vorgang.VorgangRemoteService.FIND_VORGANG_URI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class VorgangRemoteServiceTest { - @Spy - @InjectMocks - private VorgangRemoteService vorgangRemoteService; - - @Mock - private RestClient fachstellenProxyClient; + @Spy + @InjectMocks + private VorgangRemoteService vorgangRemoteService; - @Mock - private FachstellenProxyProperties fachstellenProxyProperties; + @Mock + private RestClient fachstellenProxyClient; - @Mock - private VorgangMapper vorgangMapper; + @Mock + private FachstellenProxyProperties fachstellenProxyProperties; - @Nested - class TestFindVorgang { + @Mock + private VorgangMapper vorgangMapper; - private static final String COLLABORATION_MANAGER_ADDRESS_HEADER = "some-header"; - private static final String COLLABORATION_MANAGER_ADDRESS = VorgangWithCollaborationManagerAddressTestFactory.COLLABORATION_MANAGER_ADDRESS; + @Nested + class TestFindVorgang { - private RestClient.RequestHeadersSpec headersSpec; - private RestClient.ResponseSpec responseSpec; - private ResponseEntity responseEntity; + private static final String COLLABORATION_MANAGER_ADDRESS_HEADER = "some-header"; + private static final String COLLABORATION_MANAGER_ADDRESS = VorgangWithCollaborationManagerAddressTestFactory.COLLABORATION_MANAGER_ADDRESS; - @BeforeEach - void init() { - var uriSpec = mock(RestClient.RequestHeadersUriSpec.class); - headersSpec = mock(RestClient.RequestHeadersSpec.class); - responseSpec = mock(RestClient.ResponseSpec.class); - responseEntity = mock(ResponseEntity.class); + private RestClient.RequestHeadersSpec headersSpec; + private RestClient.ResponseSpec responseSpec; + private ResponseEntity responseEntity; - doReturn(FIND_VORGANG_URI).when(vorgangRemoteService).buildFindVorgangUri(VorgangTestFactory.ID); - when(fachstellenProxyProperties.getCollaborationManagerAddressHeader()).thenReturn(COLLABORATION_MANAGER_ADDRESS_HEADER); - when(fachstellenProxyClient.get()).thenReturn(uriSpec); - when(uriSpec.uri(FIND_VORGANG_URI)).thenReturn(headersSpec); - when(headersSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(headersSpec); - when(headersSpec.header(COLLABORATION_MANAGER_ADDRESS_HEADER, COLLABORATION_MANAGER_ADDRESS)).thenReturn(headersSpec); - when(headersSpec.retrieve()).thenReturn(responseSpec); - } + @BeforeEach + void init() { + var uriSpec = mock(RestClient.RequestHeadersUriSpec.class); + headersSpec = mock(RestClient.RequestHeadersSpec.class); + responseSpec = mock(RestClient.ResponseSpec.class); + responseEntity = mock(ResponseEntity.class); - @Test - void shouldCallRestClient() { - when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); - when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); + doReturn(FIND_VORGANG_URI).when(vorgangRemoteService).buildFindVorgangUri(VorgangTestFactory.ID); + when(fachstellenProxyProperties.getCollaborationManagerAddressHeader()).thenReturn(COLLABORATION_MANAGER_ADDRESS_HEADER); + when(fachstellenProxyClient.get()).thenReturn(uriSpec); + when(uriSpec.uri(FIND_VORGANG_URI)).thenReturn(headersSpec); + when(headersSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(headersSpec); + when(headersSpec.header(COLLABORATION_MANAGER_ADDRESS_HEADER, COLLABORATION_MANAGER_ADDRESS)).thenReturn(headersSpec); + when(headersSpec.retrieve()).thenReturn(responseSpec); + } - vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); + @Test + void shouldCallRestClient() { + when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); + when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); - verify(fachstellenProxyClient).get(); - } + vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); - @Test - void shouldCallVorgangMapper() { - when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); - when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); + verify(fachstellenProxyClient).get(); + } - vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); + @Test + void shouldCallVorgangMapper() { + when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); + when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); - verify(vorgangMapper).toVorgang(any()); - } + vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); - @Test - void shouldGetResponse() { - when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); - when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); - when(vorgangMapper.toVorgang(any())).thenReturn(VorgangTestFactory.create()); + verify(vorgangMapper).toVorgang(any()); + } - var response = vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); + @Test + void shouldGetResponse() { + when(responseSpec.toEntity(CollaborationGrpcFindVorgangResponse.class)).thenReturn(responseEntity); + when(responseEntity.getBody()).thenReturn(new CollaborationGrpcFindVorgangResponse()); + when(vorgangMapper.toVorgang(any())).thenReturn(VorgangTestFactory.create()); - assertThat(response).isNotNull(); - } + var response = vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS); - @Test - void shouldThrowException() { - when(headersSpec.retrieve()).thenThrow(new RestClientException("some error")); + assertThat(response).isNotNull(); + } - assertThatExceptionOfType(RestClientException.class).isThrownBy( - () -> vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS)); - } + @Test + void shouldThrowException() { + when(headersSpec.retrieve()).thenThrow(new RestClientException("some error")); - } + assertThatExceptionOfType(RestClientException.class).isThrownBy( + () -> vorgangRemoteService.findVorgang(VorgangTestFactory.ID, COLLABORATION_MANAGER_ADDRESS)); + } - @Nested - class TestBuildFindVorgangUri { + } - @Test - void shouldReturnVorgangUri() { - final String expectedUri = new UriTemplate(FIND_VORGANG_URI).expand(VorgangTestFactory.ID, "--dummy-saml-token--").toString(); - final String uri = vorgangRemoteService.buildFindVorgangUri(VorgangTestFactory.ID); + @Nested + class TestBuildFindVorgangUri { - assertThat(uri).isEqualTo(expectedUri); - } + @Test + void shouldReturnVorgangUri() { + final String expectedUri = new UriTemplate(FIND_VORGANG_URI).expand(VorgangTestFactory.ID, VorgangRemoteService.DUMMY_SAML_TOKEN).toString(); + final String uri = vorgangRemoteService.buildFindVorgangUri(VorgangTestFactory.ID); - } + assertThat(uri).isEqualTo(expectedUri); + } + } -} \ No newline at end of file +}