Commit c5f3802a authored by Jean-Baptiste Nizet's avatar Jean-Baptiste Nizet
Browse files

feat: add security on harvest and actuator endpoints

Also:
- configure actuator for more information
- use Import instead of SpringBootTest to add config classes to tests
parent 4ca92206
...@@ -83,14 +83,15 @@ environment variable for example). ...@@ -83,14 +83,15 @@ environment variable for example).
The files must have the extension `.json`, and must be stored in that directory (not in a sub-directory). The files must have the extension `.json`, and must be stored in that directory (not in a sub-directory).
Once the files are ready and the server is started, the harvest is triggered by sending a POST request Once the files are ready and the server is started, the harvest is triggered by sending a POST request
to the endpoint `/api/harvests`, without any request body. to the endpoint `/api/harvests`, without any request body.
This endpoint, as well as the actuator endpoints, is only accessible to an authenticated user. The user (`rare`) and its password (`f01a7031fc17`) are configured in the application.yml file (and can thus be overridden using environment variables for example).
Example with the `http` command ([HTTPie](https://httpie.org/)): Example with the `http` command ([HTTPie](https://httpie.org/)):
http POST http://localhost:8080/api/harvests http --auth rare:f01a7031fc17 POST http://localhost:8080/api/harvests
Example with the `curl` command: Example with the `curl` command:
curl -i -X POST http://localhost:8080/api/harvests curl -i -X POST -u rare:f01a7031fc17 http://localhost:8080/api/harvests
The harvest job is executed asynchronously, and a response is immediately sent back, with the URL allowing The harvest job is executed asynchronously, and a response is immediately sent back, with the URL allowing
to get the result of the job. For example: to get the result of the job. For example:
...@@ -102,11 +103,11 @@ to get the result of the job. For example: ...@@ -102,11 +103,11 @@ to get the result of the job. For example:
To get the result of the job, you can then send a GET request to the returned URL: To get the result of the job, you can then send a GET request to the returned URL:
http GET http://localhost:8080/api/harvests/abb5784d-3006-48fb-b5db-d3ff9583e8b9 http --auth rare:f01a7031fc17 GET http://localhost:8080/api/harvests/abb5784d-3006-48fb-b5db-d3ff9583e8b9
or or
curl http://localhost:8080/api/harvests/abb5784d-3006-48fb-b5db-d3ff9583e8b9 curl -u rare:f01a7031fc17 http://localhost:8080/api/harvests/abb5784d-3006-48fb-b5db-d3ff9583e8b9
`http` has the advantage of nicely formetting the returned JSON. `http` has the advantage of nicely formetting the returned JSON.
......
...@@ -72,6 +72,8 @@ tasks { ...@@ -72,6 +72,8 @@ tasks {
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.data:spring-data-elasticsearch:3.1.0.M3") implementation("org.springframework.data:spring-data-elasticsearch:3.1.0.M3")
implementation("org.elasticsearch:elasticsearch:6.3.1") implementation("org.elasticsearch:elasticsearch:6.3.1")
implementation("org.elasticsearch.client:transport:6.3.1") implementation("org.elasticsearch.client:transport:6.3.1")
......
package fr.inra.urgi.rare.config;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
/**
* Security configuration. It makes sure that harvest endpoints and the actuator endpoints are only accessible to
* authenticated users
* @author JB Nizet
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/harvests", "/api/harvests/**").authenticated()
.requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated() // EndpointRequest.toAnyEndpoint()
// only targets the **actuator**
// endpoints.
.antMatchers("/**").permitAll()
.and()
.httpBasic()
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
...@@ -3,6 +3,11 @@ rare: ...@@ -3,6 +3,11 @@ rare:
elasticsearch-prefix: '' elasticsearch-prefix: ''
spring: spring:
security:
user:
name: rare
password: f01a7031fc17
data: data:
elasticsearch: elasticsearch:
cluster: cluster:
...@@ -10,6 +15,15 @@ spring: ...@@ -10,6 +15,15 @@ spring:
host: 127.0.0.1 host: 127.0.0.1
port: 9300 port: 9300
management:
endpoint:
health:
show-details: 'always'
endpoints:
web:
exposure:
include: '*'
server: server:
compression: compression:
enabled: true enabled: true
......
...@@ -10,13 +10,13 @@ import org.junit.jupiter.api.Test; ...@@ -10,13 +10,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@TestPropertySource("/test.properties") @TestPropertySource("/test.properties")
@SpringBootTest(classes = ElasticSearchConfig.class) @Import(ElasticSearchConfig.class)
@JsonTest @JsonTest
class GeneticResourceDaoTest { class GeneticResourceDaoTest {
......
...@@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; ...@@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
...@@ -19,7 +19,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; ...@@ -19,7 +19,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@TestPropertySource("/test.properties") @TestPropertySource("/test.properties")
@SpringBootTest(classes = ElasticSearchConfig.class) @Import(ElasticSearchConfig.class)
@JsonTest @JsonTest
class HarvestResultDaoTest { class HarvestResultDaoTest {
@Autowired @Autowired
......
...@@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; ...@@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
/** /**
...@@ -21,7 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; ...@@ -21,7 +21,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
* @author JB Nizet * @author JB Nizet
*/ */
@JsonTest @JsonTest
@SpringBootTest(classes = HarvestConfig.class) @Import(HarvestConfig.class)
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
class GeneticResourceTest { class GeneticResourceTest {
......
package fr.inra.urgi.rare.harvest;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import fr.inra.urgi.rare.config.SecurityConfig;
import fr.inra.urgi.rare.dao.HarvestResultDao;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
/**
* Tests that security is active for {@link HarvesterController}
* @author JB Nizet
*/
@ExtendWith(SpringExtension.class)
@Import(SecurityConfig.class)
@WebMvcTest(controllers = HarvesterController.class)
class HarvesterControllerSecurityTest {
@MockBean
private AsyncHarvester mockAsyncHarvester;
@MockBean
private HarvestResultDao mockHarvestResultDao;
@Autowired
private MockMvc mockMvc;
@Test
void shouldBeUnauthorizedWhenGetting() throws Exception {
HarvestResult harvestResult = HarvestResult.builder().build();
mockMvc.perform(get("/api/harvests/{id}", harvestResult.getId()))
.andExpect(status().isUnauthorized());
}
@Test
void shouldBeUnauthorizedWhenCreating() throws Exception {
mockMvc.perform(post("/api/harvests"))
.andExpect(status().isUnauthorized());
}
}
...@@ -24,7 +24,7 @@ import org.springframework.test.web.servlet.MockMvc; ...@@ -24,7 +24,7 @@ import org.springframework.test.web.servlet.MockMvc;
* @author JB Nizet * @author JB Nizet
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = HarvesterController.class) @WebMvcTest(controllers = HarvesterController.class, secure = false)
class HarvesterControllerTest { class HarvesterControllerTest {
@MockBean @MockBean
private AsyncHarvester mockAsyncHarvester; private AsyncHarvester mockAsyncHarvester;
......
...@@ -35,7 +35,7 @@ import org.mockito.Mock; ...@@ -35,7 +35,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
/** /**
...@@ -45,7 +45,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; ...@@ -45,7 +45,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
@TestInstance(Lifecycle.PER_CLASS) @TestInstance(Lifecycle.PER_CLASS)
@ExtendWith({MockitoExtension.class, TempDirectory.class, SpringExtension.class}) @ExtendWith({MockitoExtension.class, TempDirectory.class, SpringExtension.class})
@JsonTest @JsonTest
@SpringBootTest(classes = HarvestConfig.class) @Import(HarvestConfig.class)
class HarvesterTest { class HarvesterTest {
@Mock @Mock
......
...@@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. ...@@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.List; import java.util.List;
import fr.inra.urgi.rare.config.SecurityConfig;
import fr.inra.urgi.rare.dao.GeneticResourceDao; import fr.inra.urgi.rare.dao.GeneticResourceDao;
import fr.inra.urgi.rare.domain.GeneticResource; import fr.inra.urgi.rare.domain.GeneticResource;
import fr.inra.urgi.rare.domain.GeneticResourceBuilder; import fr.inra.urgi.rare.domain.GeneticResourceBuilder;
...@@ -17,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith; ...@@ -17,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.elasticsearch.core.query.SearchQuery; import org.springframework.data.elasticsearch.core.query.SearchQuery;
...@@ -27,6 +29,7 @@ import org.springframework.test.web.servlet.MockMvc; ...@@ -27,6 +29,7 @@ import org.springframework.test.web.servlet.MockMvc;
* MVC tests for {@link SearchController} * MVC tests for {@link SearchController}
*/ */
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
@Import(SecurityConfig.class)
@WebMvcTest(controllers = SearchController.class) @WebMvcTest(controllers = SearchController.class)
class SearchControllerTest { class SearchControllerTest {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment