diff --git a/openex-api/pom.xml b/openex-api/pom.xml index 039c5ed5a9..cb36f94877 100644 --- a/openex-api/pom.xml +++ b/openex-api/pom.xml @@ -184,12 +184,30 @@ + + org.springframework.security + spring-security-test + test + + + com.h2database + h2 + test + commons-codec commons-codec 1.15 + + org.projectlombok + lombok + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/openex-api/src/main/java/io/openex/migration/V2_60__Systems_zones.java b/openex-api/src/main/java/io/openex/migration/V2_60__Systems_zones.java new file mode 100644 index 0000000000..918ac56f78 --- /dev/null +++ b/openex-api/src/main/java/io/openex/migration/V2_60__Systems_zones.java @@ -0,0 +1,53 @@ +package io.openex.migration; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +import java.sql.Connection; +import java.sql.Statement; + +@Component +public class V2_60__Systems_zones extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Connection connection = context.getConnection(); + Statement select = connection.createStatement(); + // Create table system + select.execute(""" + CREATE TABLE systems ( + system_id varchar(255) not null constraint systems_pkey primary key, + system_name varchar(255) not null, + system_type varchar(255) not null, + system_ip varchar(255) not null, + system_hostname varchar(255) not null, + system_os varchar(255) not null, + system_created_at timestamp not null default now(), + system_updated_at timestamp not null default now() + ); + CREATE INDEX idx_systems on systems (system_id); + """); + // Create table zone + select.execute(""" + CREATE TABLE zones ( + zone_id varchar(255) not null constraint zones_pkey primary key, + zone_name varchar(255) not null, + zone_description varchar(255) not null, + zone_created_at timestamp not null default now(), + zone_updated_at timestamp not null default now() + ); + CREATE INDEX idx_zones on zones (zone_id); + """); + // Add association table between system and zone + select.execute(""" + CREATE TABLE systems_zones ( + system_id varchar(255) not null constraint system_id_fk references systems on delete cascade, + zone_id varchar(255) not null constraint zone_id_fk references zones on delete cascade, + constraint systems_zones_pkey primary key (system_id, zone_id) + ); + CREATE INDEX idx_systems_zones_system on systems_zones (system_id); + CREATE INDEX idx_systems_zones_zone on systems_zones (zone_id); + """); + } +} diff --git a/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java b/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java index 6dbd76805f..92e9eb80d5 100644 --- a/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java +++ b/openex-api/src/main/java/io/openex/rest/security/SecurityExpression.java @@ -130,4 +130,4 @@ public Object getThis() { return this; } // endregion -} \ No newline at end of file +} diff --git a/openex-api/src/main/java/io/openex/rest/system/SystemApi.java b/openex-api/src/main/java/io/openex/rest/system/SystemApi.java new file mode 100644 index 0000000000..cb65395f37 --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/system/SystemApi.java @@ -0,0 +1,65 @@ +package io.openex.rest.system; + +import io.openex.database.model.System; +import io.openex.database.model.System.OS_TYPE; +import io.openex.database.model.System.SYSTEM_TYPE; +import io.openex.database.repository.SystemRepository; +import io.openex.rest.system.form.SystemInput; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.time.Instant; + +import static io.openex.database.model.User.ROLE_ADMIN; +import static io.openex.helper.StreamHelper.fromIterable; + +@RestController +@RequiredArgsConstructor +public class SystemApi { + public static final String SYSTEM_URI = "/api/systems"; + + private final SystemRepository systemRepository; + + // -- CRUD -- + + @PostMapping(SYSTEM_URI) + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public System createSystem(@Valid @RequestBody final SystemInput input) { + System system = new System(); + system.setUpdateAttributes(input); + // Handle enum + system.setType(SYSTEM_TYPE.valueOf(input.getType())); + system.setOs(OS_TYPE.valueOf(input.getOs())); + return this.systemRepository.save(system); + } + + @GetMapping(SYSTEM_URI) + @PreAuthorize("isObserver()") + public Iterable systems() { + return fromIterable(this.systemRepository.findAll()); + } + + @PutMapping(SYSTEM_URI + "/{systemId}") + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public System updateSystem(@PathVariable @NotBlank final String systemId, + @Valid @RequestBody final SystemInput input) { + System system = this.systemRepository.findById(systemId).orElseThrow(); + system.setUpdateAttributes(input); + system.setUpdatedAt(Instant.now()); + return this.systemRepository.save(system); + } + + @DeleteMapping(SYSTEM_URI + "/{systemId}") + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public void deleteSystem(@PathVariable @NotBlank final String systemId) { + this.systemRepository.deleteById(systemId); + } +} diff --git a/openex-api/src/main/java/io/openex/rest/system/form/SystemInput.java b/openex-api/src/main/java/io/openex/rest/system/form/SystemInput.java new file mode 100644 index 0000000000..edfc039cd0 --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/system/form/SystemInput.java @@ -0,0 +1,34 @@ +package io.openex.rest.system.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; + +import static io.openex.config.AppConfig.MANDATORY_MESSAGE; + +@Getter +@Setter +public class SystemInput { + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("system_name") + private String name; + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("system_type") + private String type; + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("system_ip") + private String ip; + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("system_hostname") + private String hostname; + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("system_os") + private String os; +} diff --git a/openex-api/src/main/java/io/openex/rest/zone/ZoneApi.java b/openex-api/src/main/java/io/openex/rest/zone/ZoneApi.java new file mode 100644 index 0000000000..e0c2ea1fca --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/zone/ZoneApi.java @@ -0,0 +1,85 @@ +package io.openex.rest.zone; + +import io.openex.database.model.System; +import io.openex.database.model.Zone; +import io.openex.database.repository.SystemRepository; +import io.openex.database.repository.ZoneRepository; +import io.openex.rest.zone.form.UpdateSystemsZoneInput; +import io.openex.rest.zone.form.ZoneInput; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.time.Instant; +import java.util.List; + +import static io.openex.database.model.User.ROLE_ADMIN; +import static io.openex.helper.StreamHelper.fromIterable; + +@RestController +@RequiredArgsConstructor +public class ZoneApi { + public static final String ZONE_URI = "/api/zones"; + + private final ZoneRepository zoneRepository; + private final SystemRepository systemRepository; + + // -- CRUD -- + + @PostMapping(ZONE_URI) + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public Zone createZone(@Valid @RequestBody final ZoneInput input) { + Zone zone = new Zone(); + zone.setUpdateAttributes(input); + return this.zoneRepository.save(zone); + } + + @GetMapping(ZONE_URI) + @PreAuthorize("isObserver()") + public Iterable zones() { + return fromIterable(this.zoneRepository.findAll()); + } + + @PutMapping(ZONE_URI + "/{zoneId}") + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public Zone updateZone(@PathVariable @NotBlank final String zoneId, + @Valid @RequestBody final ZoneInput input) { + Zone zone = this.zoneRepository.findById(zoneId).orElseThrow(); + zone.setUpdateAttributes(input); + zone.setUpdatedAt(Instant.now()); + return this.zoneRepository.save(zone); + } + + @DeleteMapping(ZONE_URI + "/{zoneId}") + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public void deleteZone(@PathVariable @NotBlank final String zoneId) { + this.zoneRepository.deleteById(zoneId); + } + + // -- SYSTEM -- + + @GetMapping(ZONE_URI + "/{zoneId}/systems") + @Transactional(rollbackOn = Exception.class) + @PreAuthorize("isObserver()") + public List zoneSystems(@PathVariable @NotBlank final String zoneId) { + return this.zoneRepository.findById(zoneId).orElseThrow().getSystems(); + } + + @PutMapping(ZONE_URI + "/{zoneId}/systems") + @Transactional(rollbackOn = Exception.class) + @RolesAllowed(ROLE_ADMIN) + public Zone addSystemToZone(@PathVariable @NotBlank final String zoneId, + @Valid @RequestBody final UpdateSystemsZoneInput input) { + Zone zone = this.zoneRepository.findById(zoneId).orElseThrow(); + Iterable systems = this.systemRepository.findAllById(input.getSystemIds()); + zone.setSystems(fromIterable(systems)); + return this.zoneRepository.save(zone); + } +} diff --git a/openex-api/src/main/java/io/openex/rest/zone/form/UpdateSystemsZoneInput.java b/openex-api/src/main/java/io/openex/rest/zone/form/UpdateSystemsZoneInput.java new file mode 100644 index 0000000000..063585caab --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/zone/form/UpdateSystemsZoneInput.java @@ -0,0 +1,18 @@ +package io.openex.rest.zone.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Getter +@Setter +public class UpdateSystemsZoneInput { + + @NotEmpty + @JsonProperty("zone_systems") + private List systemIds; + +} diff --git a/openex-api/src/main/java/io/openex/rest/zone/form/ZoneInput.java b/openex-api/src/main/java/io/openex/rest/zone/form/ZoneInput.java new file mode 100644 index 0000000000..139093126b --- /dev/null +++ b/openex-api/src/main/java/io/openex/rest/zone/form/ZoneInput.java @@ -0,0 +1,21 @@ +package io.openex.rest.zone.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; + +import static io.openex.config.AppConfig.MANDATORY_MESSAGE; + +@Getter +@Setter +public class ZoneInput { + + @NotBlank(message = MANDATORY_MESSAGE) + @JsonProperty("zone_name") + private String name; + + @JsonProperty("zone_description") + private String description; +} diff --git a/openex-api/src/test/java/io/openex/AppTests.java b/openex-api/src/test/java/io/openex/AppTests.java deleted file mode 100644 index 2c43ddda47..0000000000 --- a/openex-api/src/test/java/io/openex/AppTests.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.openex; - -import io.openex.database.model.User; -import io.openex.database.repository.UserRepository; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -class AppTests { - - @Autowired - private UserRepository userRepository; - - @Test - void whenTechnical_thenIncidentTypeShouldBeFound() { - Optional technical = userRepository.findByEmail("admin@openex.io"); - assertThat(technical.isPresent()).isEqualTo(true); - } -} diff --git a/openex-api/src/test/java/io/openex/rest/system/SystemApiTest.java b/openex-api/src/test/java/io/openex/rest/system/SystemApiTest.java new file mode 100644 index 0000000000..241d5e6c35 --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/system/SystemApiTest.java @@ -0,0 +1,154 @@ +package io.openex.rest.system; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.openex.database.model.System; +import io.openex.database.repository.SystemRepository; +import io.openex.rest.system.form.SystemInput; +import io.openex.rest.utils.WithMockObserverUser; +import io.openex.rest.utils.WithMockPlannerUser; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static io.openex.database.model.System.OS_TYPE.LINUX; +import static io.openex.database.model.System.SYSTEM_TYPE.ENDPOINT; +import static io.openex.rest.system.SystemApi.SYSTEM_URI; +import static io.openex.rest.utils.JsonUtils.asJsonString; +import static io.openex.rest.utils.JsonUtils.asStringJson; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(OrderAnnotation.class) +@TestInstance(PER_CLASS) +public class SystemApiTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private SystemRepository systemRepository; + + @AfterAll + void teardown() { + this.systemRepository.deleteAll(); + } + + @Test + @Order(1) + @WithMockUser(roles={"ADMIN"}) + void createSystemTest() throws Exception { + // Prepare + SystemInput systemInput = new SystemInput(); + systemInput.setName("System"); + systemInput.setType(ENDPOINT.name()); + systemInput.setIp("127.0.0.1"); + systemInput.setHostname("hostname"); + systemInput.setOs(LINUX.name()); + + // Execute + String response = this.mvc + .perform(post(SYSTEM_URI) + .content(asJsonString(systemInput)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + System systemResponse = asStringJson(response, System.class); + + // Assert + assertEquals("System", systemResponse.getName()); + } + + @Test + @Order(2) + @WithMockObserverUser + void systemsTestSuccess() throws Exception { + // Execute + String response = this.mvc + .perform(get(SYSTEM_URI).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + List systemsResponse = asStringJson(response, new TypeReference<>() { + }); + + // Assert + assertEquals(1, systemsResponse.size()); + } + + @Test + @Order(3) + @WithMockPlannerUser + void systemsTestForbidden() throws Exception { + // Execute & Assert + this.mvc.perform(get(SYSTEM_URI).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + @Order(4) + @WithMockUser(roles={"ADMIN"}) + void updateSystemTest() throws Exception { + // Prepare + System systemResponse = getFirstSystem(); + SystemInput systemInput = new SystemInput(); + systemInput.setName("Change system name"); + systemInput.setType(systemResponse.getType().name()); + systemInput.setIp(systemResponse.getIp()); + systemInput.setHostname(systemResponse.getHostname()); + systemInput.setOs(systemResponse.getOs().name()); + + // Execute + String response = this.mvc + .perform(put(SYSTEM_URI + "/" + systemResponse.getId()) + .content(asJsonString(systemInput)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + systemResponse = asStringJson(response, System.class); + + // Assert + assertEquals("Change system name", systemResponse.getName()); + } + + @Test + @Order(5) + @WithMockUser(roles={"ADMIN"}) + void deleteSystemTest() throws Exception { + // Prepare + System systemResponse = getFirstSystem(); + + // Execute + this.mvc.perform(delete(SYSTEM_URI + "/" + systemResponse.getId()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + + // Assert + assertEquals(0, this.systemRepository.count()); + } + + // -- PRIVATE -- + + private System getFirstSystem() { + return this.systemRepository.findAll().iterator().next(); + } + +} diff --git a/openex-api/src/test/java/io/openex/rest/utils/JsonUtils.java b/openex-api/src/test/java/io/openex/rest/utils/JsonUtils.java new file mode 100644 index 0000000000..3b279635aa --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/utils/JsonUtils.java @@ -0,0 +1,47 @@ +package io.openex.rest.utils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +public class JsonUtils { + + private static ObjectMapper mapper; + + private static ObjectMapper getMapper() { + if (mapper != null) { + return mapper; + } + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } + + public static String asJsonString(@NotNull final Object obj) { + try { + return getMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static T asStringJson(@NotBlank final String obj, @NotNull final Class clazz) { + try { + return getMapper().readValue(obj, clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static T asStringJson(@NotBlank final String obj, @NotNull final TypeReference typeReference) { + try { + return getMapper().readValue(obj, typeReference); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUser.java b/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUser.java new file mode 100644 index 0000000000..d5a0159b30 --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUser.java @@ -0,0 +1,14 @@ +package io.openex.rest.utils; + +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.openex.rest.utils.WithMockObserverUserSecurityContextFactory.MOCK_USER_OBSERVER_EMAIL; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockObserverUserSecurityContextFactory.class) +public @interface WithMockObserverUser { + String email() default MOCK_USER_OBSERVER_EMAIL; +} diff --git a/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUserSecurityContextFactory.java b/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUserSecurityContextFactory.java new file mode 100644 index 0000000000..a504cfefda --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/utils/WithMockObserverUserSecurityContextFactory.java @@ -0,0 +1,75 @@ +package io.openex.rest.utils; + +import io.openex.database.model.Grant; +import io.openex.database.model.Group; +import io.openex.database.model.User; +import io.openex.database.repository.GrantRepository; +import io.openex.database.repository.GroupRepository; +import io.openex.database.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.List; + +import static io.openex.database.model.Grant.GRANT_TYPE.OBSERVER; + +@Component +public class WithMockObserverUserSecurityContextFactory implements WithSecurityContextFactory { + + public static final String MOCK_USER_OBSERVER_EMAIL = "observer@opencti.io"; + @Autowired + private GrantRepository grantRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private UserRepository userRepository; + + @Override + public SecurityContext createSecurityContext(WithMockObserverUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + User principal = this.userRepository.findByEmail(customUser.email()).orElseThrow(); + Authentication auth = + new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities()); + context.setAuthentication(auth); + return context; + } + + @PostConstruct + private void postConstruct() { + this.createObserverMockUser(); + } + + @PreDestroy + public void preDestroy() { + this.userRepository.deleteById(this.userRepository.findByEmail(MOCK_USER_OBSERVER_EMAIL).orElseThrow().getId()); + } + + private void createObserverMockUser() { + if (this.userRepository.findByEmail(MOCK_USER_OBSERVER_EMAIL).isPresent()) { + return; + } + + // Create group + Group group = new Group(); + group.setName("Observer group"); + group = this.groupRepository.save(group); + // Create grant + Grant grant = new Grant(); + grant.setName(OBSERVER); + grant.setGroup(group); + this.grantRepository.save(grant); + // Create user + User user = new User(); + user.setGroups(List.of(group)); + user.setEmail(MOCK_USER_OBSERVER_EMAIL); + this.userRepository.save(user); + } +} diff --git a/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUser.java b/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUser.java new file mode 100644 index 0000000000..85db0f29a5 --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUser.java @@ -0,0 +1,14 @@ +package io.openex.rest.utils; + +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.openex.rest.utils.WithMockPlannerUserSecurityContextFactory.MOCK_USER_PLANNER_EMAIL; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockPlannerUserSecurityContextFactory.class) +public @interface WithMockPlannerUser { + String email() default MOCK_USER_PLANNER_EMAIL; +} diff --git a/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUserSecurityContextFactory.java b/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUserSecurityContextFactory.java new file mode 100644 index 0000000000..78aa626ac3 --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/utils/WithMockPlannerUserSecurityContextFactory.java @@ -0,0 +1,75 @@ +package io.openex.rest.utils; + +import io.openex.database.model.Grant; +import io.openex.database.model.Group; +import io.openex.database.model.User; +import io.openex.database.repository.GrantRepository; +import io.openex.database.repository.GroupRepository; +import io.openex.database.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.List; + +import static io.openex.database.model.Grant.GRANT_TYPE.PLANNER; + +@Component +public class WithMockPlannerUserSecurityContextFactory implements WithSecurityContextFactory { + + public static final String MOCK_USER_PLANNER_EMAIL = "planner@opencti.io"; + @Autowired + private GrantRepository grantRepository; + @Autowired + private GroupRepository groupRepository; + @Autowired + private UserRepository userRepository; + + @Override + public SecurityContext createSecurityContext(WithMockPlannerUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + User principal = this.userRepository.findByEmail(customUser.email()).orElseThrow(); + Authentication auth = + new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities()); + context.setAuthentication(auth); + return context; + } + + @PostConstruct + private void postConstruct() { + this.createPlannerMockUser(); + } + + @PreDestroy + public void preDestroy() { + this.userRepository.deleteById(this.userRepository.findByEmail(MOCK_USER_PLANNER_EMAIL).orElseThrow().getId()); + } + + private void createPlannerMockUser() { + if (this.userRepository.findByEmail(MOCK_USER_PLANNER_EMAIL).isPresent()) { + return; + } + + // Create group + Group group = new Group(); + group.setName("Planner group"); + group = this.groupRepository.save(group); + // Create grant + Grant grant = new Grant(); + grant.setName(PLANNER); + grant.setGroup(group); + this.grantRepository.save(grant); + // Create user + User user = new User(); + user.setGroups(List.of(group)); + user.setEmail(MOCK_USER_PLANNER_EMAIL); + this.userRepository.save(user); + } +} diff --git a/openex-api/src/test/java/io/openex/rest/zone/ZoneApiTest.java b/openex-api/src/test/java/io/openex/rest/zone/ZoneApiTest.java new file mode 100644 index 0000000000..d4525d9ebf --- /dev/null +++ b/openex-api/src/test/java/io/openex/rest/zone/ZoneApiTest.java @@ -0,0 +1,201 @@ +package io.openex.rest.zone; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.openex.database.model.System; +import io.openex.database.model.Zone; +import io.openex.database.repository.SystemRepository; +import io.openex.database.repository.ZoneRepository; +import io.openex.rest.utils.WithMockObserverUser; +import io.openex.rest.utils.WithMockPlannerUser; +import io.openex.rest.zone.form.UpdateSystemsZoneInput; +import io.openex.rest.zone.form.ZoneInput; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static io.openex.database.model.System.OS_TYPE.LINUX; +import static io.openex.database.model.System.SYSTEM_TYPE.ENDPOINT; +import static io.openex.rest.utils.JsonUtils.asJsonString; +import static io.openex.rest.utils.JsonUtils.asStringJson; +import static io.openex.rest.zone.ZoneApi.ZONE_URI; +import static java.util.List.of; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +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; + +@SpringBootTest +@AutoConfigureMockMvc +@TestMethodOrder(OrderAnnotation.class) +@TestInstance(PER_CLASS) +public class ZoneApiTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ZoneRepository zoneRepository; + @Autowired + private SystemRepository systemRepository; + + @AfterAll + public void teardown() { + this.zoneRepository.deleteAll(); + this.systemRepository.deleteAll(); + } + + @Test + @Order(1) + @WithMockUser(roles={"ADMIN"}) + void createZoneTest() throws Exception { + // Prepare + ZoneInput zoneInput = new ZoneInput(); + zoneInput.setName("Zone"); + + // Execute + String response = this.mvc + .perform(post(ZONE_URI) + .content(asJsonString(zoneInput)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + Zone zoneResponse = asStringJson(response, Zone.class); + + // Assert + assertEquals("Zone", zoneResponse.getName()); + } + + @Test + @Order(2) + @WithMockObserverUser + void zonesTest() throws Exception { + // Execute + String response = this.mvc + .perform(get(ZONE_URI).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + List zonesResponse = asStringJson(response, new TypeReference<>() { + }); + + // Assert + assertEquals(1, zonesResponse.size()); + } + + @Test + @Order(3) + @WithMockPlannerUser + void systemsTestForbidden() throws Exception { + // Execute & Assert + this.mvc.perform(get(ZONE_URI).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is4xxClientError()); + } + + @Test + @Order(4) + @WithMockUser(roles={"ADMIN"}) + void updateZoneTest() throws Exception { + // Prepare + Zone zoneResponse = getFirstZone(); + ZoneInput zoneInput = new ZoneInput(); + zoneInput.setName("Change zone name"); + + // Execute + String response = this.mvc + .perform(put(ZONE_URI + "/" + zoneResponse.getId()) + .content(asJsonString(zoneInput)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + zoneResponse = asStringJson(response, Zone.class); + + // Assert + assertEquals("Change zone name", zoneResponse.getName()); + } + + @Test + @Order(5) + @WithMockUser(roles={"ADMIN"}) + void addSystemToZoneTest() throws Exception { + // Prepare + Zone zoneResponse = getFirstZone(); + System system = new System(); + system.setName("System"); + system.setType(ENDPOINT); + system.setIp("127.0.0.1"); + system.setHostname("hostname"); + system.setOs(LINUX); + system = this.systemRepository.save(system); + UpdateSystemsZoneInput input = new UpdateSystemsZoneInput(); + input.setSystemIds(of(system.getId())); + + // Execute + String response = this.mvc.perform(put(ZONE_URI + "/" + zoneResponse.getId() + "/systems") + .content(asJsonString(input)) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + + // Assert + assertTrue(response.contains(system.getId())); + } + + @Test + @Order(6) + @WithMockObserverUser + void zoneSystemsTest() throws Exception { + // Prepare + Zone zoneResponse = getFirstZone(); + + // Execute & Assert + this.mvc.perform(get(ZONE_URI + "/" + zoneResponse.getId() + "/systems") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$", Matchers.not(Matchers.emptyArray()))); + } + + @Test + @Order(7) + @WithMockUser(roles={"ADMIN"}) + void deleteZoneTest() throws Exception { + // Prepare + Zone zoneResponse = getFirstZone(); + + // Execute + this.mvc.perform(delete(ZONE_URI + "/" + zoneResponse.getId()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()); + + // Assert + assertEquals(0, this.zoneRepository.count()); + } + + // -- PRIVATE -- + + private Zone getFirstZone() { + return this.zoneRepository.findAll().iterator().next(); + } + +} diff --git a/openex-api/src/test/resources/application.properties b/openex-api/src/test/resources/application.properties index 35fa4fe587..d884bef4d4 100644 --- a/openex-api/src/test/resources/application.properties +++ b/openex-api/src/test/resources/application.properties @@ -31,27 +31,18 @@ spring.mvc.pathmatch.matching-strategy=ant_path_matcher spring.quartz.properties.org.quartz.threadPool.threadCount=1 # Database Properties -spring.datasource.url=jdbc:postgresql://... -spring.datasource.username=openex -spring.datasource.password= -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL92Dialect -spring.jpa.hibernate.ddl-auto=validate +spring.datasource.url = jdbc:h2:mem:test +spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect # spring.jpa.show-sql=true # spring.jpa.properties.hibernate.format_sql=true -spring.flyway.url=${spring.datasource.url} -spring.flyway.user=${spring.datasource.username} -spring.flyway.password=${spring.datasource.password} -spring.flyway.table=migrations -spring.flyway.locations=classpath:io/openex/migration -spring.flyway.baseline-on-migrate=true -spring.flyway.baseline-version=0 +spring.flyway.enabled=false # Minio Properties -minio.endpoint=127.0.0.1 +minio.endpoint=localhost +minio.port=10000 minio.bucket=openex -minio.port=9000 -minio.access-key= -minio.access-secret= +minio.access-key=minioadmin +minio.access-secret=minioadmin # Logging logging.level.root=fatal @@ -90,4 +81,4 @@ openex.mail.imap.sent=Sent openex.mail.imap.ssl.trust=* openex.mail.imap.ssl.enable=true openex.mail.imap.auth=true -openex.mail.imap.starttls.enable=true \ No newline at end of file +openex.mail.imap.starttls.enable=true