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