Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial work on authentication #844

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/push-docker-image-job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ jobs:
uses: sigstore/cosign-installer@e11c0892438d2c0a48e49dee376e4883f10f2e59
- name: Sign the published Docker image
run: cosign sign --yes ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }}
# - name: Download cosign vulnerability scan record
# uses: actions/download-artifact@v4
# with:
# name: "vuln.json"
# - name: Attest vulnerability scan
# run: cosign attest --yes --replace --predicate vuln.json --type vuln ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }}
# - name: Download cosign vulnerability scan record
# uses: actions/download-artifact@v4
# with:
# name: "vuln.json"
# - name: Attest vulnerability scan
# run: cosign attest --yes --replace --predicate vuln.json --type vuln ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }}
- name: Send status to Slack
uses: digitalservicebund/notify-on-failure-gha@814d0c4b2ad6a3443e89c991f8657b10126510bf # v1.5.0
if: ${{ failure() }}
Expand Down
2 changes: 2 additions & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -84,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)
Expand Down
3 changes: 3 additions & 0 deletions backend/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@ spring-boot-starter-validation = { module = "org.springframework.boot:spring-boo
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-security-test = "org.springframework.security:spring-security-test:6.4.2"
spring-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" }
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" }
# CVE-2023-3635
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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";
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -33,17 +34,15 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
"/.well-known/security.txt",
"/favicon.svg",
"/actuator/health/**",
"/actuator/prometheus",
"/api/**",
"/index.html",
"/",
"/assets/**"
"/actuator/prometheus"
)
.permitAll()
.anyRequest()
.denyAll()
.authenticated()
)
.oauth2Login(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
);
Expand Down
12 changes: 11 additions & 1 deletion backend/src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ spring:
multipart:
max-file-size: 10MB
max-request-size: 10MB

security:
oauth2:
client:
registration:
oidcclient:
provider: keycloak
scope:
- openid
- profile
- email
server:
servlet:
session:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
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;

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading