diff --git a/admin-service/src/main/kotlin/com/michibaum/admin_service/security/SecurityConfiguration.kt b/admin-service/src/main/kotlin/com/michibaum/admin_service/security/SecurityConfiguration.kt index e78c77de..98dc910b 100644 --- a/admin-service/src/main/kotlin/com/michibaum/admin_service/security/SecurityConfiguration.kt +++ b/admin-service/src/main/kotlin/com/michibaum/admin_service/security/SecurityConfiguration.kt @@ -23,11 +23,8 @@ class SecurityConfiguration { ): SecurityFilterChain { return http .authorizeHttpRequests { - it.requestMatchers( - "/actuator", - "/actuator/**" - ).hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyRequest().authenticated() + it.anyRequest() + .hasAnyAuthority(Permissions.ADMIN_SERVICE.name) } .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } diff --git a/admin-service/src/main/resources/application.yml b/admin-service/src/main/resources/application.yml index a43f64fa..3ce2c9b0 100644 --- a/admin-service/src/main/resources/application.yml +++ b/admin-service/src/main/resources/application.yml @@ -72,4 +72,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/admin-service/src/test/kotlin/com/michibaum/admin_service/ActuatorIT.kt b/admin-service/src/test/kotlin/com/michibaum/admin_service/ActuatorIT.kt index 3c40e86d..ffa36d64 100644 --- a/admin-service/src/test/kotlin/com/michibaum/admin_service/ActuatorIT.kt +++ b/admin-service/src/test/kotlin/com/michibaum/admin_service/ActuatorIT.kt @@ -1,10 +1,13 @@ package com.michibaum.admin_service -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource 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.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* @AutoConfigureMockMvc @@ -15,102 +18,34 @@ import java.util.* class ActuatorIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.get() - .uri("/actuator") - .exchange() - .expectStatus() - .isUnauthorized + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/health") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/info") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk // TODO returns 302 redirect to / because of success authentication - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator/health") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk // TODO returns 302 redirect to / because of success authentication - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.get() - .uri("/actuator/info") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk // TODO returns 302 redirect to / because of success authentication + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/admin-service/src/test/resources/application.yml b/admin-service/src/test/resources/application.yml index 53906558..2a6849dc 100644 --- a/admin-service/src/test/resources/application.yml +++ b/admin-service/src/test/resources/application.yml @@ -69,4 +69,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ReactiveDelegateAuthenticationManager.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ReactiveDelegateAuthenticationManager.kt deleted file mode 100644 index 6079361a..00000000 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ReactiveDelegateAuthenticationManager.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.michibaum.authentication_library.security - -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.core.Authentication -import org.springframework.security.core.context.ReactiveSecurityContextHolder -import reactor.core.publisher.Mono - -class ReactiveDelegateAuthenticationManager(private val authenticationManagers: List): ReactiveAuthenticationManager { - override fun authenticate(authentication: Authentication?): Mono { - if (authentication == null) { - return Mono.empty() - } - - for(authManager in authenticationManagers){ - if(authManager.supports(authentication.javaClass)){ - val auth = authManager.authenticate(authentication) ?: return Mono.error(Exception("Empty authentication")) - ReactiveSecurityContextHolder.withAuthentication(auth) - return Mono.just(authentication) - } - } - - throw NoAuthenticationManagerException("No authentication Manager found for ${authentication::class}") - } - - - - -} \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ServletDelegateAuthenticationManager.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ServletDelegateAuthenticationManager.kt index 449a9e61..ef7ad319 100644 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ServletDelegateAuthenticationManager.kt +++ b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/ServletDelegateAuthenticationManager.kt @@ -1,6 +1,7 @@ package com.michibaum.authentication_library.security import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContextHolder @@ -10,14 +11,21 @@ class ServletDelegateAuthenticationManager(private val authenticationManagers: L throw Exception("Empty authentication") } + val auths = mutableListOf() for(authManager in authenticationManagers){ if(authManager.supports(authentication.javaClass)){ - val auth = authManager.authenticate(authentication) ?: throw Exception("Empty authentication") - SecurityContextHolder.getContext().authentication = auth - return auth + val auth = authManager.authenticate(authentication) ?: continue + auths.add(auth) } } - throw NoAuthenticationManagerException("No authentication Manager found for ${authentication::class}") + val authenticated = auths.filter { it.isAuthenticated } + + if(authenticated.size > 1 || authenticated.isEmpty()) { + throw BadCredentialsException("More than one, or none authentication was authenticated.") + } + + SecurityContextHolder.getContext().authentication = authenticated[0] + return authenticated[0] } } \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/BasicExchangeMatcher.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/BasicExchangeMatcher.kt deleted file mode 100644 index 12bc311b..00000000 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/BasicExchangeMatcher.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.michibaum.authentication_library.security.basic - -import org.springframework.http.HttpHeaders -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher -import org.springframework.web.server.ServerWebExchange -import reactor.core.publisher.Mono - -class BasicExchangeMatcher: ServerWebExchangeMatcher { - override fun matches(exchange: ServerWebExchange?): Mono { - val authHeader = exchange?.request?.headers?.getFirst(HttpHeaders.AUTHORIZATION) - if(authHeader != null && authHeader.startsWith("Basic ")) { - return ServerWebExchangeMatcher.MatchResult.match() - } - return ServerWebExchangeMatcher.MatchResult.notMatch() - } -} \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/netty/BasicAuthenticationConverter.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/netty/BasicAuthenticationConverter.kt deleted file mode 100644 index db3978d7..00000000 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/basic/netty/BasicAuthenticationConverter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.michibaum.authentication_library.security.basic.netty - -import com.michibaum.authentication_library.security.basic.BasicAuthentication -import com.michibaum.permission_library.Permissions -import org.springframework.http.HttpHeaders -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.userdetails.User -import org.springframework.security.core.userdetails.UserDetails -import org.springframework.security.web.server.authentication.ServerAuthenticationConverter -import org.springframework.web.server.ServerWebExchange -import reactor.core.publisher.Mono -import java.util.* - -class BasicAuthenticationConverter: ServerAuthenticationConverter { - override fun convert(exchange: ServerWebExchange): Mono { - return Mono.justOrEmpty(exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)) - .filter { header -> header.startsWith("Basic ") } - .map { header -> header.substring("Basic ".length) } - .map { token -> BasicAuthentication(createUserDetails(token)) } - } - - private fun createUserDetails(token: String): UserDetails { - val decoded = Base64.getDecoder().decode(token) - val usernameAndPassword = String(decoded).split(":") - return User.builder() - .username(usernameAndPassword[0]) - .authorities(SimpleGrantedAuthority(Permissions.ADMIN_SERVICE.name)) - .password(usernameAndPassword[1]) - .build() - } -} \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/JwtExchangeMatcher.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/JwtExchangeMatcher.kt deleted file mode 100644 index 0e08b3a4..00000000 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/JwtExchangeMatcher.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.michibaum.authentication_library.security.jwt - -import org.springframework.http.HttpHeaders -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher -import org.springframework.web.server.ServerWebExchange -import reactor.core.publisher.Mono - -class JwtExchangeMatcher: ServerWebExchangeMatcher { - override fun matches(exchange: ServerWebExchange?): Mono { - - val header = exchange?.request?.headers?.getFirst(HttpHeaders.AUTHORIZATION) - - if(header != null && header.startsWith("Bearer ")) { - return ServerWebExchangeMatcher.MatchResult.match() - } - - val cookie = exchange?.request?.cookies?.getFirst("jwt")?.value - - if(!cookie.isNullOrBlank()) { - return ServerWebExchangeMatcher.MatchResult.match() - } - - return ServerWebExchangeMatcher.MatchResult.notMatch() - - } -} \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/netty/JwtAuthenticationConverter.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/netty/JwtAuthenticationConverter.kt deleted file mode 100644 index cfb32df8..00000000 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/netty/JwtAuthenticationConverter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.michibaum.authentication_library.security.jwt.netty - -import com.michibaum.authentication_library.JwsWrapper -import com.michibaum.authentication_library.security.jwt.JwtAuthentication -import org.springframework.http.HttpHeaders -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.userdetails.User -import org.springframework.security.core.userdetails.UserDetails -import org.springframework.security.web.server.authentication.ServerAuthenticationConverter -import org.springframework.web.server.ServerWebExchange -import reactor.core.publisher.Mono - - -class JwtAuthenticationConverter: ServerAuthenticationConverter { - - override fun convert(exchange: ServerWebExchange?): Mono { - if(exchange == null) - return Mono.empty() - - val token = getToken(exchange) ?: return Mono.empty() - return Mono.just(JwtAuthentication(token, createUserDetails(token))) - } - - fun getToken(exchange: ServerWebExchange): String? { - val header = exchange.request?.headers?.getFirst(HttpHeaders.AUTHORIZATION) - - if(header != null && header.startsWith("Bearer ")) { - return header.substring("Bearer ".length) - } - - val cookie = exchange.request?.cookies?.getFirst("jwt")?.value - - if(!cookie.isNullOrBlank()) { - return cookie - } - - return null - } - - - private fun createUserDetails(token: String): UserDetails { - val username: String = JwsWrapper(token).getUsername() - return User.builder() - .username(username) - .authorities(createAuthorities(token)) - .password("") - .build() - } - - private fun createAuthorities(token: String): List { - return JwsWrapper(token).getPermissions().stream() - .map { permission -> "$permission" } - .map { permission: String? -> SimpleGrantedAuthority(permission) } - .toList() - } - -} \ No newline at end of file diff --git a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/servlet/JwtAuthenticationConverter.kt b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/servlet/JwtAuthenticationConverter.kt index d40ffe55..fd7b50e8 100644 --- a/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/servlet/JwtAuthenticationConverter.kt +++ b/authentication-library/src/main/kotlin/com/michibaum/authentication_library/security/jwt/servlet/JwtAuthenticationConverter.kt @@ -25,11 +25,6 @@ class JwtAuthenticationConverter: AuthenticationConverter { if(header != null && header.startsWith("Bearer ")) { return header.substring("Bearer ".length) } - - val cookie = request.cookies?.firstOrNull { cookie -> cookie.name == "jwt" }?.value - if(!cookie.isNullOrBlank()) { - return cookie - } return null } diff --git a/authentication-service/pom.xml b/authentication-service/pom.xml index 65507564..0d0fac46 100644 --- a/authentication-service/pom.xml +++ b/authentication-service/pom.xml @@ -21,7 +21,7 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-web @@ -112,8 +112,24 @@ test - com.h2database - h2 + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mariadb test diff --git a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/authentication/AuthenticationController.kt b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/authentication/AuthenticationController.kt index f3db16a1..8446f081 100644 --- a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/authentication/AuthenticationController.kt +++ b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/authentication/AuthenticationController.kt @@ -35,21 +35,8 @@ class AuthenticationController ( val jws = authenticationService.generateJWS(userDetailsDto)!! - val cookie = ResponseCookie.from("jwt", jws) - .httpOnly(true) - .maxAge(Duration.ofHours(8)) - .domain("michibaum.ch") - .secure(true) - .sameSite("Lax") - .build() - val responseBody = AuthenticationResponse(authenticationDto.username, jws) - return ResponseEntity.ok() - .headers { - it.set(HttpHeaders.SET_COOKIE, cookie.toString()) - it.set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") - } - .body(responseBody) + return ResponseEntity.ok().body(responseBody) } @PostMapping(value = ["/api/register"]) @@ -73,23 +60,6 @@ class AuthenticationController ( return ResponseEntity.status(HttpStatus.CONFLICT).body(responseBody) } - @PostMapping(value = ["/api/logout"]) - fun logout(): ResponseEntity { - val cookie = ResponseCookie.from("jwt") - .maxAge(0) - .httpOnly(true) - .maxAge(Duration.ofHours(8)) - .domain("michibaum.ch") - .secure(true) - .build() - return ResponseEntity.ok() - .headers { - it.set(HttpHeaders.SET_COOKIE, cookie.toString()) - it.set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true") - } - .build() - } - override fun publicKey(): PublicKeyDto { return authenticationService.publicKey } diff --git a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityBeansConfiguration.kt b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityBeansConfiguration.kt index 76f46819..2b821bf7 100644 --- a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityBeansConfiguration.kt +++ b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityBeansConfiguration.kt @@ -2,25 +2,23 @@ package com.michibaum.authentication_service.security import com.michibaum.authentication_library.AuthenticationClient import com.michibaum.authentication_library.PublicKeyDto -import com.michibaum.authentication_library.security.ReactiveDelegateAuthenticationManager +import com.michibaum.authentication_library.security.ServletAuthenticationFilter +import com.michibaum.authentication_library.security.ServletDelegateAuthenticationManager import com.michibaum.authentication_library.security.SpecificAuthenticationManager import com.michibaum.authentication_library.security.basic.BasicAuthenticationManager -import com.michibaum.authentication_library.security.basic.BasicExchangeMatcher import com.michibaum.authentication_library.security.basic.CredentialsValidator -import com.michibaum.authentication_library.security.basic.netty.BasicAuthenticationConverter +import com.michibaum.authentication_library.security.basic.servlet.BasicAuthenticationConverter import com.michibaum.authentication_library.security.jwt.JwsValidator import com.michibaum.authentication_library.security.jwt.JwtAuthenticationManager -import com.michibaum.authentication_library.security.jwt.JwtExchangeMatcher -import com.michibaum.authentication_library.security.jwt.netty.JwtAuthenticationConverter +import com.michibaum.authentication_library.security.jwt.servlet.JwtAuthenticationConverter import com.michibaum.authentication_service.authentication.AuthenticationService import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.AuthenticationConverter @Configuration class SecurityBeansConfiguration { - @Bean fun adminServiceCredentials(): AdminServiceCredentials = AdminServiceCredentials() @@ -55,39 +53,22 @@ class SecurityBeansConfiguration { BasicAuthenticationManager(credentialsValidator) @Bean - fun authenticationManager(specificAuthenticationManagers: List): ReactiveAuthenticationManager = - ReactiveDelegateAuthenticationManager(specificAuthenticationManagers) + fun authenticationManager(specificAuthenticationManagers: List): AuthenticationManager = + ServletDelegateAuthenticationManager(specificAuthenticationManagers) @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter = + fun jwtAuthenticationConverter(): AuthenticationConverter = JwtAuthenticationConverter() @Bean - fun basicAuthenticationConverter(): BasicAuthenticationConverter = + fun basicAuthenticationConverter(): AuthenticationConverter = BasicAuthenticationConverter() @Bean - fun jwtAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - jwtAuthenticationConverter: JwtAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(JwtExchangeMatcher()) - setServerAuthenticationConverter(jwtAuthenticationConverter) - } - - @Bean - fun basicAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - basicAuthenticationConverter: BasicAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(BasicExchangeMatcher()) - setServerAuthenticationConverter(basicAuthenticationConverter) - } - + fun authenticationFilter(authenticationManager: AuthenticationManager, authenticationConverters: List) = + ServletAuthenticationFilter(authenticationManager, authenticationConverters) } \ No newline at end of file diff --git a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityConfiguration.kt b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityConfiguration.kt index 44e779b5..795a8fee 100644 --- a/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityConfiguration.kt +++ b/authentication-service/src/main/kotlin/com/michibaum/authentication_service/security/SecurityConfiguration.kt @@ -1,45 +1,41 @@ package com.michibaum.authentication_service.security +import com.michibaum.authentication_library.security.ServletAuthenticationFilter import com.michibaum.permission_library.Permissions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity -import org.springframework.security.config.web.server.SecurityWebFiltersOrder -import org.springframework.security.config.web.server.ServerHttpSecurity -import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec -import org.springframework.security.web.server.SecurityWebFilterChain -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter @Configuration -@EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableWebSecurity +@EnableMethodSecurity class SecurityConfiguration { @Bean fun securityFilterChain( - http: ServerHttpSecurity, - jwtAuthenticationWebFilter: AuthenticationWebFilter, - basicAuthenticationWebFilter: AuthenticationWebFilter, - ): SecurityWebFilterChain { + http: HttpSecurity, + authenticationFilter: ServletAuthenticationFilter + ): SecurityFilterChain { return http - .authorizeExchange { exchanges: AuthorizeExchangeSpec -> - exchanges - .pathMatchers( + .authorizeHttpRequests { + it + .requestMatchers( "/api/authenticate", "/api/getAuthDetails", - "/api/logout", "/api/register" ).permitAll() - .pathMatchers( + .requestMatchers( "/actuator", "/actuator/**" ).hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyExchange().authenticated() + .anyRequest().authenticated() } - .addFilterAt(basicAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } .formLogin { formLoginSpec -> formLoginSpec.disable() } .csrf { csrfSpec -> csrfSpec.disable() } diff --git a/authentication-service/src/main/resources/application.yml b/authentication-service/src/main/resources/application.yml index 8b50a456..000d268a 100644 --- a/authentication-service/src/main/resources/application.yml +++ b/authentication-service/src/main/resources/application.yml @@ -65,4 +65,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/ActuatorIT.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/ActuatorIT.kt index c24b735d..b209b5c0 100644 --- a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/ActuatorIT.kt +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/ActuatorIT.kt @@ -1,116 +1,52 @@ package com.michibaum.authentication_service -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest(properties = [ "spring.boot.admin.client.username=someUsername", "spring.boot.admin.client.password=somePasswööörd" ]) +@TestcontainersConfiguration class ActuatorIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.get() - .uri("/actuator") - .exchange() - .expectStatus() - .isUnauthorized + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/health") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/info") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator/health") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.get() - .uri("/actuator/info") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/AuthenticationServiceApplicationIT.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/AuthenticationServiceApplicationIT.kt index c1679695..b3773836 100644 --- a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/AuthenticationServiceApplicationIT.kt +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/AuthenticationServiceApplicationIT.kt @@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext @SpringBootTest +@TestcontainersConfiguration class AuthenticationServiceApplicationIT { @Autowired diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/FlywayClearDatabaseJunitExtension.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/FlywayClearDatabaseJunitExtension.kt new file mode 100644 index 00000000..2338c82f --- /dev/null +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/FlywayClearDatabaseJunitExtension.kt @@ -0,0 +1,18 @@ +package com.michibaum.authentication_service + +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + + +class FlywayClearDatabaseJunitExtension: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext?) { + if(context == null) throw IllegalArgumentException("Context must not be null") + + val flyway = SpringExtension.getApplicationContext(context) + .getBean(Flyway::class.java) + flyway.clean() + flyway.migrate() + } +} \ No newline at end of file diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersConfiguration.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersConfiguration.kt new file mode 100644 index 00000000..ebd59f5e --- /dev/null +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersConfiguration.kt @@ -0,0 +1,12 @@ +package com.michibaum.authentication_service + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.annotation.Import +import org.springframework.test.context.TestPropertySource + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(value = [FlywayClearDatabaseJunitExtension::class]) +@Import(value = [TestcontainersTestConfiguration::class]) +@TestPropertySource(properties = ["spring.flyway.clean-disabled=false"]) +annotation class TestcontainersConfiguration() diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersTestConfiguration.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersTestConfiguration.kt new file mode 100644 index 00000000..9a286a36 --- /dev/null +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/TestcontainersTestConfiguration.kt @@ -0,0 +1,18 @@ +package com.michibaum.authentication_service + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.MariaDBContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersTestConfiguration { + + @Bean + @ServiceConnection + fun mariaDbContainer(): MariaDBContainer<*> { + return MariaDBContainer(DockerImageName.parse("mariadb:10.11")) + } + +} \ No newline at end of file diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationControllerIT.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationControllerIT.kt index 3d67a2d5..50f5eaaf 100644 --- a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationControllerIT.kt +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationControllerIT.kt @@ -1,33 +1,33 @@ package com.michibaum.authentication_service.authentication +import com.michibaum.authentication_service.TestcontainersConfiguration import org.junit.jupiter.api.Test 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.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc +@TestcontainersConfiguration class AuthenticationControllerIT { @Autowired - lateinit var client: WebTestClient + lateinit var mockMvc: MockMvc @Test fun `getAuthDetails endpoint returns result`() { - client.get() - .uri("/api/getAuthDetails") - .exchange() - .expectAll( - {it.expectStatus().isOk}, - {it.expectBody() - .jsonPath("$.algorithm").isNotEmpty - .jsonPath("$.key").isNotEmpty - } + mockMvc.perform(get("/api/getAuthDetails")) + .andExpectAll( + status().isOk, + jsonPath("$.algorithm").isNotEmpty, + jsonPath("$.key").isNotEmpty ) - //.expectBody().consumeWith(System.out::println) } diff --git a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationServiceIT.kt b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationServiceIT.kt index 288c7ee5..fc857cee 100644 --- a/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationServiceIT.kt +++ b/authentication-service/src/test/kotlin/com/michibaum/authentication_service/authentication/AuthenticationServiceIT.kt @@ -2,6 +2,7 @@ package com.michibaum.authentication_service.authentication import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm +import com.michibaum.authentication_service.TestcontainersConfiguration import com.michibaum.usermanagement_library.UserDetailsDto import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test @@ -13,6 +14,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @SpringBootTest +@TestcontainersConfiguration class AuthenticationServiceIT { @Autowired diff --git a/authentication-service/src/test/resources/application.yml b/authentication-service/src/test/resources/application.yml index 295c24ae..ae264584 100644 --- a/authentication-service/src/test/resources/application.yml +++ b/authentication-service/src/test/resources/application.yml @@ -20,17 +20,6 @@ spring: lazy-attributes-resolution: true main: allow-bean-definition-overriding: true - datasource: - username: sa - password: password - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:authentication-db - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - globally_quoted_identifiers: true eureka: instance: @@ -64,4 +53,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/chess-service/pom.xml b/chess-service/pom.xml index 6c9b5818..9376447d 100644 --- a/chess-service/pom.xml +++ b/chess-service/pom.xml @@ -22,13 +22,14 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-web org.springframework.boot spring-boot-starter-security + org.springframework.boot spring-boot-starter-validation @@ -79,23 +80,6 @@ true - - - - - - - - - - - - - - - - - com.michibaum @@ -130,13 +114,30 @@ test - com.h2database - h2 + org.wiremock + wiremock-standalone test + - org.wiremock - wiremock-standalone + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mariadb test diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/ApiService.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/ApiService.kt index 9458d77a..874e23e7 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/ApiService.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/ApiService.kt @@ -27,6 +27,7 @@ class ApiService( ChessPlatform.CHESSCOM -> chesscomApiService.getStats(account) ChessPlatform.LICHESS -> lichessApiService.getStats(account) ChessPlatform.FIDE -> TODO() + ChessPlatform.FREESTYLE -> TODO() } } @@ -36,6 +37,7 @@ class ApiService( ChessPlatform.CHESSCOM -> chesscomApiService.getGames(account) ChessPlatform.LICHESS -> lichessApiService.getGames(account) ChessPlatform.FIDE -> TODO() + ChessPlatform.FREESTYLE -> TODO() } if(result is Success){ diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/chesscom/ApiServiceImpl.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/chesscom/ApiServiceImpl.kt index c5a2e7ce..7a217827 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/chesscom/ApiServiceImpl.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/chesscom/ApiServiceImpl.kt @@ -29,8 +29,7 @@ class ApiServiceImpl( .uri("/pub/player/{0}", username) .accept(MediaType.APPLICATION_JSON) .retrieve() - .bodyToMono(ChesscomAccountDto::class.java) // TODO onError - .block() + .body(ChesscomAccountDto::class.java) // TODO onError } catch (throwable: Throwable){ return Exception("Exception chesscom findUser with username=$username", throwable) } @@ -48,8 +47,7 @@ class ApiServiceImpl( .uri("/pub/player/{0}/stats", account.username) .accept(MediaType.APPLICATION_JSON) .retrieve() - .bodyToMono(ChesscomStatsDto::class.java) // TODO onError - .block() + .body(ChesscomStatsDto::class.java) // TODO onError } catch (throwable: Throwable){ return Exception("Exception chesscom getStats with username=${account.username}", throwable) } @@ -82,9 +80,8 @@ class ApiServiceImpl( .uri("/pub/player/{0}/games/{1}/{2}", account.username, currentYear, currentMonth) .accept(MediaType.APPLICATION_JSON) .retrieve() - .bodyToMono(Gamesresult::class.java) - .mapNotNull { it.games } - .block() + .body(Gamesresult::class.java) + ?.games } catch (throwable: Throwable){ return Exception("Exception chesscom getGames with username=${account.username} and year=$currentYear and month=$currentMonth", throwable) } @@ -109,8 +106,7 @@ class ApiServiceImpl( .uri("/pub/leaderboards") .accept(MediaType.APPLICATION_JSON) .retrieve() - .bodyToMono(ChesscomLeaderboards::class.java) // TODO onError - .block() + .body(ChesscomLeaderboards::class.java) // TODO onError } catch (throwable: Throwable){ return Exception("Exception chesscom findTopAccounts", throwable) } diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/config/ApisConfigProperties.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/config/ApisConfigProperties.kt index 35289f8b..0a4d667c 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/config/ApisConfigProperties.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/config/ApisConfigProperties.kt @@ -2,7 +2,7 @@ package com.michibaum.chess_service.apis.config import com.michibaum.chess_service.domain.ChessPlatform import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.client.RestClient /** * Configuration properties for API clients. @@ -27,13 +27,9 @@ data class ApisConfigProperties( * This instance is built with a base URL and a custom User-Agent header. * Additionally, it configures the maximum in-memory size for codecs to 10MB. */ - val webClient: WebClient = WebClient.builder() + val webClient: RestClient = RestClient.builder() .baseUrl(baseUrl) .defaultHeader("User-Agent", "chess-statistic-application/0.1; +http://www.michibaum.ch") - .codecs { configurer -> - configurer.defaultCodecs() - .maxInMemorySize(10000000) - } .build() } @@ -42,7 +38,7 @@ data class ApisConfigProperties( data class ChessConfigProperties( var properties: Map = emptyMap() ) { - fun getWebClient(apis: ChessPlatform): WebClient { + fun getWebClient(apis: ChessPlatform): RestClient { return properties[apis]?.webClient ?: throw NoSuchElementException("No WebClient configured for $apis") } } diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/lichess/ApiServiceImpl.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/lichess/ApiServiceImpl.kt index e89a79fe..4aaa302c 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/lichess/ApiServiceImpl.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/apis/lichess/ApiServiceImpl.kt @@ -8,6 +8,7 @@ import com.michibaum.chess_service.apis.dtos.StatsDto import com.michibaum.chess_service.apis.dtos.TopAccountDto import com.michibaum.chess_service.domain.Account import com.michibaum.chess_service.domain.ChessPlatform +import org.springframework.core.ParameterizedTypeReference import org.springframework.http.MediaType import org.springframework.stereotype.Service @@ -28,8 +29,7 @@ class ApiServiceImpl( } .accept(MediaType.APPLICATION_NDJSON) .retrieve() - .bodyToMono(LichessAccountDto::class.java) - .block() + .body(LichessAccountDto::class.java) } catch (throwable: Throwable) { return Exception("Exception lichess findUser with username=${username}", throwable) } @@ -47,8 +47,7 @@ class ApiServiceImpl( .uri("/api/user/{0}/perf/{1}", account.username, perf) .accept(MediaType.APPLICATION_JSON) .retrieve() - .bodyToMono(LichessStatsDto::class.java) - .block() + .body(LichessStatsDto::class.java) } val bullet = try { @@ -82,13 +81,13 @@ class ApiServiceImpl( override fun getGames(account: Account): ApiResult> { val result = try { val url = "/api/games/user/{0}?perfType=bullet,blitz,rapid&pgnInJson=true&tags=true&clocks=true" + val typeRef: ParameterizedTypeReference> = + object : ParameterizedTypeReference>() {} client.get() .uri(url, account.username) .accept(MediaType.APPLICATION_NDJSON) .retrieve() - .bodyToFlux(LichessGameDto::class.java) - .collectList() - .block() + .body(typeRef) } catch (throwable: Throwable) { return Exception("Exception lichess getGames with username=${account.username}", throwable) } @@ -106,8 +105,7 @@ class ApiServiceImpl( .uri("/api/player") .accept(MediaType.APPLICATION_NDJSON) .retrieve() - .bodyToMono(LichessLeaderboards::class.java) - .block() + .body(LichessLeaderboards::class.java) } catch (throwable: Throwable) { return Exception("Exception lichess findTopAccounts", throwable) } diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/app/event/EventConverter.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/app/event/EventConverter.kt index bca07e84..4510fba8 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/app/event/EventConverter.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/app/event/EventConverter.kt @@ -32,7 +32,7 @@ class EventConverter( } private fun getInternalComment(comment: String): String { - val authentication = ReactiveSecurityContextHolder.getContext().map { it.authentication }.block() + val authentication = SecurityContextHolder.getContext().authentication return if (authentication != null && authentication.anyOf(Permissions.CHESS_SERVICE_ADMIN)) comment else "" } diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/domain/ChessPlatform.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/domain/ChessPlatform.kt index de9b03f1..4511c185 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/domain/ChessPlatform.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/domain/ChessPlatform.kt @@ -3,5 +3,6 @@ package com.michibaum.chess_service.domain enum class ChessPlatform{ CHESSCOM, LICHESS, - FIDE + FIDE, + FREESTYLE } \ No newline at end of file diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityBeansConfiguration.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityBeansConfiguration.kt index f172baef..4baea59f 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityBeansConfiguration.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityBeansConfiguration.kt @@ -1,21 +1,20 @@ package com.michibaum.chess_service.security import com.michibaum.authentication_library.AuthenticationClient -import com.michibaum.authentication_library.security.ReactiveDelegateAuthenticationManager +import com.michibaum.authentication_library.security.ServletAuthenticationFilter +import com.michibaum.authentication_library.security.ServletDelegateAuthenticationManager import com.michibaum.authentication_library.security.SpecificAuthenticationManager import com.michibaum.authentication_library.security.basic.BasicAuthenticationManager -import com.michibaum.authentication_library.security.basic.BasicExchangeMatcher import com.michibaum.authentication_library.security.basic.CredentialsValidator -import com.michibaum.authentication_library.security.basic.netty.BasicAuthenticationConverter +import com.michibaum.authentication_library.security.basic.servlet.BasicAuthenticationConverter import com.michibaum.authentication_library.security.jwt.JwsValidator import com.michibaum.authentication_library.security.jwt.JwtAuthenticationManager -import com.michibaum.authentication_library.security.jwt.JwtExchangeMatcher -import com.michibaum.authentication_library.security.jwt.netty.JwtAuthenticationConverter +import com.michibaum.authentication_library.security.jwt.servlet.JwtAuthenticationConverter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.AuthenticationConverter @Configuration class SecurityBeansConfiguration { @@ -48,39 +47,22 @@ class SecurityBeansConfiguration { BasicAuthenticationManager(credentialsValidator) @Bean - fun authenticationManager(specificAuthenticationManagers: List): ReactiveAuthenticationManager = - ReactiveDelegateAuthenticationManager(specificAuthenticationManagers) + fun authenticationManager(specificAuthenticationManagers: List): AuthenticationManager = + ServletDelegateAuthenticationManager(specificAuthenticationManagers) @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter = + fun jwtAuthenticationConverter(): AuthenticationConverter = JwtAuthenticationConverter() @Bean - fun basicAuthenticationConverter(): BasicAuthenticationConverter = + fun basicAuthenticationConverter(): AuthenticationConverter = BasicAuthenticationConverter() @Bean - fun jwtAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - jwtAuthenticationConverter: JwtAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(JwtExchangeMatcher()) - setServerAuthenticationConverter(jwtAuthenticationConverter) - } - - @Bean - fun basicAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - basicAuthenticationConverter: BasicAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(BasicExchangeMatcher()) - setServerAuthenticationConverter(basicAuthenticationConverter) - } - + fun authenticationFilter(authenticationManager: AuthenticationManager, authenticationConverters: List) = + ServletAuthenticationFilter(authenticationManager, authenticationConverters) } \ No newline at end of file diff --git a/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityConfiguration.kt b/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityConfiguration.kt index 80a85326..e117de9d 100644 --- a/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityConfiguration.kt +++ b/chess-service/src/main/kotlin/com/michibaum/chess_service/security/SecurityConfiguration.kt @@ -1,37 +1,41 @@ package com.michibaum.chess_service.security +import com.michibaum.authentication_library.security.ServletAuthenticationFilter import com.michibaum.permission_library.Permissions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity +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.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec import org.springframework.security.config.web.server.ServerHttpSecurity.http +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.AuthenticationWebFilter @Configuration -@EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableWebSecurity +@EnableMethodSecurity class SecurityConfiguration { @Bean fun securityFilterChain( - http: ServerHttpSecurity, - jwtAuthenticationWebFilter: AuthenticationWebFilter, - basicAuthenticationWebFilter: AuthenticationWebFilter, - authenticationManager: ReactiveAuthenticationManager - ): SecurityWebFilterChain { + http: HttpSecurity, + authenticationFilter: ServletAuthenticationFilter + ): SecurityFilterChain { return http - .authorizeExchange { exchanges: AuthorizeExchangeSpec -> - exchanges - .pathMatchers( + .authorizeHttpRequests { + it + .requestMatchers( HttpMethod.GET,"/api/events", "/api/events/*", "/api/events/*/participants", @@ -39,15 +43,13 @@ class SecurityConfiguration { "/api/event-categories", "/api/event-categories/with-events" ).permitAll() - .pathMatchers( + .requestMatchers( "/actuator", "/actuator/**" ).hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyExchange().authenticated() + .anyRequest().authenticated() } - .addFilterAt(basicAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .authenticationManager(authenticationManager) + .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } .formLogin { formLoginSpec -> formLoginSpec.disable() } .csrf { csrfSpec -> csrfSpec.disable() } diff --git a/chess-service/src/main/resources/application.yml b/chess-service/src/main/resources/application.yml index 188e2ba5..25f1984a 100644 --- a/chess-service/src/main/resources/application.yml +++ b/chess-service/src/main/resources/application.yml @@ -66,6 +66,21 @@ management: info: git: mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true chess-apis: properties: diff --git a/chess-service/src/main/resources/db/migration/V3__move_fide_infos_to_account.sql b/chess-service/src/main/resources/db/migration/V3__move_fide_infos_to_account.sql index 8618b80b..83ec484d 100644 --- a/chess-service/src/main/resources/db/migration/V3__move_fide_infos_to_account.sql +++ b/chess-service/src/main/resources/db/migration/V3__move_fide_infos_to_account.sql @@ -1,7 +1,7 @@ -- Modify User and account table that user infos drop into separate account alter table account modify platform enum ('CHESSCOM', 'LICHESS', 'OVER_THE_BOARD', 'FIDE') not null; UPDATE account a SET a.platform = 'FIDE' WHERE a.platform = 'OVER_THE_BOARD'; -alter table account modify platform enum ('CHESSCOM', 'LICHESS', 'FIDE') not null; +alter table account modify platform enum ('CHESSCOM', 'LICHESS', 'FIDE', 'FREESTYLE') not null; INSERT INTO account (id, created_at, person_id, name, platform_id, url, username, platform) SELECT @@ -26,5 +26,5 @@ WHERE alter table account drop column url; alter table person drop column fide_id; alter table event add platform enum ('CHESSCOM', 'LICHESS', 'FIDE') NOT NULL DEFAULT 'FIDE'; -alter table event MODIFY platform ENUM('CHESSCOM', 'LICHESS', 'FIDE') NOT NULL; +alter table event MODIFY platform ENUM('CHESSCOM', 'LICHESS', 'FIDE', 'FREESTYLE') NOT NULL; alter table event add internal_comment TEXT default '' not null; \ No newline at end of file diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/ActuatorIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/ActuatorIT.kt index 1ab86a41..7d43f7d3 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/ActuatorIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/ActuatorIT.kt @@ -1,116 +1,52 @@ package com.michibaum.chess_service -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest(properties = [ "spring.boot.admin.client.username=someUsername", "spring.boot.admin.client.password=somePasswööörd" ]) +@TestcontainersConfiguration class ActuatorIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.get() - .uri("/actuator") - .exchange() - .expectStatus() - .isUnauthorized + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/health") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/info") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator/health") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.get() - .uri("/actuator/info") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/ChessServiceApplicationIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/ChessServiceApplicationIT.kt index 108db87e..b60cd7b7 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/ChessServiceApplicationIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/ChessServiceApplicationIT.kt @@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext @SpringBootTest +@TestcontainersConfiguration class ChessServiceApplicationIT { @Autowired diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/FlywayClearDatabaseJunitExtension.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/FlywayClearDatabaseJunitExtension.kt new file mode 100644 index 00000000..c808bba6 --- /dev/null +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/FlywayClearDatabaseJunitExtension.kt @@ -0,0 +1,18 @@ +package com.michibaum.chess_service + +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + + +class FlywayClearDatabaseJunitExtension: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext?) { + if(context == null) throw IllegalArgumentException("Context must not be null") + + val flyway = SpringExtension.getApplicationContext(context) + .getBean(Flyway::class.java) + flyway.clean() + flyway.migrate() + } +} \ No newline at end of file diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersConfiguration.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersConfiguration.kt new file mode 100644 index 00000000..d8a3dfc6 --- /dev/null +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersConfiguration.kt @@ -0,0 +1,12 @@ +package com.michibaum.chess_service + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.annotation.Import +import org.springframework.test.context.TestPropertySource + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(value = [FlywayClearDatabaseJunitExtension::class]) +@Import(value = [TestcontainersTestConfiguration::class]) +@TestPropertySource(properties = ["spring.flyway.clean-disabled=false"]) +annotation class TestcontainersConfiguration() diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersTestConfiguration.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersTestConfiguration.kt new file mode 100644 index 00000000..448c8283 --- /dev/null +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/TestcontainersTestConfiguration.kt @@ -0,0 +1,18 @@ +package com.michibaum.chess_service + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.MariaDBContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersTestConfiguration { + + @Bean + @ServiceConnection + fun mariaDbContainer(): MariaDBContainer<*> { + return MariaDBContainer(DockerImageName.parse("mariadb:10.11")) + } + +} \ No newline at end of file diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/ApiServiceIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/ApiServiceIT.kt index e948133b..37a691d7 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/ApiServiceIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/ApiServiceIT.kt @@ -1,5 +1,6 @@ package com.michibaum.chess_service.apis +import com.michibaum.chess_service.TestcontainersConfiguration import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Disabled @@ -11,6 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest "chess-apis.properties.lichess.base-url=https://lichess.org", "chess-apis.properties.chesscom.base-url=https://api.chess.com" ]) +@TestcontainersConfiguration class ApiServiceIT{ @Autowired diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/chesscom/IApiServiceImplIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/chesscom/IApiServiceImplIT.kt index c92a1889..5553f485 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/chesscom/IApiServiceImplIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/chesscom/IApiServiceImplIT.kt @@ -4,6 +4,7 @@ import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching import com.github.tomakehurst.wiremock.core.WireMockConfiguration import com.github.tomakehurst.wiremock.junit5.WireMockExtension +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.apis.Exception import com.michibaum.chess_service.apis.IApiService import com.michibaum.chess_service.apis.Success @@ -21,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest(properties = [ "chess-apis.properties.chesscom.base-url=http://localhost:8099" ]) +@TestcontainersConfiguration class IApiServiceImplIT { @Autowired diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/lichess/IApiServiceImplIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/lichess/IApiServiceImplIT.kt index 7ae75ddb..3839fa34 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/lichess/IApiServiceImplIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/apis/lichess/IApiServiceImplIT.kt @@ -3,6 +3,7 @@ package com.michibaum.chess_service.apis.lichess import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.core.WireMockConfiguration import com.github.tomakehurst.wiremock.junit5.WireMockExtension +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.apis.Exception import com.michibaum.chess_service.apis.IApiService import com.michibaum.chess_service.apis.Success @@ -21,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest(properties = [ "chess-apis.properties.lichess.base-url=http://localhost:8098" ]) +@TestcontainersConfiguration class IApiServiceImplIT{ @Autowired lateinit var lichessApiService: IApiService diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountRepositoryIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountRepositoryIT.kt index d8631560..3f30c270 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountRepositoryIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountRepositoryIT.kt @@ -1,14 +1,18 @@ package com.michibaum.chess_service.app.account +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.app.person.PersonRepository import com.michibaum.chess_service.domain.AccountProvider import com.michibaum.chess_service.domain.PersonProvider import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestcontainersConfiguration class AccountRepositoryIT { @Autowired @@ -32,7 +36,6 @@ class AccountRepositoryIT { assertEquals(account.name, result.name) assertEquals(account.platformId, result.platformId) assertEquals(account.username, result.username) - assertEquals(account.url, result.url) assertEquals(account.platform, result.platform) assertEquals(account.person, result.person) } diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceIT.kt index 70742226..8cdd61da 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceIT.kt @@ -1,13 +1,16 @@ package com.michibaum.chess_service.app.account +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.domain.AccountProvider import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.util.* @SpringBootTest +@TestcontainersConfiguration class AccountServiceIT { @Autowired @@ -16,6 +19,7 @@ class AccountServiceIT { @Autowired lateinit var accountRepository: AccountRepository + @Test fun `findById if saved account exists`(){ // GIVEN @@ -24,6 +28,7 @@ class AccountServiceIT { // WHEN val result = accountService.findById(savedAccount.idOrThrow()) + println(accountRepository.count()) // THEN assertNotNull(result) @@ -32,10 +37,11 @@ class AccountServiceIT { @Test fun `findById if saved account does not exists`(){ // GIVEN - val account = AccountProvider.account() + val id = UUID.randomUUID() // WHEN - val result = accountService.findById(account.idOrThrow()) + val result = accountService.findById(id) + println(accountRepository.count()) // THEN assertNull(result) diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceUT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceUT.kt deleted file mode 100644 index 55ba7c3c..00000000 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/account/AccountServiceUT.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.michibaum.chess_service.app.account - -import com.michibaum.chess_service.anyIterable -import com.michibaum.chess_service.apis.ApiService -import com.michibaum.chess_service.apis.Success -import com.michibaum.chess_service.apis.dtos.AccountDto -import com.michibaum.chess_service.domain.ChessPlatform -import io.mockk.every -import io.mockk.mockk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.junit.jupiter.MockitoExtension -import java.time.LocalDate - -@ExtendWith(MockitoExtension::class) -class AccountServiceUT { - - private val apiService: ApiService = mockk() - private val accountRepository: AccountRepository = mockk() - private val accountService: AccountService = AccountService(apiService, accountRepository) - - @Test - fun `successfully found account` (){ - // GIVEN - val username = "someUsername" - val apiAccount = AccountDto( - id = "someId", - url = "someUrl", - username = username, - name = "Some Name", - platform = ChessPlatform.CHESSCOM, - createdAt = LocalDate.now() - ) - every { apiService.findAccount(username) } returns listOf(Success(apiAccount)) - every { accountRepository.saveAll(anyIterable()) } returnsArgument 0 - - // WHEN - val result = accountService.getAccounts(username, false) - - // THEN - assertEquals(1, result.size) - assertEquals(apiAccount.id, result[0].platformId) - assertEquals(apiAccount.username, result[0].username) - - } - - @Test - fun `successfully found multiple accounts` (){ - // GIVEN - val username = "someUsername" - val apiAccount1 = AccountDto( - id = "someId1", - url = "someUrl1", - username = username, - name = "Some Name1", - platform = ChessPlatform.CHESSCOM, - createdAt = LocalDate.now() - ) - val apiAccount2 = AccountDto( - id = "someId2", - url = "someUrl2", - username = username, - name = "Some Name2", - platform = ChessPlatform.LICHESS, - createdAt = LocalDate.now() - ) - every { apiService.findAccount(username) } returns listOf(Success(apiAccount1), Success(apiAccount2)) - every { accountRepository.saveAll(anyIterable()) } returnsArgument 0 - - // WHEN - val result = accountService.getAccounts(username, false) - - // THEN - assertEquals(2, result.size) - } - -} \ No newline at end of file diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/event/EventControllerIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/event/EventControllerIT.kt index 02a5d50d..6825e224 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/event/EventControllerIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/event/EventControllerIT.kt @@ -1,26 +1,27 @@ package com.michibaum.chess_service.app.event -import com.fasterxml.jackson.databind.ObjectMapper +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.app.person.PersonRepository import com.michibaum.chess_service.domain.EventProvider import com.michibaum.chess_service.domain.PersonProvider import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.MediaType -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest +@TestcontainersConfiguration class EventControllerIT { @Autowired - lateinit var webTestClient: WebTestClient - - @Autowired - lateinit var objectMapper: ObjectMapper + lateinit var mockMvc: MockMvc @Autowired lateinit var eventRepository: EventRepository @@ -36,14 +37,15 @@ class EventControllerIT { val savedEvent = eventRepository.save(event) // WHEN - webTestClient.get() - .uri("/api/events") + mockMvc.perform( + get("/api/events") .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isOk}, - {it.expectBody()} - ) + ).andExpectAll( + status().isOk, + jsonPath("$.[0].id").exists(), + jsonPath("$.[0].title").exists(), + ) + // THEN @@ -57,14 +59,14 @@ class EventControllerIT { val savedEvent = eventRepository.save(event) // WHEN - webTestClient.get() - .uri("/api/events/${savedEvent.id}") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isOk}, - {it.expectBody()} - ) + mockMvc.perform( + get("/api/events/${savedEvent.id}") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isOk, + jsonPath("$.id").exists(), + jsonPath("$.title").exists(), + ) // THEN @@ -82,13 +84,12 @@ class EventControllerIT { } while (savedEvent.id == uuid) // WHEN - webTestClient.get() - .uri("/api/events/${uuid}") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isNotFound}, - ) + mockMvc.perform( + get("/api/events/${uuid}") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isNotFound + ) // THEN @@ -103,17 +104,15 @@ class EventControllerIT { val uuid = "abc" // WHEN - webTestClient.get() - .uri("/api/events/${uuid}") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isBadRequest}, - ) + mockMvc.perform( + get("/api/events/${uuid}") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isBadRequest + ) // THEN - } @Test @@ -125,14 +124,15 @@ class EventControllerIT { val savedEvent = eventRepository.save(event) // WHEN - webTestClient.get() - .uri("/api/events/${savedEvent.id}/participants") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isOk}, - {it.expectBody()} - ) + mockMvc.perform( + get("/api/events/${savedEvent.id}/participants") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isOk, + jsonPath("$.[0].id").exists(), + jsonPath("$.[0].firstname").exists(), + jsonPath("$.[0].lastname").exists(), + ) // THEN @@ -152,13 +152,12 @@ class EventControllerIT { } while (savedEvent.id == uuid) // WHEN - webTestClient.get() - .uri("/api/events/${uuid}/participants") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isNotFound} - ) + mockMvc.perform( + get("/api/events/${uuid}/participants") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isNotFound + ) // THEN @@ -175,13 +174,12 @@ class EventControllerIT { val uuid = "abc" // WHEN - webTestClient.get() - .uri("/api/events/${uuid}/participants") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectAll( - {it.expectStatus().isBadRequest} - ) + mockMvc.perform( + get("/api/events/${uuid}/participants") + .accept(MediaType.APPLICATION_JSON) + ).andExpectAll( + status().isBadRequest + ) // THEN diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonControllerIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonControllerIT.kt index 9723a273..f2cc665c 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonControllerIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonControllerIT.kt @@ -3,6 +3,7 @@ package com.michibaum.chess_service.app.person import com.fasterxml.jackson.databind.ObjectMapper import com.michibaum.authentication_library.JwsValidationSuccess import com.michibaum.authentication_library.security.jwt.JwsValidator +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.domain.Gender import com.michibaum.chess_service.domain.Person import org.junit.jupiter.api.Test @@ -17,6 +18,7 @@ import java.util.* @AutoConfigureWebTestClient @SpringBootTest +@TestcontainersConfiguration class PersonControllerIT { @Autowired @@ -40,13 +42,11 @@ class PersonControllerIT { val requestPersonDto = WritePersonDto( firstname = "John", lastname = "Doe", - fideId = "121212", gender = Gender.MALE, ) val convertedPerson = Person( firstname = requestPersonDto.firstname, lastname = requestPersonDto.lastname, - fideId = requestPersonDto.fideId, gender = Gender.MALE, accounts = emptySet(), federation = null, @@ -56,7 +56,6 @@ class PersonControllerIT { id = UUID.randomUUID(), firstname = convertedPerson.firstname, lastname = convertedPerson.lastname, - fideId = convertedPerson.fideId, accounts = emptySet(), federation = null, birthday = null, @@ -66,7 +65,6 @@ class PersonControllerIT { id = savedPerson.idOrThrow(), firstname = savedPerson.firstname, lastname = savedPerson.lastname, - fideId = savedPerson.fideId, federation = savedPerson.federation, birthday = savedPerson.birthday.toString(), gender = savedPerson.gender, diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonRepositoryIT.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonRepositoryIT.kt index 56a380d1..0a8521f7 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonRepositoryIT.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/app/person/PersonRepositoryIT.kt @@ -1,13 +1,17 @@ package com.michibaum.chess_service.app.person +import com.michibaum.chess_service.TestcontainersConfiguration import com.michibaum.chess_service.domain.PersonProvider import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestcontainersConfiguration class PersonRepositoryIT { @Autowired diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/AccountProvider.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/AccountProvider.kt index fbfcdbd5..d1fd5ea1 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/AccountProvider.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/AccountProvider.kt @@ -1,9 +1,5 @@ package com.michibaum.chess_service.domain -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneId - class AccountProvider { companion object { fun account(username: String = "Michi1", person: Person? = null): Account { @@ -11,10 +7,9 @@ class AccountProvider { name = "Michi", platformId = "TGsdisd3", username = username, - url = "https://chess-is-best.com/Michi1", platform = ChessPlatform.CHESSCOM, person = person, - createdAt = LocalDate.ofInstant(Instant.ofEpochSecond(1595005065053), ZoneId.systemDefault()), + createdAt = null, games = emptySet() ) } diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/EventProvider.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/EventProvider.kt index fe2014fa..b395e754 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/EventProvider.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/EventProvider.kt @@ -14,7 +14,8 @@ class EventProvider { embedUrl = "https://www.chess.com/events/embed/2024-fide-chess-world-championship", dateFrom = LocalDate.of(2024, 11, 25), dateTo = LocalDate.of(2024, 12, 13), - participants = participants + participants = participants, + internalComment = "" ) } diff --git a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/PersonProvider.kt b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/PersonProvider.kt index 39e4c677..7e223f9a 100644 --- a/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/PersonProvider.kt +++ b/chess-service/src/test/kotlin/com/michibaum/chess_service/domain/PersonProvider.kt @@ -9,7 +9,6 @@ class PersonProvider { return Person( firstname = "Michi", lastname = "Baum", - fideId = null, accounts = accounts, federation = "SUI", birthday = LocalDate.of(2001, 3, 31), diff --git a/chess-service/src/test/resources/application.yml b/chess-service/src/test/resources/application.yml index e62babc4..00c97da3 100644 --- a/chess-service/src/test/resources/application.yml +++ b/chess-service/src/test/resources/application.yml @@ -20,20 +20,6 @@ spring: lazy-attributes-resolution: true main: allow-bean-definition-overriding: true - datasource: - username: sa - password: password - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:chess-db - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - globally_quoted_identifiers: true - h2: - console: - enabled: true eureka: instance: @@ -68,6 +54,21 @@ management: info: git: mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true chess-apis: properties: diff --git a/fitness-service/pom.xml b/fitness-service/pom.xml index 5efa5d0e..8bbd7e33 100644 --- a/fitness-service/pom.xml +++ b/fitness-service/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-web @@ -99,8 +99,24 @@ test - com.h2database - h2 + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mariadb test diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/api/FitbitApiImpl.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/api/FitbitApiImpl.kt index 2212473f..1ed9a521 100644 --- a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/api/FitbitApiImpl.kt +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/api/FitbitApiImpl.kt @@ -9,21 +9,17 @@ import com.michibaum.fitness_service.fitbit.oauth.FitbitOAuthCredentials import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Mono +import org.springframework.web.client.RestClient +import org.springframework.web.client.body @Component class FitbitApiImpl: FitbitApi { - val client = WebClient.builder() + val client = RestClient.builder() .baseUrl("https://api.fitbit.com") .defaultHeaders { it.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) } - .codecs { configurer -> - configurer.defaultCodecs() - .maxInMemorySize(10000000) - } .build() override fun profile(credentials: FitbitOAuthCredentials): ProfileDto? { @@ -33,14 +29,11 @@ class FitbitApiImpl: FitbitApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 400 }, { Mono.error(Exception()) }) // TODO The request had bad syntax or was inherently impossible to be satisfied. - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) // TODO Forbidden - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. - .bodyToMono(ProfileDto::class.java) - // TODO .onError - .block() - + .onStatus({ t -> t.value() == 400 }, { _, _ -> }) // TODO The request had bad syntax or was inherently impossible to be satisfied. + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) // TODO Forbidden + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. + .body() } @@ -53,13 +46,11 @@ class FitbitApiImpl: FitbitApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 400 }, { Mono.error(Exception()) }) // TODO The request had bad syntax or was inherently impossible to be satisfied. - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) // TODO Forbidden - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. - .bodyToMono(WeightLogDto::class.java) - // TODO .onError - .block() + .onStatus({ t -> t.value() == 400 }, { _, _ -> }) // TODO The request had bad syntax or was inherently impossible to be satisfied. + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) // TODO Forbidden + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. + .body() ?.weight ?: emptyList() } @@ -72,13 +63,11 @@ class FitbitApiImpl: FitbitApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 400 }, { Mono.error(Exception()) }) // TODO The request had bad syntax or was inherently impossible to be satisfied. - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) // TODO Forbidden - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. - .bodyToMono(SleepLogDto::class.java) - // TODO .onError - .block() + .onStatus({ t -> t.value() == 400 }, { _, _ -> }) // TODO The request had bad syntax or was inherently impossible to be satisfied. + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) // TODO The request requires user authentication. Use FitbitOAuth to refresh token + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) // TODO Forbidden + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) // TODO Returned if the application has reached the rate limit for a specific user. The rate limit will be reset at the top of the hour. + .body() ?.sleep ?: emptyList() } } \ No newline at end of file diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthController.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthController.kt index ce6d75ef..f391b070 100644 --- a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthController.kt +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthController.kt @@ -6,8 +6,7 @@ import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import org.springframework.web.reactive.function.BodyInserters -import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.client.RestClient import reactor.core.publisher.Mono import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -19,13 +18,13 @@ class FitbitOAuthController( private val fitbitOAuthProperties: FitbitOAuthProperties ) { - private final val client: WebClient + private final val client: RestClient init { val clientAndSecret = fitbitOAuthProperties.clientId + ":" + fitbitOAuthProperties.clientSecret val authBasic = Base64.getUrlEncoder().withoutPadding().encodeToString(clientAndSecret.encodeToByteArray()) - client = WebClient.builder() + client = RestClient.builder() .baseUrl("https://api.fitbit.com") .defaultHeaders { it.set("Authorization", "Basic $authBasic") @@ -65,21 +64,15 @@ class FitbitOAuthController( fun authorizationCode(@RequestParam code: String, @RequestParam state: String){ val oAuthData = fitbitOAuthService.findByState(state) ?: throw Exception("No oAuthData found by state $state") + val body = FitbitTokenBodyDto(fitbitOAuthProperties.clientId, "authorization_code", code, oAuthData.codeVerifier, "https://fitness.michibaum.ch/api/fitbit/auth") val response = client .post() .uri("/oauth2/token") .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body( - BodyInserters.fromFormData("client_id", fitbitOAuthProperties.clientId) - .with("grant_type", "authorization_code") - .with("code", code) - .with("code_verifier", oAuthData.codeVerifier) - .with("redirect_uri", "https://fitness.michibaum.ch/api/fitbit/auth") - ) + .body(body) // TODO needs testing if correct deserialized .retrieve() - .onStatus({ t -> t.is4xxClientError }, { Mono.error(Exception()) }) // TODO https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/error-messages/#authorization-errors - .bodyToMono(FitbitOAuthCredentialsDto::class.java) - .block() + .onStatus({ t -> t.is4xxClientError }, { _, _ -> }) // TODO https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/error-messages/#authorization-errors + .body(FitbitOAuthCredentialsDto::class.java) if(response == null){ throw Exception("Fitbit OAuth exchange for access token returned null") diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthImpl.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthImpl.kt index b1d06d2d..495ae9d5 100644 --- a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthImpl.kt +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitOAuthImpl.kt @@ -5,8 +5,7 @@ import com.michibaum.fitness_service.fitbit.FitbitOAuth import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.BodyInserters -import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.client.RestClient import reactor.core.publisher.Mono import java.time.Instant import java.util.* @@ -23,7 +22,7 @@ class FitbitOAuthImpl( val clientAndSecret = fitbitOAuthProperties.clientId + ":" + fitbitOAuthProperties.clientSecret val authBasic = Base64.getUrlEncoder().withoutPadding().encodeToString(clientAndSecret.encodeToByteArray()) - val client = WebClient.builder() + val client = RestClient.builder() .baseUrl("https://api.fitbit.com") .defaultHeaders { it.set("Authorization", "Basic $authBasic") @@ -31,18 +30,15 @@ class FitbitOAuthImpl( it.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) } .build() + val body = FitbitRefreshBodyDto("refresh_token", credentials.refreshToken) val response = client .post() .uri("/oauth2/token") .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body( - BodyInserters.fromFormData("grant_type", "refresh_token") - .with("refresh_token", credentials.refreshToken) - ) + .body(body) // TODO needs testing if this works .retrieve() - .onStatus({ t -> t.is4xxClientError }, { Mono.error(Exception()) }) // TODO https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/error-messages/#authorization-errors - .bodyToMono(FitbitOAuthCredentialsDto::class.java) - .block() + .onStatus({ t -> t.is4xxClientError }, { _, _ -> }) // TODO https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/error-messages/#authorization-errors + .body(FitbitOAuthCredentialsDto::class.java) if(response == null){ throw Exception("Fitbit OAuth refresh access token returned null") diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitRefreshBodyDto.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitRefreshBodyDto.kt new file mode 100644 index 00000000..5625a632 --- /dev/null +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitRefreshBodyDto.kt @@ -0,0 +1,6 @@ +package com.michibaum.fitness_service.fitbit.oauth + +data class FitbitRefreshBodyDto( + val grantType: String, + val refreshToken: String +) diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitTokenBodyDto.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitTokenBodyDto.kt new file mode 100644 index 00000000..0551c586 --- /dev/null +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/fitbit/oauth/FitbitTokenBodyDto.kt @@ -0,0 +1,9 @@ +package com.michibaum.fitness_service.fitbit.oauth + +data class FitbitTokenBodyDto( + val clientId: String, + val grantType: String, + val code: String, + val codeVerifier: String, + val redirectUri: String +) diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityBeansConfiguration.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityBeansConfiguration.kt index 6aa49772..f3a40f28 100644 --- a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityBeansConfiguration.kt +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityBeansConfiguration.kt @@ -1,21 +1,20 @@ package com.michibaum.fitness_service.security import com.michibaum.authentication_library.AuthenticationClient -import com.michibaum.authentication_library.security.ReactiveDelegateAuthenticationManager +import com.michibaum.authentication_library.security.ServletAuthenticationFilter +import com.michibaum.authentication_library.security.ServletDelegateAuthenticationManager import com.michibaum.authentication_library.security.SpecificAuthenticationManager import com.michibaum.authentication_library.security.basic.BasicAuthenticationManager -import com.michibaum.authentication_library.security.basic.BasicExchangeMatcher import com.michibaum.authentication_library.security.basic.CredentialsValidator -import com.michibaum.authentication_library.security.basic.netty.BasicAuthenticationConverter +import com.michibaum.authentication_library.security.basic.servlet.BasicAuthenticationConverter import com.michibaum.authentication_library.security.jwt.JwsValidator import com.michibaum.authentication_library.security.jwt.JwtAuthenticationManager -import com.michibaum.authentication_library.security.jwt.JwtExchangeMatcher -import com.michibaum.authentication_library.security.jwt.netty.JwtAuthenticationConverter +import com.michibaum.authentication_library.security.jwt.servlet.JwtAuthenticationConverter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.AuthenticationConverter @Configuration class SecurityBeansConfiguration { @@ -48,39 +47,22 @@ class SecurityBeansConfiguration { BasicAuthenticationManager(credentialsValidator) @Bean - fun authenticationManager(specificAuthenticationManagers: List): ReactiveAuthenticationManager = - ReactiveDelegateAuthenticationManager(specificAuthenticationManagers) + fun authenticationManager(specificAuthenticationManagers: List): AuthenticationManager = + ServletDelegateAuthenticationManager(specificAuthenticationManagers) @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter = + fun jwtAuthenticationConverter(): AuthenticationConverter = JwtAuthenticationConverter() @Bean - fun basicAuthenticationConverter(): BasicAuthenticationConverter = + fun basicAuthenticationConverter(): AuthenticationConverter = BasicAuthenticationConverter() @Bean - fun jwtAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - jwtAuthenticationConverter: JwtAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(JwtExchangeMatcher()) - setServerAuthenticationConverter(jwtAuthenticationConverter) - } - - @Bean - fun basicAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - basicAuthenticationConverter: BasicAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(BasicExchangeMatcher()) - setServerAuthenticationConverter(basicAuthenticationConverter) - } - + fun authenticationFilter(authenticationManager: AuthenticationManager, authenticationConverters: List) = + ServletAuthenticationFilter(authenticationManager, authenticationConverters) } \ No newline at end of file diff --git a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityConfiguration.kt b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityConfiguration.kt index 967d849c..439b3286 100644 --- a/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityConfiguration.kt +++ b/fitness-service/src/main/kotlin/com/michibaum/fitness_service/security/SecurityConfiguration.kt @@ -1,42 +1,46 @@ package com.michibaum.fitness_service.security +import com.michibaum.authentication_library.security.ServletAuthenticationFilter import com.michibaum.permission_library.Permissions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity +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.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.AuthenticationWebFilter @Configuration -@EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableWebSecurity +@EnableMethodSecurity class SecurityConfiguration { @Bean fun securityFilterChain( - http: ServerHttpSecurity, - jwtAuthenticationWebFilter: AuthenticationWebFilter, - basicAuthenticationWebFilter: AuthenticationWebFilter, - ): SecurityWebFilterChain { + http: HttpSecurity, + authenticationFilter: ServletAuthenticationFilter + ): SecurityFilterChain { return http - .authorizeExchange { exchanges: AuthorizeExchangeSpec -> - exchanges - .pathMatchers( + .authorizeHttpRequests { + it + .requestMatchers( "/api/fitbit/auth", ).permitAll() - .pathMatchers( + .requestMatchers( "/actuator", "/actuator/**" ).hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyExchange().authenticated() + .anyRequest().authenticated() } - .addFilterAt(basicAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } .formLogin { formLoginSpec -> formLoginSpec.disable() } .csrf { csrfSpec -> csrfSpec.disable() } diff --git a/fitness-service/src/main/resources/application.yml b/fitness-service/src/main/resources/application.yml index e47ec558..213b7dec 100644 --- a/fitness-service/src/main/resources/application.yml +++ b/fitness-service/src/main/resources/application.yml @@ -65,4 +65,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/ActuatorIT.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/ActuatorIT.kt index 62f54012..f3fa4bb5 100644 --- a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/ActuatorIT.kt +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/ActuatorIT.kt @@ -1,116 +1,52 @@ package com.michibaum.fitness_service -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest(properties = [ "spring.boot.admin.client.username=someUsername", "spring.boot.admin.client.password=somePasswööörd" ]) +@TestcontainersConfiguration class ActuatorIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.get() - .uri("/actuator") - .exchange() - .expectStatus() - .isUnauthorized + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/health") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/info") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator/health") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.get() - .uri("/actuator/info") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitbitOAuthControllerIT.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitbitOAuthControllerIT.kt index dff90fa6..e9053fb5 100644 --- a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitbitOAuthControllerIT.kt +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitbitOAuthControllerIT.kt @@ -9,19 +9,24 @@ import org.mockito.Mockito.anyString import org.mockito.Mockito.`when` import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.bean.override.mockito.MockitoBean import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest(properties = [ "fitbit.oauth.client-id=someClientId", "fitbit.oauth.client-secret=someClientSecret", ]) +@TestcontainersConfiguration class FitbitOAuthControllerIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc @Autowired lateinit var fitbitOAuthRepository: FitbitOAuthRepository @@ -34,11 +39,8 @@ class FitbitOAuthControllerIT { // GIVEN // WHEN - webClient.get() - .uri("/api/fitbit/token") - .exchange() - .expectStatus() - .isUnauthorized() + mockMvc.get("/api/fitbit/token") + .andExpect { status { isUnauthorized() } } // THEN } @@ -49,12 +51,9 @@ class FitbitOAuthControllerIT { `when`(jwsValidator.validate(anyString())).thenReturn(JwsValidationSuccess()) // WHEN - webClient.get() - .uri("/api/fitbit/token") - .header("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoZW50aWNhdGlvbi1zZXJ2aWNlIiwic3ViIjoiU29tZVVzZXJuYW1lIiwiZXhwIjoxNzMwNDA2MzM4LCJpYXQiOjE3MzAzNzc1MzgsInVzZXJJZCI6IjBmMzdjMTkyLTRjNTgtNGQyZS1iM2U5LTljNDMyZTk4ZWQzMSIsInBlcm1pc3Npb25zIjpbIkNIRVNTX1NFUlZJQ0UiLCJSRUdJU1RSWV9TRVJWSUNFIiwiQURNSU5fU0VSVklDRSJdfQ.FGEUdC0S7CveGnSSUMIPBGaOnzljBTpv1Nfs1dYHiocwCA5gSEY-_WkQJN3p18Nc0FsuGqNXAbJrLkRd_Mzc8W1OHIb7s5TW-klX1ePkhDxkNmzSpxpHTad04hjTiIeXmeCUb52S-9m_MqU5OBYlaWn8k2zBRe_P14cmUbMa2X3aXDxKkBmNmDS12ry0czPIz_20RPgpNuRyp9mk79uGlrUPNBbNpcodULgvjr9WGN32VAu5CNMtZM-wmpSz5e6YGq9S46OdPOWkRquS3vktJ4ikx7u4fKPNXWqkhpuGM81IlQu7OC2Dy8jKkc_6QUD4gnHqvLc51R8XXQfxWLqdrkzvXR6BqvOQ_iQvntR1VPjU15D4KUKUnBFRDWM_lUdJJRGKlq1xkS37AV96Jqqb9MlXfso57uenjdfrhaF5sCd7PJIEfHRUu0QhymnCyvxfWAmY3NBi_Fk6S-XXCKOcDXhKjAVE_xMkZzidMOwZ6GR9GPBbrDkL5VheozcGm_D9lq9klpFRdScAAg7mIsYnfmNMGzmW3l4TyNpTqTvKlu5tq_9dXgqGJgVQ3L_123eLJBNwSpKG0GHcPYtMZhJeqWG0rJ2cCsyOEfd7l8CPAzY3NNA-D289SJOJTJyOLssi3eDmPj8UISR5ClAg12jdhSsMGJYuLCnbm5DngCKh9XI") - .exchange() - .expectStatus() - .isOk() + mockMvc.get("/api/fitbit/token") { + header("Authorization", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoZW50aWNhdGlvbi1zZXJ2aWNlIiwic3ViIjoiU29tZVVzZXJuYW1lIiwiZXhwIjoxNzMwNDA2MzM4LCJpYXQiOjE3MzAzNzc1MzgsInVzZXJJZCI6IjBmMzdjMTkyLTRjNTgtNGQyZS1iM2U5LTljNDMyZTk4ZWQzMSIsInBlcm1pc3Npb25zIjpbIkNIRVNTX1NFUlZJQ0UiLCJSRUdJU1RSWV9TRVJWSUNFIiwiQURNSU5fU0VSVklDRSJdfQ.FGEUdC0S7CveGnSSUMIPBGaOnzljBTpv1Nfs1dYHiocwCA5gSEY-_WkQJN3p18Nc0FsuGqNXAbJrLkRd_Mzc8W1OHIb7s5TW-klX1ePkhDxkNmzSpxpHTad04hjTiIeXmeCUb52S-9m_MqU5OBYlaWn8k2zBRe_P14cmUbMa2X3aXDxKkBmNmDS12ry0czPIz_20RPgpNuRyp9mk79uGlrUPNBbNpcodULgvjr9WGN32VAu5CNMtZM-wmpSz5e6YGq9S46OdPOWkRquS3vktJ4ikx7u4fKPNXWqkhpuGM81IlQu7OC2Dy8jKkc_6QUD4gnHqvLc51R8XXQfxWLqdrkzvXR6BqvOQ_iQvntR1VPjU15D4KUKUnBFRDWM_lUdJJRGKlq1xkS37AV96Jqqb9MlXfso57uenjdfrhaF5sCd7PJIEfHRUu0QhymnCyvxfWAmY3NBi_Fk6S-XXCKOcDXhKjAVE_xMkZzidMOwZ6GR9GPBbrDkL5VheozcGm_D9lq9klpFRdScAAg7mIsYnfmNMGzmW3l4TyNpTqTvKlu5tq_9dXgqGJgVQ3L_123eLJBNwSpKG0GHcPYtMZhJeqWG0rJ2cCsyOEfd7l8CPAzY3NNA-D289SJOJTJyOLssi3eDmPj8UISR5ClAg12jdhSsMGJYuLCnbm5DngCKh9XI") + }.andExpect { status { isOk() } } // THEN val all = fitbitOAuthRepository.findAll() diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitnessServiceApplicationIT.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitnessServiceApplicationIT.kt index 0e5926dd..8fe56aa7 100644 --- a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitnessServiceApplicationIT.kt +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FitnessServiceApplicationIT.kt @@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext @SpringBootTest +@TestcontainersConfiguration class FitnessServiceApplicationIT{ @Autowired lateinit var applicationContext: ApplicationContext diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FlywayClearDatabaseJunitExtension.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FlywayClearDatabaseJunitExtension.kt new file mode 100644 index 00000000..bb540778 --- /dev/null +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/FlywayClearDatabaseJunitExtension.kt @@ -0,0 +1,18 @@ +package com.michibaum.fitness_service + +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + + +class FlywayClearDatabaseJunitExtension: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext?) { + if(context == null) throw IllegalArgumentException("Context must not be null") + + val flyway = SpringExtension.getApplicationContext(context) + .getBean(Flyway::class.java) + flyway.clean() + flyway.migrate() + } +} \ No newline at end of file diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersConfiguration.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersConfiguration.kt new file mode 100644 index 00000000..7a54e8b2 --- /dev/null +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersConfiguration.kt @@ -0,0 +1,16 @@ +package com.michibaum.fitness_service + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.annotation.Import +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.DirtiesContext.ClassMode.* +import org.springframework.test.context.TestPropertySource +import kotlin.annotation.AnnotationRetention.* +import kotlin.annotation.AnnotationTarget.* + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(value = [FlywayClearDatabaseJunitExtension::class]) +@Import(value = [TestcontainersTestConfiguration::class]) +@TestPropertySource(properties = ["spring.flyway.clean-disabled=false"]) +annotation class TestcontainersConfiguration() diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersTestConfiguration.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersTestConfiguration.kt new file mode 100644 index 00000000..bdfce427 --- /dev/null +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/TestcontainersTestConfiguration.kt @@ -0,0 +1,18 @@ +package com.michibaum.fitness_service + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.MariaDBContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersTestConfiguration { + + @Bean + @ServiceConnection + fun mariaDbContainer(): MariaDBContainer<*> { + return MariaDBContainer(DockerImageName.parse("mariadb:10.11")) + } + +} \ No newline at end of file diff --git a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/fitbit/api/weight/RepositoryIT.kt b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/fitbit/api/weight/RepositoryIT.kt index 7e6cde29..063d573d 100644 --- a/fitness-service/src/test/kotlin/com/michibaum/fitness_service/fitbit/api/weight/RepositoryIT.kt +++ b/fitness-service/src/test/kotlin/com/michibaum/fitness_service/fitbit/api/weight/RepositoryIT.kt @@ -1,9 +1,11 @@ package com.michibaum.fitness_service.fitbit.api.weight +import com.michibaum.fitness_service.TestcontainersConfiguration import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @SpringBootTest +@TestcontainersConfiguration class RepositoryIT { @Autowired diff --git a/fitness-service/src/test/resources/application.yml b/fitness-service/src/test/resources/application.yml index 33b449b0..d48d373a 100644 --- a/fitness-service/src/test/resources/application.yml +++ b/fitness-service/src/test/resources/application.yml @@ -20,17 +20,6 @@ spring: lazy-attributes-resolution: true main: allow-bean-definition-overriding: true - datasource: - username: sa - password: password - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:fitness-db - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - globally_quoted_identifiers: true eureka: instance: @@ -64,4 +53,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/gateway-service/src/main/kotlin/com/michibaum/gatewayservice/AuthenticationGatewayFilterFactory.kt b/gateway-service/src/main/kotlin/com/michibaum/gatewayservice/AuthenticationGatewayFilterFactory.kt index 29607db7..475e1d04 100644 --- a/gateway-service/src/main/kotlin/com/michibaum/gatewayservice/AuthenticationGatewayFilterFactory.kt +++ b/gateway-service/src/main/kotlin/com/michibaum/gatewayservice/AuthenticationGatewayFilterFactory.kt @@ -52,12 +52,6 @@ class AuthenticationGatewayFilterFactory( if(authHeader?.startsWith("Bearer ") == true) return authHeader.substring("Bearer ".length) } - - val authCookie = serverWebExchange.request.cookies["jwt"] - val cookieExists = authCookie?.size == 1 - if(cookieExists){ - return authCookie?.get(0)?.value - } return null } diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index f95dec16..13e89c90 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -109,3 +109,18 @@ management: info: git: mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml index a08b96e1..eab97c4f 100644 --- a/gateway-service/src/test/resources/application.yml +++ b/gateway-service/src/test/resources/application.yml @@ -64,3 +64,18 @@ management: info: git: mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/music-service/pom.xml b/music-service/pom.xml index 392f59ae..9bb5475b 100644 --- a/music-service/pom.xml +++ b/music-service/pom.xml @@ -19,7 +19,7 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-web @@ -99,8 +99,24 @@ test - com.h2database - h2 + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mariadb test diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityBeansConfiguration.kt b/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityBeansConfiguration.kt index 2a5bed02..e3f89ca3 100644 --- a/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityBeansConfiguration.kt +++ b/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityBeansConfiguration.kt @@ -1,21 +1,20 @@ package com.michibaum.music_service.security import com.michibaum.authentication_library.AuthenticationClient -import com.michibaum.authentication_library.security.ReactiveDelegateAuthenticationManager +import com.michibaum.authentication_library.security.ServletAuthenticationFilter +import com.michibaum.authentication_library.security.ServletDelegateAuthenticationManager import com.michibaum.authentication_library.security.SpecificAuthenticationManager import com.michibaum.authentication_library.security.basic.BasicAuthenticationManager -import com.michibaum.authentication_library.security.basic.BasicExchangeMatcher import com.michibaum.authentication_library.security.basic.CredentialsValidator -import com.michibaum.authentication_library.security.basic.netty.BasicAuthenticationConverter +import com.michibaum.authentication_library.security.basic.servlet.BasicAuthenticationConverter import com.michibaum.authentication_library.security.jwt.JwsValidator import com.michibaum.authentication_library.security.jwt.JwtAuthenticationManager -import com.michibaum.authentication_library.security.jwt.JwtExchangeMatcher -import com.michibaum.authentication_library.security.jwt.netty.JwtAuthenticationConverter +import com.michibaum.authentication_library.security.jwt.servlet.JwtAuthenticationConverter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.AuthenticationConverter @Configuration class SecurityBeansConfiguration { @@ -48,39 +47,22 @@ class SecurityBeansConfiguration { BasicAuthenticationManager(credentialsValidator) @Bean - fun authenticationManager(specificAuthenticationManagers: List): ReactiveAuthenticationManager = - ReactiveDelegateAuthenticationManager(specificAuthenticationManagers) + fun authenticationManager(specificAuthenticationManagers: List): AuthenticationManager = + ServletDelegateAuthenticationManager(specificAuthenticationManagers) @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter = + fun jwtAuthenticationConverter(): AuthenticationConverter = JwtAuthenticationConverter() @Bean - fun basicAuthenticationConverter(): BasicAuthenticationConverter = + fun basicAuthenticationConverter(): AuthenticationConverter = BasicAuthenticationConverter() @Bean - fun jwtAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - jwtAuthenticationConverter: JwtAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(JwtExchangeMatcher()) - setServerAuthenticationConverter(jwtAuthenticationConverter) - } - - @Bean - fun basicAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - basicAuthenticationConverter: BasicAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(BasicExchangeMatcher()) - setServerAuthenticationConverter(basicAuthenticationConverter) - } - + fun authenticationFilter(authenticationManager: AuthenticationManager, authenticationConverters: List) = + ServletAuthenticationFilter(authenticationManager, authenticationConverters) } \ No newline at end of file diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityConfiguration.kt b/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityConfiguration.kt index 78a98b9b..174604f3 100644 --- a/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityConfiguration.kt +++ b/music-service/src/main/kotlin/com/michibaum/music_service/security/SecurityConfiguration.kt @@ -1,42 +1,46 @@ package com.michibaum.music_service.security +import com.michibaum.authentication_library.security.ServletAuthenticationFilter import com.michibaum.permission_library.Permissions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity +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.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.AuthenticationWebFilter @Configuration -@EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableWebSecurity +@EnableMethodSecurity class SecurityConfiguration { @Bean fun securityFilterChain( - http: ServerHttpSecurity, - jwtAuthenticationWebFilter: AuthenticationWebFilter, - basicAuthenticationWebFilter: AuthenticationWebFilter, - ): SecurityWebFilterChain { + http: HttpSecurity, + authenticationFilter: ServletAuthenticationFilter + ): SecurityFilterChain { return http - .authorizeExchange { exchanges: AuthorizeExchangeSpec -> - exchanges - .pathMatchers( + .authorizeHttpRequests { + it + .requestMatchers( "/api/spotify/auth", ).permitAll() - .pathMatchers( + .requestMatchers( "/actuator", "/actuator/**" ).hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyExchange().authenticated() + .anyRequest().authenticated() } - .addFilterAt(basicAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } .formLogin { formLoginSpec -> formLoginSpec.disable() } .csrf { csrfSpec -> csrfSpec.disable() } diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/AbstractSpotifyApiClient.kt b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/AbstractSpotifyApiClient.kt index f7cd6bfd..8985b4fc 100644 --- a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/AbstractSpotifyApiClient.kt +++ b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/AbstractSpotifyApiClient.kt @@ -2,19 +2,15 @@ package com.michibaum.music_service.spotify.api import org.springframework.http.HttpHeaders import org.springframework.http.MediaType -import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.client.RestClient open class AbstractSpotifyApiClient { - val client = WebClient.builder() + val client = RestClient.builder() .baseUrl("https://api.spotify.com/v1") .defaultHeaders { it.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) } - .codecs { configurer -> - configurer.defaultCodecs() - .maxInMemorySize(10000000) - } .build() } diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/user/SpotifyUserApiImpl.kt b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/user/SpotifyUserApiImpl.kt index cfda9a8a..cb0d0b93 100644 --- a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/user/SpotifyUserApiImpl.kt +++ b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/api/user/SpotifyUserApiImpl.kt @@ -20,11 +20,10 @@ class SpotifyUserApiImpl: AbstractSpotifyApiClient(), SpotifyUserApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) - .bodyToMono(SpotifyMeDto::class.java) - .block() + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) + .body(SpotifyMeDto::class.java) } override fun myTopTracks(timeRange: TimeRange, credentials: SpotifyOAuthCredentials): SpotifyTopItemsDto? { @@ -35,11 +34,10 @@ class SpotifyUserApiImpl: AbstractSpotifyApiClient(), SpotifyUserApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) - .bodyToMono(SpotifyTopItemsDto::class.java) - .block() + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) + .body(SpotifyTopItemsDto::class.java) } override fun myTopArtists(timeRange: TimeRange, credentials: SpotifyOAuthCredentials): SpotifyTopItemsDto? { @@ -50,11 +48,10 @@ class SpotifyUserApiImpl: AbstractSpotifyApiClient(), SpotifyUserApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) - .bodyToMono(SpotifyTopItemsDto::class.java) - .block() + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) + .body(SpotifyTopItemsDto::class.java) } override fun profileFor(userId: String, credentials: SpotifyOAuthCredentials): SpotifyUserDto? { @@ -64,10 +61,9 @@ class SpotifyUserApiImpl: AbstractSpotifyApiClient(), SpotifyUserApi { it.setBearerAuth(credentials.accessToken) } .retrieve() - .onStatus({ t -> t.value() == 401 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 403 }, { Mono.error(Exception()) }) - .onStatus({ t -> t.value() == 429 }, { Mono.error(Exception()) }) - .bodyToMono(SpotifyUserDto::class.java) - .block() + .onStatus({ t -> t.value() == 401 }, { _, _ -> }) + .onStatus({ t -> t.value() == 403 }, { _, _ -> }) + .onStatus({ t -> t.value() == 429 }, { _, _ -> }) + .body(SpotifyUserDto::class.java) } } \ No newline at end of file diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthController.kt b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthController.kt index 4ae9e807..043b5e36 100644 --- a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthController.kt +++ b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthController.kt @@ -6,9 +6,7 @@ import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import org.springframework.web.reactive.function.BodyInserters -import org.springframework.web.reactive.function.client.WebClient -import reactor.core.publisher.Mono +import org.springframework.web.client.RestClient import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.util.* @@ -19,13 +17,13 @@ class SpotifyOAuthController( private val spotifyOAuthProperties: SpotifyOAuthProperties ) { // https://developer.spotify.com/documentation/web-api/tutorials/code-flow - private final val client: WebClient + private final val client: RestClient init { val clientAndSecret = spotifyOAuthProperties.clientId + ":" + spotifyOAuthProperties.clientSecret val authBasic = Base64.getUrlEncoder().withoutPadding().encodeToString(clientAndSecret.encodeToByteArray()) - client = WebClient.builder() + client = RestClient.builder() .baseUrl("https://accounts.spotify.com") .defaultHeaders { it.set("Authorization", "Basic $authBasic") @@ -66,19 +64,15 @@ class SpotifyOAuthController( fun authorizationCode(@RequestParam code: String, @RequestParam state: String) { val oAuthData = spotifyOAuthService.findByState(state) ?: throw Exception("No oAuthData found by state $state") + val data = SpotifyOAuthDto(code, "authorization_code", "https://music.michibaum.ch/api/spotify/auth") val response = client .post() .uri("/api/token") .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body( - BodyInserters.fromFormData("code", code) - .with("grant_type", "authorization_code") - .with("redirect_uri", "https://music.michibaum.ch/api/spotify/auth") - ) + .body(data)// TODO needs testing .retrieve() - .onStatus({ t -> t.is4xxClientError }, { Mono.error(Exception()) }) - .bodyToMono(SpotifyOAuthCredentialsDto::class.java) - .block() + .onStatus({ t -> t.is4xxClientError }, { _, _ -> }) + .body(SpotifyOAuthCredentialsDto::class.java) if(response == null){ throw Exception("Spotify OAuth exchange for access token returned null") diff --git a/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthDto.kt b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthDto.kt new file mode 100644 index 00000000..e9accfed --- /dev/null +++ b/music-service/src/main/kotlin/com/michibaum/music_service/spotify/oauth/SpotifyOAuthDto.kt @@ -0,0 +1,7 @@ +package com.michibaum.music_service.spotify.oauth + +data class SpotifyOAuthDto( + val code: String, + val grantType: String, + val redirectUri: String +) \ No newline at end of file diff --git a/music-service/src/main/resources/application.yml b/music-service/src/main/resources/application.yml index d9e39c91..78f08f98 100644 --- a/music-service/src/main/resources/application.yml +++ b/music-service/src/main/resources/application.yml @@ -65,4 +65,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/music-service/src/test/kotlin/com/michibaum/music_service/ActuatorIT.kt b/music-service/src/test/kotlin/com/michibaum/music_service/ActuatorIT.kt new file mode 100644 index 00000000..5096ce9e --- /dev/null +++ b/music-service/src/test/kotlin/com/michibaum/music_service/ActuatorIT.kt @@ -0,0 +1,55 @@ +package com.michibaum.music_service + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +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.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.* + +@AutoConfigureMockMvc +@SpringBootTest(properties = [ + "spring.boot.admin.client.username=someUsername", + "spring.boot.admin.client.password=somePasswööörd" +]) +@TestcontainersConfiguration +class ActuatorIT { + + @Autowired + lateinit var mockMvc: MockMvc + + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ + // GIVEN + + // WHEN + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) + + // THEN + + } + + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ + // GIVEN + val basicAuth = "someUsername:somePasswööörd" + val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) + + // WHEN + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) + + // THEN + + } + +} \ No newline at end of file diff --git a/music-service/src/test/kotlin/com/michibaum/music_service/FlywayClearDatabaseJunitExtension.kt b/music-service/src/test/kotlin/com/michibaum/music_service/FlywayClearDatabaseJunitExtension.kt new file mode 100644 index 00000000..47042844 --- /dev/null +++ b/music-service/src/test/kotlin/com/michibaum/music_service/FlywayClearDatabaseJunitExtension.kt @@ -0,0 +1,18 @@ +package com.michibaum.music_service + +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + + +class FlywayClearDatabaseJunitExtension: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext?) { + if(context == null) throw IllegalArgumentException("Context must not be null") + + val flyway = SpringExtension.getApplicationContext(context) + .getBean(Flyway::class.java) + flyway.clean() + flyway.migrate() + } +} \ No newline at end of file diff --git a/music-service/src/test/kotlin/com/michibaum/music_service/MusicServiceApplicationIT.kt b/music-service/src/test/kotlin/com/michibaum/music_service/MusicServiceApplicationIT.kt index e3f06307..5ac3028e 100644 --- a/music-service/src/test/kotlin/com/michibaum/music_service/MusicServiceApplicationIT.kt +++ b/music-service/src/test/kotlin/com/michibaum/music_service/MusicServiceApplicationIT.kt @@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext @SpringBootTest +@TestcontainersConfiguration class MusicServiceApplicationIT{ @Autowired lateinit var applicationContext: ApplicationContext diff --git a/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersConfiguration.kt b/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersConfiguration.kt new file mode 100644 index 00000000..d094536a --- /dev/null +++ b/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersConfiguration.kt @@ -0,0 +1,12 @@ +package com.michibaum.music_service + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.annotation.Import +import org.springframework.test.context.TestPropertySource + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(value = [FlywayClearDatabaseJunitExtension::class]) +@Import(value = [TestcontainersTestConfiguration::class]) +@TestPropertySource(properties = ["spring.flyway.clean-disabled=false"]) +annotation class TestcontainersConfiguration() diff --git a/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersTestConfiguration.kt b/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersTestConfiguration.kt new file mode 100644 index 00000000..835471fd --- /dev/null +++ b/music-service/src/test/kotlin/com/michibaum/music_service/TestcontainersTestConfiguration.kt @@ -0,0 +1,18 @@ +package com.michibaum.music_service + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.MariaDBContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersTestConfiguration { + + @Bean + @ServiceConnection + fun mariaDbContainer(): MariaDBContainer<*> { + return MariaDBContainer(DockerImageName.parse("mariadb:10.11")) + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9dedfbc7..1f6d7969 100644 --- a/pom.xml +++ b/pom.xml @@ -336,7 +336,6 @@ ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java diff --git a/registry-service/src/main/resources/application.yml b/registry-service/src/main/resources/application.yml index 96683f3a..6744ba2e 100644 --- a/registry-service/src/main/resources/application.yml +++ b/registry-service/src/main/resources/application.yml @@ -56,4 +56,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/registry-service/src/test/kotlin/com/michibaum/registry_service/ActuatorIT.kt b/registry-service/src/test/kotlin/com/michibaum/registry_service/ActuatorIT.kt new file mode 100644 index 00000000..5cefa1ad --- /dev/null +++ b/registry-service/src/test/kotlin/com/michibaum/registry_service/ActuatorIT.kt @@ -0,0 +1,54 @@ +package com.michibaum.registry_service + +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +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.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.util.* + +@AutoConfigureMockMvc +@SpringBootTest(properties = [ + "spring.boot.admin.client.username=someUsername", + "spring.boot.admin.client.password=somePasswööörd" +]) +class ActuatorIT { + + @Autowired + lateinit var mockMvc: MockMvc + + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ + // GIVEN + + // WHEN + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) + + // THEN + + } + + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ + // GIVEN + val basicAuth = "someUsername:somePasswööörd" + val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) + + // WHEN + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) + + // THEN + + } + +} \ No newline at end of file diff --git a/registry-service/src/test/resources/application.yml b/registry-service/src/test/resources/application.yml index c46b5016..20eccded 100644 --- a/registry-service/src/test/resources/application.yml +++ b/registry-service/src/test/resources/application.yml @@ -54,4 +54,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/usermanagement-service/pom.xml b/usermanagement-service/pom.xml index 791801d7..be68a15a 100644 --- a/usermanagement-service/pom.xml +++ b/usermanagement-service/pom.xml @@ -21,7 +21,7 @@ org.springframework.boot - spring-boot-starter-webflux + spring-boot-starter-web org.springframework.boot @@ -107,8 +107,24 @@ test - com.h2database - h2 + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + mariadb test diff --git a/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityBeansConfiguration.kt b/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityBeansConfiguration.kt index 8ea273ae..bf34ec32 100644 --- a/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityBeansConfiguration.kt +++ b/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityBeansConfiguration.kt @@ -1,21 +1,20 @@ package com.michibaum.usermanagement_service.security import com.michibaum.authentication_library.AuthenticationClient -import com.michibaum.authentication_library.security.ReactiveDelegateAuthenticationManager +import com.michibaum.authentication_library.security.ServletAuthenticationFilter +import com.michibaum.authentication_library.security.ServletDelegateAuthenticationManager import com.michibaum.authentication_library.security.SpecificAuthenticationManager import com.michibaum.authentication_library.security.basic.BasicAuthenticationManager -import com.michibaum.authentication_library.security.basic.BasicExchangeMatcher import com.michibaum.authentication_library.security.basic.CredentialsValidator -import com.michibaum.authentication_library.security.basic.netty.BasicAuthenticationConverter +import com.michibaum.authentication_library.security.basic.servlet.BasicAuthenticationConverter import com.michibaum.authentication_library.security.jwt.JwsValidator import com.michibaum.authentication_library.security.jwt.JwtAuthenticationManager -import com.michibaum.authentication_library.security.jwt.JwtExchangeMatcher -import com.michibaum.authentication_library.security.jwt.netty.JwtAuthenticationConverter +import com.michibaum.authentication_library.security.jwt.servlet.JwtAuthenticationConverter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Lazy -import org.springframework.security.authentication.ReactiveAuthenticationManager -import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.web.authentication.AuthenticationConverter @Configuration class SecurityBeansConfiguration { @@ -47,39 +46,22 @@ class SecurityBeansConfiguration { BasicAuthenticationManager(credentialsValidator) @Bean - fun authenticationManager(specificAuthenticationManagers: List): ReactiveAuthenticationManager = - ReactiveDelegateAuthenticationManager(specificAuthenticationManagers) + fun authenticationManager(specificAuthenticationManagers: List): AuthenticationManager = + ServletDelegateAuthenticationManager(specificAuthenticationManagers) @Bean - fun jwtAuthenticationConverter(): JwtAuthenticationConverter = + fun jwtAuthenticationConverter(): AuthenticationConverter = JwtAuthenticationConverter() @Bean - fun basicAuthenticationConverter(): BasicAuthenticationConverter = + fun basicAuthenticationConverter(): AuthenticationConverter = BasicAuthenticationConverter() @Bean - fun jwtAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - jwtAuthenticationConverter: JwtAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(JwtExchangeMatcher()) - setServerAuthenticationConverter(jwtAuthenticationConverter) - } - - @Bean - fun basicAuthenticationWebFilter( - authenticationManager: ReactiveAuthenticationManager, - basicAuthenticationConverter: BasicAuthenticationConverter - ) = - AuthenticationWebFilter(authenticationManager).apply { - setRequiresAuthenticationMatcher(BasicExchangeMatcher()) - setServerAuthenticationConverter(basicAuthenticationConverter) - } - + fun authenticationFilter(authenticationManager: AuthenticationManager, authenticationConverters: List) = + ServletAuthenticationFilter(authenticationManager, authenticationConverters) } \ No newline at end of file diff --git a/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityConfiguration.kt b/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityConfiguration.kt index 624b32eb..1aa3811c 100644 --- a/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityConfiguration.kt +++ b/usermanagement-service/src/main/kotlin/com/michibaum/usermanagement_service/security/SecurityConfiguration.kt @@ -1,38 +1,42 @@ package com.michibaum.usermanagement_service.security +import com.michibaum.authentication_library.security.ServletAuthenticationFilter import com.michibaum.permission_library.Permissions import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod.POST +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity +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.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.authentication.AuthenticationWebFilter @Configuration -@EnableWebFluxSecurity -@EnableReactiveMethodSecurity +@EnableWebSecurity +@EnableMethodSecurity class SecurityConfiguration { @Bean fun securityFilterChain( - http: ServerHttpSecurity, - jwtAuthenticationWebFilter: AuthenticationWebFilter, - basicAuthenticationWebFilter: AuthenticationWebFilter, - ): SecurityWebFilterChain { + http: HttpSecurity, + authenticationFilter: ServletAuthenticationFilter + ): SecurityFilterChain { return http - .authorizeExchange { exchanges: AuthorizeExchangeSpec -> - exchanges - .pathMatchers(POST,"/api/checkUserDetails", "/api/users").permitAll() - .pathMatchers("/actuator", "/actuator/**").hasAnyAuthority(Permissions.ADMIN_SERVICE.name) - .anyExchange().authenticated() + .authorizeHttpRequests { + it + .requestMatchers(POST,"/api/checkUserDetails", "/api/users").permitAll() + .requestMatchers("/actuator", "/actuator/**").hasAnyAuthority(Permissions.ADMIN_SERVICE.name) + .anyRequest().authenticated() } - .addFilterAt(basicAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) - .addFilterAt(jwtAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .httpBasic { httpBasicSpec -> httpBasicSpec.disable() } .formLogin { formLoginSpec -> formLoginSpec.disable() } .csrf { csrfSpec -> csrfSpec.disable() } diff --git a/usermanagement-service/src/main/resources/application.yml b/usermanagement-service/src/main/resources/application.yml index ba05c1df..49d5751d 100644 --- a/usermanagement-service/src/main/resources/application.yml +++ b/usermanagement-service/src/main/resources/application.yml @@ -65,4 +65,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/ActuatorIT.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/ActuatorIT.kt index e0d152b2..ff01e60c 100644 --- a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/ActuatorIT.kt +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/ActuatorIT.kt @@ -1,116 +1,55 @@ package com.michibaum.usermanagement_service import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import java.util.* -@AutoConfigureWebTestClient +@AutoConfigureMockMvc @SpringBootTest(properties = [ "spring.boot.admin.client.username=someUsername", "spring.boot.admin.client.password=somePasswööörd" ]) +@TestcontainersConfiguration class ActuatorIT { @Autowired - lateinit var webClient: WebTestClient + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.get() - .uri("/actuator") - .exchange() - .expectStatus() - .isUnauthorized + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/health") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.get() - .uri("/actuator/info") - .exchange() - .expectStatus() - .isUnauthorized - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.get() - .uri("/actuator/health") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.get() - .uri("/actuator/info") - .headers { it.setBasicAuth(basicAuthEncoded) } - .exchange() - .expectStatus() - .isOk + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/FlywayClearDatabaseJunitExtension.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/FlywayClearDatabaseJunitExtension.kt new file mode 100644 index 00000000..4fc52d8f --- /dev/null +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/FlywayClearDatabaseJunitExtension.kt @@ -0,0 +1,18 @@ +package com.michibaum.usermanagement_service + +import org.flywaydb.core.Flyway +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.test.context.junit.jupiter.SpringExtension + + +class FlywayClearDatabaseJunitExtension: BeforeEachCallback { + override fun beforeEach(context: ExtensionContext?) { + if(context == null) throw IllegalArgumentException("Context must not be null") + + val flyway = SpringExtension.getApplicationContext(context) + .getBean(Flyway::class.java) + flyway.clean() + flyway.migrate() + } +} \ No newline at end of file diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersConfiguration.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersConfiguration.kt new file mode 100644 index 00000000..0ae48562 --- /dev/null +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersConfiguration.kt @@ -0,0 +1,12 @@ +package com.michibaum.usermanagement_service + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.context.annotation.Import +import org.springframework.test.context.TestPropertySource + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(value = [FlywayClearDatabaseJunitExtension::class]) +@Import(value = [TestcontainersTestConfiguration::class]) +@TestPropertySource(properties = ["spring.flyway.clean-disabled=false"]) +annotation class TestcontainersConfiguration() diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersTestConfiguration.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersTestConfiguration.kt new file mode 100644 index 00000000..0f7023be --- /dev/null +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/TestcontainersTestConfiguration.kt @@ -0,0 +1,18 @@ +package com.michibaum.usermanagement_service + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean +import org.testcontainers.containers.MariaDBContainer +import org.testcontainers.utility.DockerImageName + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersTestConfiguration { + + @Bean + @ServiceConnection + fun mariaDbContainer(): MariaDBContainer<*> { + return MariaDBContainer(DockerImageName.parse("mariadb:10.11")) + } + +} \ No newline at end of file diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UserRepositoryIT.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UserRepositoryIT.kt index 62a0becd..89c48e5a 100644 --- a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UserRepositoryIT.kt +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UserRepositoryIT.kt @@ -3,9 +3,12 @@ package com.michibaum.usermanagement_service import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestcontainersConfiguration class UserRepositoryIT { @Autowired diff --git a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UsermanagementServiceApplicationIT.kt b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UsermanagementServiceApplicationIT.kt index 2eee4b41..4529978e 100644 --- a/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UsermanagementServiceApplicationIT.kt +++ b/usermanagement-service/src/test/kotlin/com/michibaum/usermanagement_service/UsermanagementServiceApplicationIT.kt @@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext @SpringBootTest +@TestcontainersConfiguration class UsermanagementServiceApplicationIT { @Autowired diff --git a/usermanagement-service/src/test/resources/application.yml b/usermanagement-service/src/test/resources/application.yml index 0ca71755..ac7c9996 100644 --- a/usermanagement-service/src/test/resources/application.yml +++ b/usermanagement-service/src/test/resources/application.yml @@ -20,17 +20,6 @@ spring: lazy-attributes-resolution: true main: allow-bean-definition-overriding: true - datasource: - username: sa - password: password - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:usermanagement-db - jpa: - hibernate: - ddl-auto: create-drop - properties: - hibernate: - globally_quoted_identifiers: true eureka: instance: @@ -64,4 +53,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/website-service/src/main/resources/application.yml b/website-service/src/main/resources/application.yml index 65f18452..b1b65a2c 100644 --- a/website-service/src/main/resources/application.yml +++ b/website-service/src/main/resources/application.yml @@ -57,4 +57,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/website-service/src/test/kotlin/com/michibaum/websiteservice/ActuatorIT.kt b/website-service/src/test/kotlin/com/michibaum/websiteservice/ActuatorIT.kt index 25bb21f6..c14eaa09 100644 --- a/website-service/src/test/kotlin/com/michibaum/websiteservice/ActuatorIT.kt +++ b/website-service/src/test/kotlin/com/michibaum/websiteservice/ActuatorIT.kt @@ -1,6 +1,7 @@ package com.michibaum.websiteservice -import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest @@ -17,81 +18,34 @@ import java.util.* class ActuatorIT { @Autowired - lateinit var webClient: MockMvc + lateinit var mockMvc: MockMvc - @Test - fun `actuator without authentication returns 401`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints return 401`(endpoint: String){ // GIVEN // WHEN - webClient.perform(get("/actuator")) - .andExpect(status().isUnauthorized()) + mockMvc.perform(get(endpoint)) + .andExpect(status().isUnauthorized) // THEN } - @Test - fun `actuator health without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.perform(get("/actuator/health")) - .andExpect(status().isUnauthorized()) - - // THEN - - } - - @Test - fun `actuator info without authentication returns 401`(){ - // GIVEN - - // WHEN - webClient.perform(get("/actuator/info")) - .andExpect(status().isUnauthorized()) - - // THEN - - } - - @Test - fun `actuator with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.perform(get("/actuator").header("Authorization", "Basic $basicAuthEncoded")) - .andExpect(status().isOk()) // TODO returns 302 redirect to / because of success authentication - - // THEN - - } - - @Test - fun `actuator health with authentication returns 200`(){ - // GIVEN - val basicAuth = "someUsername:somePasswööörd" - val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) - - // WHEN - webClient.perform(get("/actuator/health").header("Authorization", "Basic $basicAuthEncoded")) - .andExpect(status().isOk()) // TODO returns 302 redirect to / because of success authentication - - // THEN - - } - - @Test - fun `actuator info with authentication returns 200`(){ + @ParameterizedTest + @ValueSource(strings = ["/actuator", "/actuator/health", "/actuator/info"]) + fun `actuator endpoints with basic authentication return 200`(endpoint: String){ // GIVEN val basicAuth = "someUsername:somePasswööörd" val basicAuthEncoded = Base64.getEncoder().encodeToString(basicAuth.toByteArray()) // WHEN - webClient.perform(get("/actuator/info").header("Authorization", "Basic $basicAuthEncoded")) - .andExpect(status().isOk()) // TODO returns 302 redirect to / because of success authentication + mockMvc.perform( + get(endpoint) + .header("Authorization", "Basic $basicAuthEncoded") + ) + .andExpect(status().isOk) // THEN diff --git a/website-service/src/test/resources/application.yml b/website-service/src/test/resources/application.yml index 4f5ea5d5..9d4c345b 100644 --- a/website-service/src/test/resources/application.yml +++ b/website-service/src/test/resources/application.yml @@ -55,4 +55,19 @@ management: enabled: true info: git: - mode: full \ No newline at end of file + mode: full + enabled: true + build: + enabled: true + defaults: + enabled: true + env: + enabled: true + java: + enabled: true + os: + enabled: true + process: + enabled: true + ssl: + enabled: true \ No newline at end of file diff --git a/website/src/app/app.config.ts b/website/src/app/app.config.ts index ea2ea37b..5ca6c981 100644 --- a/website/src/app/app.config.ts +++ b/website/src/app/app.config.ts @@ -70,7 +70,7 @@ export const appConfig: ApplicationConfig = { provideAppInitializer(() => { const initializerFn = (appInitializerFactory)(inject(TranslateService)); return initializerFn(); - }), + }), { provide : HTTP_INTERCEPTORS, useClass: AuthJwtInterceptor, diff --git a/website/src/app/chess-settings/chess-update-event/chess-update-event.component.ts b/website/src/app/chess-settings/chess-update-event/chess-update-event.component.ts index a6796d7c..c3bbf708 100644 --- a/website/src/app/chess-settings/chess-update-event/chess-update-event.component.ts +++ b/website/src/app/chess-settings/chess-update-event/chess-update-event.component.ts @@ -16,7 +16,7 @@ import {MultiSelect} from "primeng/multiselect"; import {Button} from "primeng/button"; import {InputText} from "primeng/inputtext"; import {FaIconComponent} from "@fortawesome/angular-fontawesome"; -import {NgClass, NgIf} from "@angular/common"; +import {NgIf} from "@angular/common"; import {PickList} from "primeng/picklist"; import {LazyLoad} from "../../core/models/lazy-load.model"; import {rxResource} from "@angular/core/rxjs-interop"; @@ -39,8 +39,7 @@ import {Textarea} from "primeng/textarea"; PickList, FormsModule, EventIconPipe, - Textarea, - NgClass + Textarea ], templateUrl: './chess-update-event.component.html', styleUrl: './chess-update-event.component.scss' diff --git a/website/src/app/core/interceptors/auth-cookie.interceptor.ts b/website/src/app/core/interceptors/auth-cookie.interceptor.ts deleted file mode 100644 index 7abe7ef5..00000000 --- a/website/src/app/core/interceptors/auth-cookie.interceptor.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Injectable} from "@angular/core"; -import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; -import {Observable} from "rxjs"; - -@Injectable() -export class AuthCookieInterceptor implements HttpInterceptor{ - - intercept(req: HttpRequest, next: HttpHandler): Observable> { - const modifiedRequest = req.clone({ - withCredentials: true, - }); - return next.handle(modifiedRequest); - } - -} diff --git a/website/src/app/core/models/admin/admin.model.ts b/website/src/app/core/models/admin/admin.model.ts new file mode 100644 index 00000000..49122e18 --- /dev/null +++ b/website/src/app/core/models/admin/admin.model.ts @@ -0,0 +1,23 @@ +export interface Application { + name: string + instances: Instance[] + status: Status +} + +export interface Instance { + id: string + version: number + statusInfo: StatusInfo +} + +export interface StatusInfo { + status: Status + details: any // TODO how +} + +export enum Status { + UP = 'UP', + DOWN = 'DOWN', + OUT_OF_SERVICE = 'OUT_OF_SERVICE', + UNKNOWN = 'UNKNOWN' +} diff --git a/website/src/app/core/models/chess/chess.models.ts b/website/src/app/core/models/chess/chess.models.ts index 76e94fff..98fae7fc 100644 --- a/website/src/app/core/models/chess/chess.models.ts +++ b/website/src/app/core/models/chess/chess.models.ts @@ -38,7 +38,8 @@ export interface Account { export enum ChessPlatform{ CHESSCOM="CHESSCOM", LICHESS="LICHESS", - FIDE="FIDE" + FIDE="FIDE", + FREESTYLE="FREESTYLE" } export enum ChessGameType{ diff --git a/website/src/app/core/services/admin.service.ts b/website/src/app/core/services/admin.service.ts new file mode 100644 index 00000000..02e52876 --- /dev/null +++ b/website/src/app/core/services/admin.service.ts @@ -0,0 +1,21 @@ +import {inject, Injectable} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import {catchError, Observable} from "rxjs"; +import {environment} from "../../../environments/environment"; +import {HttpErrorHandler} from "../config/http-error-handler.service"; +import {UserInfoService} from "./user-info.service"; +import {Application} from "../models/admin/admin.model"; + +@Injectable({providedIn: 'root'}) +export class AdminService { + private readonly http = inject(HttpClient); + private readonly httpErrorConfig = inject(HttpErrorHandler); + private readonly userInfoService = inject(UserInfoService); + + applications(): Observable { + return this.http.get(environment.adminService + '/applications', {headers: {"Accept": "application/json"}}) + .pipe(catchError(err => this.httpErrorConfig.handleError(err, this.userInfoService))); + } + + +} diff --git a/website/src/app/core/services/auth.service.ts b/website/src/app/core/services/auth.service.ts index 395cdac9..72a8555b 100644 --- a/website/src/app/core/services/auth.service.ts +++ b/website/src/app/core/services/auth.service.ts @@ -37,9 +37,6 @@ export class AuthService { logout(){ localStorage.removeItem('Authentication'); - this.http.post(environment.authenticationService + '/logout', {}) - .pipe(catchError(err => this.httpErrorConfig.handleError(err, this.userInfoService))) - .subscribe(); this.logoutEmitter.next(); this.router.home() } diff --git a/website/src/app/login/login.component.ts b/website/src/app/login/login.component.ts index ea3ca50b..74103879 100644 --- a/website/src/app/login/login.component.ts +++ b/website/src/app/login/login.component.ts @@ -62,6 +62,7 @@ export class LoginComponent implements OnInit{ if(username == "" || password == "") return; + this.authService.logout(); this.authService.login(username, password) } diff --git a/website/src/app/microservice-overview/microservice-overview.component.html b/website/src/app/microservice-overview/microservice-overview.component.html index e69de29b..c73ff582 100644 --- a/website/src/app/microservice-overview/microservice-overview.component.html +++ b/website/src/app/microservice-overview/microservice-overview.component.html @@ -0,0 +1 @@ + diff --git a/website/src/app/microservice-overview/microservice-overview.component.ts b/website/src/app/microservice-overview/microservice-overview.component.ts index c4e0fedd..b8c10f65 100644 --- a/website/src/app/microservice-overview/microservice-overview.component.ts +++ b/website/src/app/microservice-overview/microservice-overview.component.ts @@ -1,16 +1,20 @@ import {Component, inject, OnInit} from '@angular/core'; import {HeaderService} from "../core/services/header.service"; import {Sides} from "../core/config/sides"; +import {MsApplicationsComponent} from "./ms-applications/ms-applications.component"; @Component({ selector: 'app-microservice-overview', - imports: [], + imports: [ + MsApplicationsComponent + ], templateUrl: './microservice-overview.component.html', styleUrl: './microservice-overview.component.scss' }) export class MicroserviceOverviewComponent implements OnInit{ private readonly headerService = inject(HeaderService); + ngOnInit(): void { this.headerService.changeTitle(Sides.microservices.translationKey) } diff --git a/website/src/app/microservice-overview/ms-application/ms-application.component.html b/website/src/app/microservice-overview/ms-application/ms-application.component.html new file mode 100644 index 00000000..8bca82d0 --- /dev/null +++ b/website/src/app/microservice-overview/ms-application/ms-application.component.html @@ -0,0 +1,11 @@ + + + {{application()?.name}} + + + + + {{instance.id}} + + + diff --git a/website/src/app/microservice-overview/ms-application/ms-application.component.scss b/website/src/app/microservice-overview/ms-application/ms-application.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/website/src/app/microservice-overview/ms-application/ms-application.component.ts b/website/src/app/microservice-overview/ms-application/ms-application.component.ts new file mode 100644 index 00000000..a16a4a0f --- /dev/null +++ b/website/src/app/microservice-overview/ms-application/ms-application.component.ts @@ -0,0 +1,25 @@ +import {Component, input, Input} from '@angular/core'; +import {Card} from "primeng/card"; +import {NgForOf} from "@angular/common"; +import {Application} from "../../core/models/admin/admin.model"; +import {MsStatusIconPipe} from "../pipes/ms-status-icon.pipe"; +import {FaIconComponent} from "@fortawesome/angular-fontawesome"; +import {EventIconColorPipe} from "../../core/pipes/event-icon-color.pipe"; +import {MsStatusStylePipe} from "../pipes/ms-status-style.pipe"; + +@Component({ + selector: 'app-ms-application', + imports: [ + Card, + NgForOf, + MsStatusIconPipe, + FaIconComponent, + EventIconColorPipe, + MsStatusStylePipe + ], + templateUrl: './ms-application.component.html', + styleUrl: './ms-application.component.scss' +}) +export class MsApplicationComponent { + application = input() +} diff --git a/website/src/app/microservice-overview/ms-applications/ms-applications.component.html b/website/src/app/microservice-overview/ms-applications/ms-applications.component.html new file mode 100644 index 00000000..5b062ab0 --- /dev/null +++ b/website/src/app/microservice-overview/ms-applications/ms-applications.component.html @@ -0,0 +1,12 @@ +
+
+ + + + + + + + +
+
diff --git a/website/src/app/microservice-overview/ms-applications/ms-applications.component.scss b/website/src/app/microservice-overview/ms-applications/ms-applications.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/website/src/app/microservice-overview/ms-applications/ms-applications.component.ts b/website/src/app/microservice-overview/ms-applications/ms-applications.component.ts new file mode 100644 index 00000000..724e96c4 --- /dev/null +++ b/website/src/app/microservice-overview/ms-applications/ms-applications.component.ts @@ -0,0 +1,25 @@ +import {Component, inject} from '@angular/core'; +import {Card} from "primeng/card"; +import {NgForOf, NgIf} from "@angular/common"; +import {AdminService} from "../../core/services/admin.service"; +import {rxResource} from "@angular/core/rxjs-interop"; +import {MsApplicationComponent} from "../ms-application/ms-application.component"; + +@Component({ + selector: 'app-ms-applications', + imports: [ + Card, + NgForOf, + NgIf, + MsApplicationComponent + ], + templateUrl: './ms-applications.component.html', + styleUrl: './ms-applications.component.scss' +}) +export class MsApplicationsComponent { + private readonly adminService = inject(AdminService); + + applications = rxResource({ + loader: () => this.adminService.applications() + }) +} diff --git a/website/src/app/microservice-overview/pipes/ms-status-icon.pipe.ts b/website/src/app/microservice-overview/pipes/ms-status-icon.pipe.ts new file mode 100644 index 00000000..6901d561 --- /dev/null +++ b/website/src/app/microservice-overview/pipes/ms-status-icon.pipe.ts @@ -0,0 +1,34 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import {IconDefinition} from "@fortawesome/angular-fontawesome"; +import {faCircleCheck, faCircleExclamation, faCircleQuestion, faCircleXmark} from "@fortawesome/free-solid-svg-icons"; +import {Status} from "../../core/models/admin/admin.model"; + +@Pipe({ + name: 'msStatusIcon' +}) +export class MsStatusIconPipe implements PipeTransform { + + transform(value: String | undefined, ...args: unknown[]): IconDefinition { + if (value === undefined) { + return faCircleQuestion + } + + if (value === Status.UP) { + return faCircleCheck + } + + if (value === Status.DOWN) { + return faCircleXmark + } + + if (value === Status.UNKNOWN) { + return faCircleQuestion + } + if (value === Status.OUT_OF_SERVICE){ + return faCircleExclamation + } + + return faCircleQuestion + } + +} diff --git a/website/src/app/microservice-overview/pipes/ms-status-style.pipe.ts b/website/src/app/microservice-overview/pipes/ms-status-style.pipe.ts new file mode 100644 index 00000000..7472905c --- /dev/null +++ b/website/src/app/microservice-overview/pipes/ms-status-style.pipe.ts @@ -0,0 +1,33 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import {Status} from "../../core/models/admin/admin.model"; +import {faCircleCheck, faCircleExclamation, faCircleQuestion, faCircleXmark} from "@fortawesome/free-solid-svg-icons"; + +@Pipe({ + name: 'msStatusStyle' +}) +export class MsStatusStylePipe implements PipeTransform { + + transform(value: String | undefined, ...args: unknown[]): String { + if (value === undefined) { + return "color: yellow" + } + + if (value === Status.UP) { + return "color: green" + } + + if (value === Status.DOWN) { + return "color: red" + } + + if (value === Status.UNKNOWN) { + return "color: yellow" + } + if (value === Status.OUT_OF_SERVICE){ + return "color: orange" + } + + return "color: yellow" + } + +}