diff --git a/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java new file mode 100644 index 00000000..d81c13a8 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/CasAuthenticationFilter.java @@ -0,0 +1,35 @@ +package net.hostsharing.hsadminng.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class CasAuthenticationFilter implements Filter { + + @Autowired + private CasServiceTicketValidator ticketValidator; + + @Override + @SneakyThrows + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) { + final var httpRequest = (HttpServletRequest) request; + final var httpResponse = (HttpServletResponse) response; + + final var ticket = httpRequest.getHeader("Authorization"); + + if (ticket == null || !ticketValidator.validateTicket(ticket)) { + httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/config/CasServiceTicketValidator.java b/src/main/java/net/hostsharing/hsadminng/config/CasServiceTicketValidator.java new file mode 100644 index 00000000..bb1c78cb --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/config/CasServiceTicketValidator.java @@ -0,0 +1,40 @@ +package net.hostsharing.hsadminng.config; + +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Service +public class CasServiceTicketValidator { + + @Value("${hsadminng.cas.server-url}") + private String casServerUrl; + + @Value("${hsadminng.cas.service-url}") + private String serviceUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + @SneakyThrows + public boolean validateTicket(final String ticket) { + if (casServerUrl.equals("fake") && ticket.equals("test")) { + return true; + } + + final var url = casServerUrl + "/p3/serviceValidate" + + "?service=" + URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8) + + "&ticket=" + URLEncoder.encode(ticket, StandardCharsets.UTF_8); + + final var response = restTemplate.getForObject(url, String.class); + + final var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(new java.io.ByteArrayInputStream(response.getBytes())); + + return doc.getElementsByTagName("cas:authenticationSuccess").getLength() > 0; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f75ae429..054973b2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -36,6 +36,9 @@ liquibase: hsadminng: postgres: leakproof: + cas: + server-url: https://cas.example.com/cas + service-url: http://localhost:8080/api # TODO.conf: deployment target metrics: distribution: diff --git a/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java new file mode 100644 index 00000000..14940868 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/config/CasAuthenticationFilterIntegrationTest.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.config; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; + + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"server.port=0"}) +// IMPORTANT: To test prod config, do not use test profile! +class CasAuthenticationFilterIntegrationTest { + + @Value("${local.server.port}") + private int serverPort; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void shouldRejectRequest() { + final var result = this.restTemplate.getForEntity( + "http://localhost:" + this.serverPort + "/api/ping", String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java index a69ca9f4..de8c63ea 100644 --- a/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/config/WebSecurityConfigIntegrationTest.java @@ -8,13 +8,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = {"management.port=0", "server.port=0"}) +@TestPropertySource(properties = {"management.port=0", "server.port=0", "hsadminng.cas.server-url=fake"}) // IMPORTANT: To test prod config, do not use test profile! class WebSecurityConfigIntegrationTest { @@ -29,8 +32,18 @@ class WebSecurityConfigIntegrationTest { @Test public void shouldSupportPingEndpoint() { - final var result = this.restTemplate.getForEntity( - "http://localhost:" + this.serverPort + "/api/ping", String.class); + // fake Authorization header + final var headers = new HttpHeaders(); + headers.set("Authorization", "test"); + + // http request + final var result = restTemplate.exchange( + "http://localhost:" + this.serverPort + "/api/ping", + HttpMethod.GET, + new HttpEntity(null, headers), + String.class + ); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getBody()).startsWith("pong"); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java index 01c5dede..d0167933 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/scenarios/UseCase.java @@ -160,6 +160,7 @@ public abstract class UseCase> { .GET() .uri(new URI("http://localhost:" + testSuite.port + uriPath)) .header("current-subject", ScenarioTest.RUN_AS_USER) + .header("Authorization", "test") .timeout(seconds(10)) .build(); final var response = client.send(request, BodyHandlers.ofString()); @@ -175,6 +176,7 @@ public abstract class UseCase> { .uri(new URI("http://localhost:" + testSuite.port + uriPath)) .header("Content-Type", "application/json") .header("current-subject", ScenarioTest.RUN_AS_USER) + .header("Authorization", "test") .timeout(seconds(10)) .build(); final var response = client.send(request, BodyHandlers.ofString()); @@ -190,6 +192,7 @@ public abstract class UseCase> { .uri(new URI("http://localhost:" + testSuite.port + uriPath)) .header("Content-Type", "application/json") .header("current-subject", ScenarioTest.RUN_AS_USER) + .header("Authorization", "test") .timeout(seconds(10)) .build(); final var response = client.send(request, BodyHandlers.ofString()); @@ -204,6 +207,7 @@ public abstract class UseCase> { .uri(new URI("http://localhost:" + testSuite.port + uriPath)) .header("Content-Type", "application/json") .header("current-subject", ScenarioTest.RUN_AS_USER) + .header("Authorization", "test") .timeout(seconds(10)) .build(); final var response = client.send(request, BodyHandlers.ofString()); diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index f0df4e4b..35ec34bf 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -51,3 +51,7 @@ testcontainers: network: mode: host +hsadminng: + cas: + server-url: fake + service-url: http://localhost:8080/api # not really used in test config