Skip to content

Commit

Permalink
Add an actuator and change security to OAuth2
Browse files Browse the repository at this point in the history
  • Loading branch information
flawmop committed Aug 22, 2024
1 parent f61246b commit c43f883
Show file tree
Hide file tree
Showing 7 changed files with 2,088 additions and 47 deletions.
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ repositories {

ext {
set('springCloudVersion', "2023.0.0")
set('testKeycloakVersion', "2.3.0")
}

configurations {
Expand All @@ -34,6 +35,7 @@ configurations {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
// Note: Introduces commons-logging conflict with spring-boot-starter-web!
implementation 'org.springframework.cloud:spring-cloud-stream-binder-rabbit'
Expand All @@ -60,6 +62,7 @@ testing {
implementation project()
implementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
}

Expand All @@ -69,13 +72,26 @@ testing {
srcDirs = ['src/test-i/java']
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-test'
}
}

test_e2e(JvmTestSuite) {
sources {
java {
srcDirs = ['src/test-e2e/java']
}
resources {
srcDirs = ['src/test-e2e/resources']
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-test'
implementation 'org.testcontainers:junit-jupiter'
implementation "com.github.dasniko:testcontainers-keycloak:${testKeycloakVersion}"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

Expand Down Expand Up @@ -44,15 +47,24 @@ public SecurityConfig(@Value("${com.insilicosoft.actuator.username}") String act
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Note: We've got MVC in classpath, so mvcMatchers (not antMatchers) are in effect
http.authorizeHttpRequests((authz) -> authz.requestMatchers(EndpointRequest.to(InfoEndpoint.class)).authenticated())
http.authorizeHttpRequests((authz) -> authz.requestMatchers(EndpointRequest.to(InfoEndpoint.class)).authenticated()
.requestMatchers(RipIdentifiers.REQUEST_MAPPING_RUN.concat("/**")).hasRole("customer"))
.oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()))
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(withDefaults());
return http.build();
}

// Bypass security completely
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(RipIdentifiers.REQUEST_MAPPING_RUN.concat("/**"));
JwtAuthenticationConverter jwtAuthenticationConverter() {
var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");

var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.jwt.JwtDecoder;

@SpringBootTest
class RipSvcApplicationTests {

@MockBean
private JwtDecoder jwtDecoder;

@Test
void contextLoads() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
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.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.jwt.JwtDecoder;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ActuatorE2E {

private String actuatorUsername;
private String actuatorPassword;

@MockBean
private JwtDecoder jwtDecoder;

@Autowired
private TestRestTemplate restTemplate;

Expand All @@ -26,19 +33,24 @@ public ActuatorE2E(@Value("${com.insilicosoft.actuator.username}") String actuat
this.actuatorPassword = actuatorPassword;
}

public
@Test
void test() {
ResponseEntity<String> response = restTemplate.getForEntity("/actuator/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}

@Test
void test2() {
ResponseEntity<String> response = restTemplate.withBasicAuth(actuatorUsername, actuatorPassword)
.getForEntity("/actuator/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("{}");
@DisplayName("Test info actuator endpoint")
@Nested
class InfoActuator {
@DisplayName("Fail on unauthorized")
@Test
void failOnUnauthorized() {
ResponseEntity<String> response = restTemplate.getForEntity("/actuator/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}

@DisplayName("Success")
@Test
void sucess() {
ResponseEntity<String> response = restTemplate.withBasicAuth(actuatorUsername, actuatorPassword)
.getForEntity("/actuator/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("{}");
}
}

}
Original file line number Diff line number Diff line change
@@ -1,82 +1,175 @@
package com.insilicosoft.portal.svc.rip.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

import java.nio.file.Path;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;

import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.insilicosoft.portal.svc.rip.RipIdentifiers;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
import dasniko.testcontainers.keycloak.KeycloakContainer;

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
public class FileAsyncUploadControllerE2E {

private static final String postUrl = RipIdentifiers.REQUEST_MAPPING_RUN.concat(RipIdentifiers.REQUEST_MAPPING_UPLOAD_ASYNC);
private static final HttpHeaders httpHeaders = new HttpHeaders();
private static final String goodRequestFileName = "request_good.json";
private static final Path goodPath = Path.of("src", "test", "resources", "requests", goodRequestFileName);

private static KeycloakToken bjornTokens;

{
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
}

@Autowired
private TestRestTemplate restTemplate;
private WebTestClient webTestClient;

// Alternatively localhost:5000/keycloak:19.0
@Container
private static final KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:19.0")
.withRealmImportFile("keycloak/test-realm-config.json");


@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri",
() -> keycloak.getAuthServerUrl() + "realms/PolarBookshop");
}

@BeforeAll
static void generateAccessTokens() {
WebClient webClient = WebClient.builder().baseUrl(keycloak.getAuthServerUrl() + "realms/PolarBookshop/protocol/openid-connect/token")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.build();
bjornTokens = authenticateWith("bjorn", "password", webClient);
}

@DisplayName("Test GET method(s)")
@Nested
class GetMethods {
@DisplayName("Success")
@Test
void success() {
ResponseEntity<String> response = restTemplate.getForEntity(RipIdentifiers.REQUEST_MAPPING_RUN,
String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo("All good from FileAsyncUploadController->InputProcessorService!!");
webTestClient.get()
.uri(RipIdentifiers.REQUEST_MAPPING_RUN)
.headers(headers -> {
headers.setBearerAuth(bjornTokens.accessToken);
})
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value(body -> {
assertThat(body).isEqualTo("All good from FileAsyncUploadController->InputProcessorService!!");
});
}
}

@DisplayName("Test POST method(s)")
@Nested
class PostMethods {
@DisplayName("Fail on expected request param not supplied")
@DisplayName("Fail on unauthorized")
@Test
void failOnBadParamName() {
var linkedMVMap = new LinkedMultiValueMap<>();
void failOnUnauthorized() {
webTestClient.post()
.uri(postUrl)
.headers(headers -> {
headers.addAll(httpHeaders);
})
.exchange()
.expectStatus().isUnauthorized();
}

ResponseEntity<String> response = restTemplate.postForEntity(postUrl,
new HttpEntity<>(linkedMVMap, httpHeaders),
String.class);
@DisplayName("Fail on multipart exception")
@Test
void failOnMultipartException() {
webTestClient.post()
.uri(postUrl)
.headers(headers -> {
headers.setBearerAuth(bjornTokens.accessToken);
headers.addAll(httpHeaders);
})
.exchange()
.expectStatus().is5xxServerError()
.expectBody(String.class).value(body -> {
assertThat(body).isEqualTo("Error occurred during file upload - MultipartException");
});
}

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(response.getBody()).isEqualTo("The POST request must supply the parameter '" + RipIdentifiers.PARAM_NAME_SIMULATION_FILE + "'");
@DisplayName("Fail on expected request param not supplied")
@Test
void failOnBadParamName() {
var multipartBodyBuilder = new MultipartBodyBuilder();
multipartBodyBuilder.part("fish", new FileSystemResource(goodPath));
webTestClient.post()
.uri(postUrl)
.headers(headers -> {
headers.setBearerAuth(bjornTokens.accessToken);
})
.body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))
.exchange()
.expectStatus().isBadRequest()
.expectBody(String.class).value(body -> {
assertThat(body).isEqualTo("The POST request must supply the parameter '" + RipIdentifiers.PARAM_NAME_SIMULATION_FILE + "'");
});
}

@DisplayName("Success on a good simulations request file")
@Test
void success() {
var linkedMVMap = new LinkedMultiValueMap<>();
linkedMVMap.add(RipIdentifiers.PARAM_NAME_SIMULATION_FILE, new FileSystemResource(goodPath));
var multipartBodyBuilder = new MultipartBodyBuilder();
multipartBodyBuilder.part(RipIdentifiers.PARAM_NAME_SIMULATION_FILE, new FileSystemResource(goodPath));
webTestClient.post()
.uri(postUrl)
.headers(headers -> {
headers.setBearerAuth(bjornTokens.accessToken);
})
.body(BodyInserters.fromMultipartData(multipartBodyBuilder.build()))
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value(body -> {
assertThat(body).isEqualTo(goodRequestFileName);
});
}
}

ResponseEntity<String> response = restTemplate.postForEntity(postUrl,
new HttpEntity<>(linkedMVMap, httpHeaders),
String.class);
private static KeycloakToken authenticateWith(String username, String password, WebClient webClient) {
return webClient.post()
.body(BodyInserters.fromFormData("grant_type", "password")
.with("client_id", "polar-test")
.with("username", username)
.with("password", password))
.retrieve()
.bodyToMono(KeycloakToken.class)
.block();
}

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo(goodRequestFileName);
private record KeycloakToken(String accessToken) {
@JsonCreator
private KeycloakToken(@JsonProperty("access_token") final String accessToken) {
this.accessToken = accessToken;
}
}
}
Loading

0 comments on commit c43f883

Please sign in to comment.