From 1d1406c321394dec188cc7d94d32f6608173da6c Mon Sep 17 00:00:00 2001 From: VictorDelCampo Date: Mon, 16 Dec 2024 10:14:31 +0100 Subject: [PATCH 01/11] Initial work on authentication RISDEV-5805 --- backend/build.gradle.kts | 1 + backend/gradle/libs.versions.toml | 1 + .../ris/norms/config/SecurityConfig.java | 7 +++---- backend/src/main/resources/application.yaml | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 8137da074..a4bc66ded 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(platform(libs.aws.bom)) implementation(libs.aws.s3) implementation(libs.squareup.okio.jvm) + implementation(libs.spring.oauth2.client) compileOnly(libs.lombok) diff --git a/backend/gradle/libs.versions.toml b/backend/gradle/libs.versions.toml index 62c78037d..420ca4232 100644 --- a/backend/gradle/libs.versions.toml +++ b/backend/gradle/libs.versions.toml @@ -35,6 +35,7 @@ spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-star spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-cloud-starter-kubernetes-client-config = "org.springframework.cloud:spring-cloud-starter-kubernetes-client-config:3.2.0" +spring-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-security-test = "org.springframework.security:spring-security-test:6.4.1" spring-session-data-redis = { module = "org.springframework.session:spring-session-data-redis" } spring-starter-data-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis" } diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java index c1a5cad25..fbc37c8b0 100644 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -34,15 +35,13 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { "/favicon.svg", "/actuator/health/**", "/actuator/prometheus", - "/api/**", - "/index.html", - "/", "/assets/**" ) .permitAll() .anyRequest() - .denyAll() + .authenticated() ) + .oauth2Login(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 309ce75dc..e9a73980a 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -18,6 +18,22 @@ spring: multipart: max-file-size: 10MB max-request-size: 10MB + + security: + oauth2: + client: + registration: + oidcclient: + provider: keycloak + scope: + - openid + - profile + - email + client-id: #replace-me# + client-secret: #replace-me# + provider: + keycloak: + issuer-uri: #replace-me# server: servlet: session: From 2fac499e8390c9d6c340923439c6a28884efb26b Mon Sep 17 00:00:00 2001 From: VictorDelCampo Date: Mon, 16 Dec 2024 14:03:47 +0100 Subject: [PATCH 02/11] Add explicit ports for webapp RISDEV-5805 --- docker-compose.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index e7ee2a532..4db3699ce 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,12 +1,4 @@ services: - nginx: - build: - context: . - dockerfile: DockerfileNginxPlaywright - ports: - - 8080:8080 - depends_on: - - webapp webapp: image: ris-norms-app:001 build: @@ -16,6 +8,8 @@ services: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres14:5432/risnorms - REDIS_HOST=redis - SPRING_PROFILES_ACTIVE=local + ports: + - "8080:8080" depends_on: - postgres14 - redis From 11d3d485f88890d334dd26ae244a6d9781964604 Mon Sep 17 00:00:00 2001 From: Sebastian Rossa Date: Tue, 17 Dec 2024 10:38:24 +0100 Subject: [PATCH 03/11] Add mock user for functional tests RISDEV-5805 --- .../input/restapi/controller/AnnouncementControllerTest.java | 2 ++ .../adapter/input/restapi/controller/ArticleControllerTest.java | 2 ++ .../adapter/input/restapi/controller/ElementControllerTest.java | 2 ++ .../input/restapi/controller/NormExpressionControllerTest.java | 2 ++ .../restapi/controller/NormManifestationControllerTest.java | 2 ++ .../input/restapi/controller/ProprietaryControllerTest.java | 2 ++ .../input/restapi/controller/RenderingControllerTest.java | 2 ++ .../input/restapi/controller/TimeBoundaryControllerTest.java | 2 ++ .../input/restapi/AnnouncementControllerIntegrationTest.java | 2 ++ .../adapter/input/restapi/ArticleControllerIntegrationTest.java | 2 ++ .../adapter/input/restapi/ElementControllerIntegrationTest.java | 2 ++ .../input/restapi/NormExpressionControllerIntegrationTest.java | 2 ++ .../restapi/NormManifestationControllerIntegrationTest.java | 2 ++ .../input/restapi/ProprietaryControllerIntegrationTest.java | 2 ++ .../input/restapi/RenderingControllerIntegrationTest.java | 2 ++ .../input/restapi/TimeBoundaryControllerIntegrationTest.java | 2 ++ 16 files changed, 32 insertions(+) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java index fe046aae9..22774f862 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -35,6 +36,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(AnnouncementController.class) @Import(SecurityConfig.class) class AnnouncementControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java index 28cb4a041..e3c20cc59 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java @@ -20,6 +20,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -27,6 +28,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(ArticleController.class) @Import(SecurityConfig.class) class ArticleControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java index ef466e768..a84974fec 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -24,6 +25,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(ElementController.class) @Import(SecurityConfig.class) class ElementControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java index 3dc9d79c0..a5d4167cc 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java @@ -23,6 +23,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -30,6 +31,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(NormExpressionController.class) @Import(SecurityConfig.class) class NormExpressionControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java index b19964b8f..f6adf9c8b 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java @@ -13,6 +13,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -20,6 +21,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(NormManifestationController.class) @Import(SecurityConfig.class) public class NormManifestationControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java index 88c941dd4..e213964ad 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java @@ -25,6 +25,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -32,6 +33,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(ProprietaryController.class) @Import(SecurityConfig.class) class ProprietaryControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java index fec0f9ea0..474bfbd1a 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -26,6 +27,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Therefore, manually * setting up the {@code mockMvc} including the ControllerAdvice */ +@WithMockUser @WebMvcTest(RenderingController.class) @Import(SecurityConfig.class) class RenderingControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java index c13766217..bc1c79d22 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java @@ -31,6 +31,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.method.annotation.HandlerMethodValidationException; @@ -39,6 +40,7 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ +@WithMockUser @WebMvcTest(TimeBoundaryController.class) @Import(SecurityConfig.class) class TimeBoundaryControllerTest { diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java index 88b7183cc..46fda603f 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java @@ -28,8 +28,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class AnnouncementControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java index 329a2c435..133e5d62d 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class ArticleControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java index 4e5f9d457..c8cd54b3e 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java @@ -14,8 +14,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class ElementControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java index ae5e8d176..454e936e1 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java @@ -21,8 +21,10 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class NormExpressionControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java index 55a14b11a..eeef752f0 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java @@ -12,8 +12,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class NormManifestationControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java index 47066ae28..50d557cfa 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class ProprietaryControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java index b6d499d3e..62c6c3a10 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java @@ -15,8 +15,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class RenderingControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java index 2866d9669..768284333 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class TimeBoundaryControllerIntegrationTest extends BaseIntegrationTest { @Autowired From dae9cf7e78d66e8524ad6d47902554612a293299 Mon Sep 17 00:00:00 2001 From: VictorDelCampo Date: Tue, 17 Dec 2024 14:55:14 +0100 Subject: [PATCH 04/11] Second iteration authentication (nginx + fallback backend route) RISDEV-5805 --- .../FrontendFallbackController.java | 25 +++++++++ .../restapi/controller/LogoutController.java | 34 ++++++++++++ .../ris/norms/config/SecurityConfig.java | 2 + .../ris/norms/config/SecurityConfigLocal.java | 54 +++++++++++++++++++ .../main/resources/application-oauth2.yaml | 11 ++++ .../resources/application-production.yaml | 2 + .../main/resources/application-staging.yaml | 2 + .../src/main/resources/application-uat.yaml | 2 + docker-compose.yaml | 10 +++- nginx-playwright.conf | 6 ++- 10 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java create mode 100644 backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java create mode 100644 backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java create mode 100644 backend/src/main/resources/application-oauth2.yaml diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java new file mode 100644 index 000000000..c3f7833a8 --- /dev/null +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java @@ -0,0 +1,25 @@ +package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * A fallback controller for serving the frontend's index.html file. + *

+ * This controller handles all requests that do not match an existing resource or API endpoint. + * It ensures that the Vue.js single-page application (SPA) can take over client-side routing. + */ +@Controller +public class FrontendFallbackController { + + /** + * Serves index.html for all unmatched routes, excluding requests for static resources. + * This ensures Vue Router can handle client-side routing. + * + * @return Forward to index.html. + */ + @GetMapping(value = { "/{path:^(?!assets|.*\\.).*}", "/{path:^(?!assets|.*\\.).*}/**" }) + public String serveIndexHtml() { + return "forward:/index.html"; + } +} diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java new file mode 100644 index 000000000..d9e597fee --- /dev/null +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java @@ -0,0 +1,34 @@ +package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; + +import jakarta.servlet.http.HttpSession; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller for handling user logout requests. + *

+ * This controller provides an endpoint for logging out authenticated users by invalidating their session. + * Upon logout, the session data stored in Redis is removed, effectively ending the user's session. + */ +@RestController +@RequestMapping("/api/v1/logout") +public class LogoutController { + + /** + * Logs out the current user by invalidating their session. + *

+ * This endpoint is designed to be called by authenticated users who wish to log out. + * It invalidates the user's session, which removes the associated data from the Redis session store. + * + * @param session The current HTTP session of the user making the logout request. + * @return A confirmation message indicating that the user has been logged out successfully. + */ + @PostMapping + public String logout(final HttpSession session) { + if (session != null) { + session.invalidate(); // Invalidate the session (removes it from Redis) + } + return "Logged out successfully"; + } +} diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java index fbc37c8b0..90085af06 100644 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -16,6 +17,7 @@ */ @Configuration @EnableWebSecurity +@Profile({ "staging", "uat", "production" }) public class SecurityConfig { /** diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java new file mode 100644 index 000000000..9770a2b29 --- /dev/null +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java @@ -0,0 +1,54 @@ +package de.bund.digitalservice.ris.norms.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +/** + * Configuration class for defining security settings in the application. This class is annotated + * with {@link Configuration} and {@link EnableWebSecurity} to indicate that it provides security + * configuration for a web-based application. + */ +@Configuration +@EnableWebSecurity +@Profile("local") +public class SecurityConfigLocal { + + /** + * Configures security settings for specific HTTP requests. + * + * @param http The {@link HttpSecurity} object to configure security settings. + * @return A {@link SecurityFilterChain} configured with security settings. + * @throws Exception If an exception occurs during security configuration. + */ + @Bean + public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> + authorize + .requestMatchers( + "/.well-known/security.txt", + "/favicon.svg", + "/actuator/health/**", + "/actuator/prometheus", + "/api/**", + "/index.html", + "/", + "/assets/**" + ) + .permitAll() + .anyRequest() + .denyAll() + ) + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) + ); + return http.build(); + } +} diff --git a/backend/src/main/resources/application-oauth2.yaml b/backend/src/main/resources/application-oauth2.yaml new file mode 100644 index 000000000..10ddb1762 --- /dev/null +++ b/backend/src/main/resources/application-oauth2.yaml @@ -0,0 +1,11 @@ +spring: + security: + oauth2: + client: + registration: + oidcclient: + provider: keycloak + scope: + - openid + - profile + - email diff --git a/backend/src/main/resources/application-production.yaml b/backend/src/main/resources/application-production.yaml index f6ec21fe3..a84654c2f 100644 --- a/backend/src/main/resources/application-production.yaml +++ b/backend/src/main/resources/application-production.yaml @@ -11,6 +11,8 @@ spring: data: redis: host: redis + profiles: + include: oauth2 otc: obs: diff --git a/backend/src/main/resources/application-staging.yaml b/backend/src/main/resources/application-staging.yaml index ea110917b..c058baf8e 100644 --- a/backend/src/main/resources/application-staging.yaml +++ b/backend/src/main/resources/application-staging.yaml @@ -8,6 +8,8 @@ spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:postgresql://${database.host:localhost}:${database.port:5432}/${database.database:risnorms}?currentSchema=${database.schema:norms} + profiles: + include: oauth2 otc: obs: diff --git a/backend/src/main/resources/application-uat.yaml b/backend/src/main/resources/application-uat.yaml index 3d1631a6a..e4d8e1a50 100644 --- a/backend/src/main/resources/application-uat.yaml +++ b/backend/src/main/resources/application-uat.yaml @@ -11,6 +11,8 @@ spring: data: redis: host: redis + profiles: + include: oauth2 otc: obs: diff --git a/docker-compose.yaml b/docker-compose.yaml index 4db3699ce..e7ee2a532 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,12 @@ services: + nginx: + build: + context: . + dockerfile: DockerfileNginxPlaywright + ports: + - 8080:8080 + depends_on: + - webapp webapp: image: ris-norms-app:001 build: @@ -8,8 +16,6 @@ services: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres14:5432/risnorms - REDIS_HOST=redis - SPRING_PROFILES_ACTIVE=local - ports: - - "8080:8080" depends_on: - postgres14 - redis diff --git a/nginx-playwright.conf b/nginx-playwright.conf index d4d228214..c38d57753 100644 --- a/nginx-playwright.conf +++ b/nginx-playwright.conf @@ -2,17 +2,19 @@ server { listen 8080; server_name localhost; + # proxy_set_header Host $http_host --> to preserve the HTTP host header so that Spring Security correctly resolve the external URL, ensuring the redirect URI matches what the OAuth2 provider (Bare.ID) expects. location ~ /(.+)\.[^\/]+ { proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } location /api { proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } location / { - try_files $uri $uri/ /; proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } - } From de4e7e55ba0ec5195494e19f478b155b5dbe5821 Mon Sep 17 00:00:00 2001 From: Sebastian Rossa Date: Wed, 18 Dec 2024 08:06:32 +0100 Subject: [PATCH 05/11] Use testcontainers to spin up keycloak RISDEV-5805 --- backend/build.gradle.kts | 1 + backend/gradle/libs.versions.toml | 2 + backend/src/main/resources/application.yaml | 6 +- .../integration/BaseIntegrationTest.java | 19 + .../ris/norms/keycloak/realm-export.json | 1743 +++++++++++++++++ 5 files changed, 1768 insertions(+), 3 deletions(-) create mode 100644 backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index a88715fbc..7f9980e3c 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -85,6 +85,7 @@ dependencies { testImplementation(libs.testcontainers.core) testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.testcontainers.postgres) + testImplementation(libs.testcontainers.keycloak) schematronToXsltCompileOnly(libs.schxslt) schematronToXsltCompileOnly(libs.saxon.he) diff --git a/backend/gradle/libs.versions.toml b/backend/gradle/libs.versions.toml index d50511d26..cda8fd3e6 100644 --- a/backend/gradle/libs.versions.toml +++ b/backend/gradle/libs.versions.toml @@ -44,6 +44,8 @@ squareup-okio-jvm = "com.squareup.okio:okio-jvm:3.9.1" testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "org-testcontainers" } testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "org-testcontainers" } testcontainers-postgres = { module = "org.testcontainers:postgresql", version.ref = "org-testcontainers" } +testcontainers-keycloak = "com.github.dasniko:testcontainers-keycloak:3.5.1" + [plugins] dependency-license-report = "com.github.jk1.dependency-license-report:2.9" diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index e9a73980a..a205f2d12 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -29,11 +29,11 @@ spring: - openid - profile - email - client-id: #replace-me# - client-secret: #replace-me# + client-id: norms + client-secret: 3LEwwv3DNY0N086lCOYeuri3aLicVDw1 provider: keycloak: - issuer-uri: #replace-me# + issuer-uri: http://localhost:8081/realms/testing server: servlet: session: diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java index 0f48e9e8c..ca4774300 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java @@ -1,5 +1,6 @@ package de.bund.digitalservice.ris.norms.integration; +import dasniko.testcontainers.keycloak.KeycloakContainer; import org.junit.jupiter.api.Tag; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -37,6 +38,7 @@ @Tag("integration") public abstract class BaseIntegrationTest { + protected static final KeycloakContainer keycloak; protected static final String LOCAL_STORAGE_PATH = ".local-storage-integration-test"; @Container @@ -51,6 +53,13 @@ public abstract class BaseIntegrationTest { ) .withExposedPorts(6379); + static { + keycloak = + new KeycloakContainer() + .withRealmImportFile("de/bund/digitalservice/ris/norms/keycloak/realm-export.json"); + keycloak.start(); + } + @DynamicPropertySource static void registerDynamicProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.username", postgreSQLContainer::getUsername); @@ -69,5 +78,15 @@ static void registerDynamicProperties(DynamicPropertyRegistry registry) { // Removing user/pass for redis registry.add("spring.data.redis.username", () -> ""); registry.add("spring.data.redis.password", () -> ""); + + registry.add( + "spring.security.oauth2.client.provider.keycloak.issuer-uri", + () -> keycloak.getAuthServerUrl() + "/realms/testing" + ); + registry.add("spring.security.oauth2.client.registration.oidcclient.client-id", () -> "norms"); + registry.add( + "spring.security.oauth2.client.registration.oidcclient.client-secret", + () -> "3LEwwv3DNY0N086lCOYeuri3aLicVDw1" + ); } } diff --git a/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json b/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json new file mode 100644 index 000000000..4944651a5 --- /dev/null +++ b/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json @@ -0,0 +1,1743 @@ +{ + "id": "testing", + "realm": "testing", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "858c23ee-e27c-4721-a004-7cb14e95b683", + "name": "default-roles-testing", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "testing" + }, + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account"] + } + ] + }, + "clients": [ + { + "id": "c116fbb6-6592-49bf-ae9a-ee1984f736d6", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/testing/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/testing/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "343f0bf9-cfbb-48f6-8578-12c97012cc74", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/testing/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/testing/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "3687b345-f36f-4994-8bc8-09136f5b8a88", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5e19f3dd-c3df-4146-b8d1-b8657ebc4426", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "66e285d5-d28b-44a6-a770-bbb3a20eb977", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "c030a98d-48e4-4f75-9d4f-2c09f6132d18", + "clientId": "norms", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "3LEwwv3DNY0N086lCOYeuri3aLicVDw1", + "redirectUris": ["localhost:8080"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "387bdeff-1f9b-4284-ae59-e24b5dce6bbf", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0e5ecc89-68bb-47d3-880f-a898f2314836", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/testing/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/admin/testing/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "6556594d-416c-459f-82df-8c4f4770a242", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "41b5bb79-7d6b-4e92-b7d7-64069c66af14", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "44261995-7352-4882-b285-0ed7362bcf79", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "8ed35d50-c66e-482b-aacd-47e6feca3fa4", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "6f6643ab-530d-4261-97ba-42fd77e32de7", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "662d9d44-e0d1-4606-9fe0-6510e07102d5", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "0d088c8e-0ec0-4e9a-a8a0-40deae194b00", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "c0304a2e-b49c-4a29-b577-cb75e669f397", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ec197e80-3409-46eb-a3aa-8a27fb39db8c", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "90cca696-5a33-49fd-82d2-ca89d3b79be7", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d26afed6-ccfa-4929-b296-1ba339f8afdf", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "61d59219-5aa4-4aa2-b0c8-7f846409ce3d", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "733fbba1-39c6-466c-acc0-cce27379240e", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "e27310b4-0e8f-4dfe-a324-1e42b1277776", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "6e96348f-6d0e-498a-a9fc-6213291710c4", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "55478f17-6486-448f-b823-da1a6e9d555c", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "f67909b2-6a6a-496f-a3b8-bf2cdc3e10ce", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "1f6876f7-555d-4a7c-9fbe-fa4aab6f8602", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "e1aaccc7-b870-44fb-9e47-77e91b8f0fd8", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "c90efa04-4035-4870-8d27-339ab7636747", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "6b129f51-dbe0-4a6b-872d-f6c19706477a", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "9736eb2b-0887-44c1-947c-61599da9b08a", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "5ab1e866-ab0b-43d8-87d2-bf65a8598192", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "75b8cd24-353d-489a-b447-d87880e13bc8", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "46585087-34d3-4b6a-b49f-d5266fe0099b", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "4d296b06-a515-486f-8b53-45d1a71af0dc", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "29807788-247d-48aa-bd90-7289fc8df8b8", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a1e04145-5b4c-4456-8de2-bd4f28d14f03", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "8ea4915e-34ca-4af8-a477-5e7273bab40b", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1bd6ee1f-745b-49c8-9f79-14ee99712302", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "b0deb610-8070-47cd-ad57-90f80151702f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "57739318-89c1-4a5f-bede-53cec86cb755", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "9afafae2-4ba3-4e13-8338-8ab4d9200879", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "599e1985-8671-4905-94dd-3cd562ee4a1b", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "ef77b71f-8d7e-4c29-a38c-5e3e4bf8036d", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "d93a3ae3-6ba3-448a-8c58-23b45cd4d9ce", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "web-origins", + "email", + "roles", + "profile", + "role_list" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "phone", + "address", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "195515d9-7706-4f4c-b981-18e44c7905f9", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "337a974c-0e18-4668-bd62-c8d9b17214b2", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "be083412-9979-496e-be1a-877e2ffad9b9", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "b603bd8c-ca25-4474-8a82-fb3b3d4cec64", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "c6f3c7e4-9b57-4205-b189-e463768622a5", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "6c8ce11c-60fb-4898-b90c-19cd9ffa9e12", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "15a45693-6a1c-45e8-913d-f9863489309e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "f20aa89a-85e4-43c1-9513-07394e6cf3ad", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "71e2003e-0168-4db0-b99c-a227c0f4829c", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": ["100"], + "algorithm": ["HS256"] + } + }, + { + "id": "5e591eb1-c79e-4b4a-9513-be4cb74cc042", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "4342696d-56b7-4083-b11f-53577194e08f", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "3f61d57f-8515-423e-9981-1b8944c8750d", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": ["100"], + "algorithm": ["RSA-OAEP"] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "d861511d-6db5-4ebc-9b80-addc63b22df7", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "60a3feba-5b5d-4b46-a087-d384ccc64820", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "85a7b608-d2af-4610-a5ff-c4db223e3b97", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e80a120c-6fd1-41b7-861c-2b0ab3df5268", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b30d8d48-dae6-46a5-936b-e716bbb70256", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "83f3b43a-d7e5-44df-86f5-899203358982", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "e53adb44-5aa8-4be2-bd00-ffd99571c4fc", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1588dd80-71f6-412a-b385-30d30a294e83", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "fa62c6d4-8510-4f5b-bf32-d4917da5283a", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "772a641d-796b-4968-9922-0ab98e3f3f75", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "c3df8730-a054-49c6-9b96-3b564186ed3e", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "955e50f5-8bf0-4192-be2b-f77cf078b63f", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "59a54417-6238-4e43-9cf7-5d92c7de4a90", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "460e4113-b06d-4415-80b2-8ee2cac4b7c2", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "929da9e7-3a9c-4c7c-b07a-0efca204d46e", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "a3fd479c-d42e-4ee0-828f-ca5aaf28426a", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "11bcac6b-1266-4a90-89e1-8f4295c060bd", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "b1e71934-b6a2-46c3-9f0c-2ee2f1af341e", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b2a17b38-d4ed-48d2-b699-1951d1f340bb", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "142c01b3-2e9c-4dcc-a931-b1264e620df0", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "0b9d2e29-a641-4fe6-bddb-74fbfd626fc1", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "38727c1d-d805-4689-875a-871ac7ac4dc3", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5" + }, + "keycloakVersion": "17.0.1", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + }, + "users": [ + { + "username": "test", + "email": "test@test.com", + "firstName": "test", + "lastName": "test", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "test" + } + ], + "clientRoles": { + "account": ["view-profile", "manage-account"] + } + } + ] +} From 654008170dfe44431091d14c3fa200662a98dbfc Mon Sep 17 00:00:00 2001 From: VictorDelCampo Date: Thu, 19 Dec 2024 14:31:04 +0100 Subject: [PATCH 06/11] Handover to malte RISDEV-5805 --- .../ris/norms/config/SecurityConfig.java | 6 +- .../ris/norms/config/SecurityConfigLocal.java | 54 ------------ .../src/main/resources/application-local.yaml | 12 ++- .../main/resources/application-oauth2.yaml | 11 --- .../resources/application-production.yaml | 2 - .../main/resources/application-staging.yaml | 2 - .../src/main/resources/application-uat.yaml | 2 - backend/src/main/resources/application.yaml | 5 -- docker-compose.yaml | 54 +++++++++--- local/keycloak/realm.json | 82 +++++++++++++++++++ 10 files changed, 139 insertions(+), 91 deletions(-) delete mode 100644 backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java delete mode 100644 backend/src/main/resources/application-oauth2.yaml create mode 100644 local/keycloak/realm.json diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java index 90085af06..7ee78b13b 100644 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java @@ -2,7 +2,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -17,7 +16,6 @@ */ @Configuration @EnableWebSecurity -@Profile({ "staging", "uat", "production" }) public class SecurityConfig { /** @@ -36,8 +34,7 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { "/.well-known/security.txt", "/favicon.svg", "/actuator/health/**", - "/actuator/prometheus", - "/assets/**" + "/actuator/prometheus" ) .permitAll() .anyRequest() @@ -45,6 +42,7 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { ) .oauth2Login(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) + .cors(Customizer.withDefaults()) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) ); diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java deleted file mode 100644 index 9770a2b29..000000000 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfigLocal.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.bund.digitalservice.ris.norms.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; - -/** - * Configuration class for defining security settings in the application. This class is annotated - * with {@link Configuration} and {@link EnableWebSecurity} to indicate that it provides security - * configuration for a web-based application. - */ -@Configuration -@EnableWebSecurity -@Profile("local") -public class SecurityConfigLocal { - - /** - * Configures security settings for specific HTTP requests. - * - * @param http The {@link HttpSecurity} object to configure security settings. - * @return A {@link SecurityFilterChain} configured with security settings. - * @throws Exception If an exception occurs during security configuration. - */ - @Bean - public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { - http - .authorizeHttpRequests(authorize -> - authorize - .requestMatchers( - "/.well-known/security.txt", - "/favicon.svg", - "/actuator/health/**", - "/actuator/prometheus", - "/api/**", - "/index.html", - "/", - "/assets/**" - ) - .permitAll() - .anyRequest() - .denyAll() - ) - .csrf(AbstractHttpConfigurer::disable) - .sessionManagement(sessionManagement -> - sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) - ); - return http.build(); - } -} diff --git a/backend/src/main/resources/application-local.yaml b/backend/src/main/resources/application-local.yaml index 905d04b14..cc71c0742 100644 --- a/backend/src/main/resources/application-local.yaml +++ b/backend/src/main/resources/application-local.yaml @@ -1,10 +1,20 @@ spring: flyway: locations: classpath:db/migration,classpath:db/data + security: + oauth2: + client: + registration: + oidcclient: + client-id: ris-norms-local + client-secret: ris-norms-local + provider: + keycloak: + issuer-uri: http://keycloak:8080/realms/ris + authorization-uri: http://localhost:8443/realms/ris/protocol/openid-connect/auth local: file-storage: .local-storage - #logging: # level: # org.springframework: DEBUG diff --git a/backend/src/main/resources/application-oauth2.yaml b/backend/src/main/resources/application-oauth2.yaml deleted file mode 100644 index 10ddb1762..000000000 --- a/backend/src/main/resources/application-oauth2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -spring: - security: - oauth2: - client: - registration: - oidcclient: - provider: keycloak - scope: - - openid - - profile - - email diff --git a/backend/src/main/resources/application-production.yaml b/backend/src/main/resources/application-production.yaml index a84654c2f..f6ec21fe3 100644 --- a/backend/src/main/resources/application-production.yaml +++ b/backend/src/main/resources/application-production.yaml @@ -11,8 +11,6 @@ spring: data: redis: host: redis - profiles: - include: oauth2 otc: obs: diff --git a/backend/src/main/resources/application-staging.yaml b/backend/src/main/resources/application-staging.yaml index c058baf8e..ea110917b 100644 --- a/backend/src/main/resources/application-staging.yaml +++ b/backend/src/main/resources/application-staging.yaml @@ -8,8 +8,6 @@ spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:postgresql://${database.host:localhost}:${database.port:5432}/${database.database:risnorms}?currentSchema=${database.schema:norms} - profiles: - include: oauth2 otc: obs: diff --git a/backend/src/main/resources/application-uat.yaml b/backend/src/main/resources/application-uat.yaml index e4d8e1a50..3d1631a6a 100644 --- a/backend/src/main/resources/application-uat.yaml +++ b/backend/src/main/resources/application-uat.yaml @@ -11,8 +11,6 @@ spring: data: redis: host: redis - profiles: - include: oauth2 otc: obs: diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index a205f2d12..fcaf85c93 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -29,11 +29,6 @@ spring: - openid - profile - email - client-id: norms - client-secret: 3LEwwv3DNY0N086lCOYeuri3aLicVDw1 - provider: - keycloak: - issuer-uri: http://localhost:8081/realms/testing server: servlet: session: diff --git a/docker-compose.yaml b/docker-compose.yaml index e7ee2a532..e1fbdfbfb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,26 +1,36 @@ services: - nginx: - build: - context: . - dockerfile: DockerfileNginxPlaywright - ports: - - 8080:8080 - depends_on: - - webapp + # nginx: + # build: + # context: . + # dockerfile: DockerfileNginxPlaywright + # ports: + # - 8080:8080 + # depends_on: + # - webapp + # networks: + # - ris-norms webapp: image: ris-norms-app:001 + container_name: webapp build: context: . dockerfile: DockerfileApp + ports: + - 8080:8080 environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres14:5432/risnorms - REDIS_HOST=redis - SPRING_PROFILES_ACTIVE=local depends_on: - - postgres14 - - redis + postgres14: + condition: service_healthy + redis: + condition: service_healthy + # keycloak: + # condition: service_healthy volumes: - ./local-storage:/app/.local-storage + postgres14: image: postgres:14-alpine restart: always @@ -39,6 +49,7 @@ services: retries: 3 start_period: 5s timeout: 3s + redis: image: cgr.dev/chainguard/redis:latest extra_hosts: @@ -64,5 +75,28 @@ services: start_period: 3s timeout: 5s + keycloak: + image: quay.io/keycloak/keycloak:26.0.7 + #command: ["start-dev", "--import-realm", "--hostname-backchannel-dynamic"] + command: ["start-dev", "--import-realm"] + container_name: keycloak + ports: + - 8443:8080 # Map external 8443 to internal 8080 + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: test + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + # KC_HOSTNAME: keycloak + KC_HOSTNAME_URL: http://localhost:8443/keycloak + volumes: + - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro + # health endpoint läuft unter 9000 +# healthcheck: +# test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live'] +# interval: 5s +# timeout: 5s +# retries: 20 + volumes: postgres14-data: diff --git a/local/keycloak/realm.json b/local/keycloak/realm.json new file mode 100644 index 000000000..83f91796a --- /dev/null +++ b/local/keycloak/realm.json @@ -0,0 +1,82 @@ +{ + "id": "ris", + "realm": "ris", + "sslRequired": "none", + "ssoSessionIdleTimeout": 432000, + "ssoSessionMaxLifespan": 7776000, + "enabled": true, + "eventsEnabled": true, + "eventsExpiration": 900, + "adminEventsEnabled": true, + "adminEventsDetailsEnabled": true, + "attributes": { + "adminEventsExpiration": "900" + }, + "clients": [ + { + "id": "ris-norms-local", + "clientId": "ris-norms-local", + "name": "ris-norms-local", + "enabled": true, + "publicClient": true, + "rootUrl": "http://localhost:8080", + "baseUrl": "http://localhost:8080", + "redirectUris": [ + "http://localhost:8080/*" + ], + "webOrigins": [ + "http://localhost:8080" + ], + "clientAuthenticatorType": "client-secret", + "secret": "ris-norms-local", + "protocolMappers": [ + { + "id": "c4b86c90-3076-49df-9343-0928b135733a", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "multivalued": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + } + ] + } + ], + "groups": [ + { + "id": "4f928164-2bd4-455a-8fbd-174eeb8ea205", + "name": "Norms", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], + "users": [ + { + "id": "jane.doe", + "email": "jane.doe@example.com", + "username": "jane.doe", + "firstName": "Jane", + "lastName": "Doe", + "enabled": true, + "emailVerified": true, + "credentials": [ + { + "temporary": false, + "type": "password", + "value": "test" + } + ], + "groups": ["Norms"] + } + ] +} From d430b9ceab7b113b7e21d6dd926cb56ca8eb6e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Lauk=C3=B6tter?= Date: Mon, 6 Jan 2025 10:12:21 +0100 Subject: [PATCH 07/11] Fix local keycloak setup We use a service called localhost to be able to address the keycloak service as "localhost:8443" in the application.yaml of our application. Using "keycloak:8443" is not possible as spring would then redirect the user to "keycloak:8443" to login, but that is a host that is not available on the host system. The spring boot oauth implementation is very restrictive on the issuer and therefore creates errors if the hosts used by it and the user to connect to keycloak differ. See also: - https://github.com/spring-projects/spring-security/issues/14633 - https://github.com/keycloak/keycloak/issues/29783 - https://github.com/keycloak/keycloak/issues/24252 - https://medium.com/@kostapchuk/integrating-keycloak-with-spring-boot-in-a-dockerized-environment-813eab1f140c RISDEV-5805 --- .../src/main/resources/application-local.yaml | 3 +-- docker-compose.yaml | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/src/main/resources/application-local.yaml b/backend/src/main/resources/application-local.yaml index cc71c0742..ef1b6eaa1 100644 --- a/backend/src/main/resources/application-local.yaml +++ b/backend/src/main/resources/application-local.yaml @@ -10,8 +10,7 @@ spring: client-secret: ris-norms-local provider: keycloak: - issuer-uri: http://keycloak:8080/realms/ris - authorization-uri: http://localhost:8443/realms/ris/protocol/openid-connect/auth + issuer-uri: http://localhost:8443/realms/ris local: file-storage: .local-storage diff --git a/docker-compose.yaml b/docker-compose.yaml index e1fbdfbfb..925a3a45e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,18 +9,35 @@ services: # - webapp # networks: # - ris-norms + + # We use a service called localhost to be able to address the keycloak service as "localhost:8443" in the + # application.yaml of our application. Using "keycloak:8443" is not possible as spring would then redirect the user to + # "keycloak:8443" to login, but that is a host that is not available on the host system. The spring boot oauth + # implementation is very restrictive on the issuer and therefore creates errors if the hosts used by it and the user + # to connect to keycloak differ. + # See also: + # - https://github.com/spring-projects/spring-security/issues/14633 + # - https://github.com/keycloak/keycloak/issues/29783 + # - https://github.com/keycloak/keycloak/issues/24252 + # - https://medium.com/@kostapchuk/integrating-keycloak-with-spring-boot-in-a-dockerized-environment-813eab1f140c + localhost: + image: alpine:3.21 + command: sleep infinity + ports: + - "8443:8443" # Keycloak port + - "8080:8080" # Webapp port + webapp: image: ris-norms-app:001 container_name: webapp build: context: . dockerfile: DockerfileApp - ports: - - 8080:8080 environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres14:5432/risnorms - REDIS_HOST=redis - SPRING_PROFILES_ACTIVE=local + network_mode: "service:localhost" depends_on: postgres14: condition: service_healthy @@ -77,18 +94,16 @@ services: keycloak: image: quay.io/keycloak/keycloak:26.0.7 - #command: ["start-dev", "--import-realm", "--hostname-backchannel-dynamic"] command: ["start-dev", "--import-realm"] container_name: keycloak - ports: - - 8443:8080 # Map external 8443 to internal 8080 environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: test KC_HEALTH_ENABLED: true KC_METRICS_ENABLED: true - # KC_HOSTNAME: keycloak + KC_HTTP_PORT: 8443 KC_HOSTNAME_URL: http://localhost:8443/keycloak + network_mode: "service:localhost" volumes: - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro # health endpoint läuft unter 9000 From 61cef17635c10e65515b7afe429ca8c795070e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Lauk=C3=B6tter?= Date: Mon, 6 Jan 2025 10:17:20 +0100 Subject: [PATCH 08/11] Enable keycloak healthcheck RISDEV-5805 --- docker-compose.yaml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 925a3a45e..bef99dfac 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,8 +43,8 @@ services: condition: service_healthy redis: condition: service_healthy - # keycloak: - # condition: service_healthy + keycloak: + condition: service_healthy volumes: - ./local-storage:/app/.local-storage @@ -107,11 +107,15 @@ services: volumes: - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro # health endpoint läuft unter 9000 -# healthcheck: -# test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live'] -# interval: 5s -# timeout: 5s -# retries: 20 + healthcheck: + test: + [ + "CMD-SHELL", + '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live', + ] + interval: 5s + timeout: 5s + retries: 20 volumes: postgres14-data: From 5fbb6aab6180ce9b6953d6890876f8f793a025fd Mon Sep 17 00:00:00 2001 From: Sebastian Rossa Date: Mon, 6 Jan 2025 15:04:34 +0100 Subject: [PATCH 09/11] Fix unit tests RISDEV-5805 --- .../AnnouncementControllerTest.java | 25 +++++++++++++++---- .../controller/ArticleControllerTest.java | 8 +++--- .../controller/ElementControllerTest.java | 8 +++--- .../NormExpressionControllerTest.java | 16 +++++++++--- .../NormManifestationControllerTest.java | 8 +++--- .../controller/ProprietaryControllerTest.java | 13 +++++++--- .../controller/RenderingControllerTest.java | 20 ++++++++++++--- .../TimeBoundaryControllerTest.java | 15 ++++++++--- 8 files changed, 86 insertions(+), 27 deletions(-) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java index 22774f862..60da06b4a 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -24,8 +25,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.test.context.support.WithMockUser; @@ -37,8 +38,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(AnnouncementController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = AnnouncementController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class AnnouncementControllerTest { @Autowired @@ -268,6 +271,7 @@ void itReleaseAnAnnouncement() throws Exception { "/api/v1/announcements/eli/bund/bgbl-1/2023/413/2023-12-29/1/deu/regelungstext-1/releases" ) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) ) .andExpect(status().isOk()) .andExpect(jsonPath("releaseAt", equalTo("2024-01-02T10:20:30Z"))) @@ -308,7 +312,12 @@ void itCreatesAnAnnouncement() throws Exception { // When // Then mockMvc - .perform(multipart("/api/v1/announcements").file(file).accept(MediaType.APPLICATION_JSON)) + .perform( + multipart("/api/v1/announcements") + .file(file) + .accept(MediaType.APPLICATION_JSON) + .with(csrf()) + ) .andExpect(status().isOk()) .andExpect( jsonPath("eli", equalTo("eli/bund/bgbl-1/2017/s419/2017-03-15/1/deu/regelungstext-1")) @@ -337,6 +346,7 @@ void itCreatesAnAnnouncementWithForce() throws Exception { multipart("/api/v1/announcements?force=true") .file(file) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) ) .andExpect(status().isOk()) .andExpect( @@ -363,7 +373,12 @@ void itShouldNotExposeInternalInformationOnUnexpectedErrors() throws Exception { // When // Then mockMvc - .perform(multipart("/api/v1/announcements").file(file).accept(MediaType.APPLICATION_JSON)) + .perform( + multipart("/api/v1/announcements") + .file(file) + .accept(MediaType.APPLICATION_JSON) + .with(csrf()) + ) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("type", equalTo("/errors/internal-server-error"))) .andExpect(jsonPath("title", equalTo("Internal Server Error"))) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java index e3c20cc59..1dbaf131b 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java @@ -17,8 +17,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -29,8 +29,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(ArticleController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = ArticleController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ArticleControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java index a84974fec..b4fa68686 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java @@ -14,8 +14,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -26,8 +26,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(ElementController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = ElementController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ElementControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java index a5d4167cc..4a3e63a0c 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -20,8 +21,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -32,8 +33,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(NormExpressionController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = NormExpressionController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class NormExpressionControllerTest { @Autowired @@ -281,6 +284,7 @@ void itCallsNormServiceAndUpdatesNorm() throws Exception { .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -306,6 +310,7 @@ void itCallsNormServiceAndReturnsErrorMessage() throws Exception { .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -337,6 +342,7 @@ void itCallsNormServiceAndReturnsUnprocessableWhenNodeIsMissing() throws Excepti .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -364,6 +370,7 @@ void itCallsUpdateModUseCaseAndReturnsXml() throws Exception { .perform( put("/api/v1/norms/" + eli + "/mods/" + modEid) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"refersTo\": \"aenderungsbefehl-ersetzen\", \"timeBoundaryEid\": \"new-time-boundary-eid\", \"destinationHref\": \"new-destination-href\", \"newContent\": \"new test text\"}" @@ -394,6 +401,7 @@ void itCallsUpdateModUseCaseWithDryRunAndReturnsXml() throws Exception { .perform( put("/api/v1/norms/" + eli + "/mods/" + modEid + "?dryRun=true") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"refersTo\": \"aenderungsbefehl-ersetzen\", \"timeBoundaryEid\": \"new-time-boundary-eid\", \"destinationHref\": \"new-destination-href\", \"newContent\": \"new test text\"}" @@ -427,6 +435,7 @@ void itCallsUpdateModsUseCaseAndReturnsXml() throws Exception { .perform( patch("/api/v1/norms/" + eli + "/mods") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"mod-eid-1\": {\"timeBoundaryEid\": \"new-time-boundary-eid\"},\n" + @@ -457,6 +466,7 @@ void itCallsUpdateModsUseCaseWithDryRunAndReturnsXml() throws Exception { .perform( patch("/api/v1/norms/" + eli + "/mods?dryRun=true") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"mod-eid-1\": {\"timeBoundaryEid\": \"new-time-boundary-eid\"}}") ) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java index f6adf9c8b..f9f7d7d60 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java @@ -10,8 +10,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -22,8 +22,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(NormManifestationController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = NormManifestationController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) public class NormManifestationControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java index e213964ad..5964fb546 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -22,8 +23,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -34,8 +35,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(ProprietaryController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = ProprietaryController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ProprietaryControllerTest { @Autowired @@ -226,6 +229,7 @@ void updatesProprietarySuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{date}", eli, date.toString()) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"fna\": \"new-fna\"," + @@ -287,6 +291,7 @@ void itReturnsNotFoundIfNormIsNotFound() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{date}", eli, "1990-01-01") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"fna\": \"new-fna\"," + @@ -457,6 +462,7 @@ void updatesProprietarySuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{eid}/{date}", eli, eid, date.toString()) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"artDerNorm\": \"SN\"}") ) @@ -493,6 +499,7 @@ void itReturnsNotFoundIfNormIsNotFound() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{eid}/{date}", eli, eid, "1990-01-01") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"artDerNorm\": \"SN\"}") ) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java index 474bfbd1a..03015f1cd 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java @@ -1,13 +1,13 @@ package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import de.bund.digitalservice.ris.norms.application.port.input.ApplyPassiveModificationsUseCase; import de.bund.digitalservice.ris.norms.application.port.input.TransformLegalDocMlToHtmlUseCase; -import de.bund.digitalservice.ris.norms.config.SecurityConfig; import de.bund.digitalservice.ris.norms.domain.entity.Norm; import de.bund.digitalservice.ris.norms.utils.XmlMapper; import de.bund.digitalservice.ris.norms.utils.exceptions.XmlProcessingException; @@ -16,8 +16,8 @@ import org.junit.jupiter.api.Test; import org.mockito.AdditionalMatchers; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -28,8 +28,10 @@ * setting up the {@code mockMvc} including the ControllerAdvice */ @WithMockUser -@WebMvcTest(RenderingController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = RenderingController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class RenderingControllerTest { @Autowired @@ -60,6 +62,7 @@ void itThrowsXmlProcessingException() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -95,6 +98,7 @@ void getHtmlPreviewWithShowMetadataTrue() throws Exception { post("/api/v1/renderings") .queryParam("showMetadata", "true") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -141,6 +145,7 @@ void getHtmlPreviewWithShowMetadataFalse() throws Exception { post("/api/v1/renderings") .queryParam("showMetadata", "false") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -187,6 +192,7 @@ void getHtmlPreviewForDate() throws Exception { .queryParam("showMetadata", "true") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -239,6 +245,7 @@ void getHtmlPreviewForCurrentDate() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -277,6 +284,7 @@ void getHtmlPreviewForDateWithCustomNorms() throws Exception { .queryParam("showMetadata", "false") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -314,6 +322,7 @@ void getHtmlPreviewWithSnippetTrue() throws Exception { post("/api/v1/renderings") .queryParam("snippet", "true") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -353,6 +362,7 @@ void getXmlPreviewForCurrentDate() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -401,6 +411,7 @@ void getXmlPreviewForDate() throws Exception { post("/api/v1/renderings") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -447,6 +458,7 @@ void getXmlPreviewForDateWithCustomNorms() throws Exception { post("/api/v1/renderings") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java index bc1c79d22..eb6bf6077 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java @@ -5,6 +5,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -28,8 +29,8 @@ import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -41,8 +42,10 @@ * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ @WithMockUser -@WebMvcTest(TimeBoundaryController.class) -@Import(SecurityConfig.class) +@WebMvcTest( + controllers = TimeBoundaryController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class TimeBoundaryControllerTest { @Autowired @@ -211,6 +214,7 @@ void updateTimeBoundariesReturnsSuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "[{\"date\": \"1964-09-21\", \"eventRefEid\": \"meta-1_lebzykl-1_ereignis-2\"}]" @@ -244,6 +248,7 @@ void updateTimeBoundariesMultipleSameDatesReturns400() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "[" + @@ -277,6 +282,7 @@ void updateTimeBoundariesReturnsDateMalformed() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": \"THISISNODATE\", \"eventRefEid\": null}]") ) @@ -301,6 +307,7 @@ void updateTimeBoundariesWithEmptyListReturns400() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[]") ) @@ -336,6 +343,7 @@ void updateTimeBoundariesReturnsDateIsNull() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": null, \"eventRefEid\": null}]") ) @@ -363,6 +371,7 @@ void validationFailureInSingleQuotesForValidJsonResponse() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": null, \"eventRefEid\": null}]") ) From 8e00c5edc33adc375f08adac4264da49668f5cdc Mon Sep 17 00:00:00 2001 From: Sebastian Rossa Date: Mon, 6 Jan 2025 15:29:05 +0100 Subject: [PATCH 10/11] Ignore fallback serving index.html for api routes RISDEV-5805 --- .../input/restapi/controller/FrontendFallbackController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java index c3f7833a8..602205ada 100644 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java @@ -18,7 +18,7 @@ public class FrontendFallbackController { * * @return Forward to index.html. */ - @GetMapping(value = { "/{path:^(?!assets|.*\\.).*}", "/{path:^(?!assets|.*\\.).*}/**" }) + @GetMapping(value = { "/{path:^(?!assets|api|.*\\.).*}", "/{path:^(?!assets|api|.*\\.).*}/**" }) public String serveIndexHtml() { return "forward:/index.html"; } From 21b91de02117362497fe8ec5c317c5ad53ad1bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Lauk=C3=B6tter?= Date: Tue, 7 Jan 2025 09:35:25 +0100 Subject: [PATCH 11/11] Split docker-compose into two files One for the full application and one for just the services. We need different network configurations for both. RISDEV-5805 --- .github/workflows/end-to-end-tests.yml | 2 +- DEVELOPING.md | 8 ++- backend/README.md | 2 +- docker-compose-services.yaml | 73 +++++++++++++++++++++++ docker-compose.yaml | 82 +++----------------------- 5 files changed, 91 insertions(+), 76 deletions(-) create mode 100644 docker-compose-services.yaml diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index e3b508bf4..3ad5c4869 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -25,7 +25,7 @@ jobs: distribution: "temurin" cache: gradle - name: Start PostgreSQL - run: docker compose up --detach postgres14 redis + run: docker compose -f docker-compose-services.yaml up --detach - name: Start backend # GitHub actions automatically keep the process running and output is streamed to pipeline logs run: ./gradlew bootRun & working-directory: ./backend diff --git a/DEVELOPING.md b/DEVELOPING.md index 66b89e3b2..374563cb9 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -59,7 +59,7 @@ with the name `SLACK_WEBHOOK_URL`, containing a url for [Incoming Webhooks](http Run dependencies from the root of the project: ```bash -docker compose up -d postgres14 redis +docker compose -f docker-compose-services.yaml up -d ``` Run from `./backend`: @@ -98,6 +98,12 @@ To stop them: docker compose down ``` +If you want to run the java application and frontend yourself you can start only the other services (redis, postgres & keycloak) by running: + +```bash +docker compose -f docker-compose-services.yaml up -d +``` + # Testing ## Unit and Integration tests diff --git a/backend/README.md b/backend/README.md index c6751b305..30db4b227 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,7 +18,7 @@ Install the latest LTS version of node for running Spotless with Prettier. Set up and boot the postgres database and the redis database (from the project root): ```sh -docker compose up postgres14 redis +docker compose -f docker-compose-services.yaml up ``` You can then start the backend with two different options: diff --git a/docker-compose-services.yaml b/docker-compose-services.yaml new file mode 100644 index 000000000..52fbfe38e --- /dev/null +++ b/docker-compose-services.yaml @@ -0,0 +1,73 @@ +services: + postgres14: + image: postgres:14-alpine + restart: always + container_name: postgres14 + volumes: + - postgres14-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=risnorms + - POSTGRES_PASSWORD=test + - POSTGRES_USER=test + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U test -d risnorms"] + interval: 5s + retries: 3 + start_period: 5s + timeout: 3s + + redis: + image: cgr.dev/chainguard/redis:latest + extra_hosts: + - localhost:host-gateway + container_name: redis + command: + - "--maxmemory 256mb" + - "--maxmemory-policy allkeys-lru" + - "--timeout 300" + - "--tcp-keepalive 10" + - "--user redis on +@all -CONFIG ~* >password" + - "--user default off resetchannels -@all" + ports: + - "6379:6379" + healthcheck: + test: + [ + "CMD-SHELL", + "redis-cli -h 127.0.0.1 --user redis -a password PING | grep 'PONG' || exit 1", + ] + interval: 5s + retries: 5 + start_period: 3s + timeout: 5s + + keycloak: + image: quay.io/keycloak/keycloak:26.0.7 + command: ["start-dev", "--import-realm"] + container_name: keycloak + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: test + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + KC_HTTP_PORT: 8443 + KC_HOSTNAME_URL: http://localhost:8443/keycloak + ports: + - "8443:8443" + volumes: + - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro + # health endpoint läuft unter 9000 + healthcheck: + test: + [ + "CMD-SHELL", + '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live', + ] + interval: 5s + timeout: 5s + retries: 20 + +volumes: + postgres14-data: diff --git a/docker-compose.yaml b/docker-compose.yaml index bef99dfac..90d23c824 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,15 +1,4 @@ services: - # nginx: - # build: - # context: . - # dockerfile: DockerfileNginxPlaywright - # ports: - # - 8080:8080 - # depends_on: - # - webapp - # networks: - # - ris-norms - # We use a service called localhost to be able to address the keycloak service as "localhost:8443" in the # application.yaml of our application. Using "keycloak:8443" is not possible as spring would then redirect the user to # "keycloak:8443" to login, but that is a host that is not available on the host system. The spring boot oauth @@ -49,73 +38,20 @@ services: - ./local-storage:/app/.local-storage postgres14: - image: postgres:14-alpine - restart: always - container_name: postgres14 - volumes: - - postgres14-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=risnorms - - POSTGRES_PASSWORD=test - - POSTGRES_USER=test - ports: - - 5432:5432 - healthcheck: - test: ["CMD-SHELL", "pg_isready -U test -d risnorms"] - interval: 5s - retries: 3 - start_period: 5s - timeout: 3s + extends: + file: docker-compose-services.yaml + service: postgres14 redis: - image: cgr.dev/chainguard/redis:latest - extra_hosts: - - localhost:host-gateway - container_name: redis - command: - - "--maxmemory 256mb" - - "--maxmemory-policy allkeys-lru" - - "--timeout 300" - - "--tcp-keepalive 10" - - "--user redis on +@all -CONFIG ~* >password" - - "--user default off resetchannels -@all" - ports: - - "6379:6379" - healthcheck: - test: - [ - "CMD-SHELL", - "redis-cli -h 127.0.0.1 --user redis -a password PING | grep 'PONG' || exit 1", - ] - interval: 5s - retries: 5 - start_period: 3s - timeout: 5s + extends: + file: docker-compose-services.yaml + service: redis keycloak: - image: quay.io/keycloak/keycloak:26.0.7 - command: ["start-dev", "--import-realm"] - container_name: keycloak - environment: - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: test - KC_HEALTH_ENABLED: true - KC_METRICS_ENABLED: true - KC_HTTP_PORT: 8443 - KC_HOSTNAME_URL: http://localhost:8443/keycloak + extends: + file: docker-compose-services.yaml + service: keycloak network_mode: "service:localhost" - volumes: - - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro - # health endpoint läuft unter 9000 - healthcheck: - test: - [ - "CMD-SHELL", - '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live', - ] - interval: 5s - timeout: 5s - retries: 20 volumes: postgres14-data: