diff --git a/.drone.yml b/.drone.yml index 24e1cbdfad..8ef1f38aab 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,7 +10,7 @@ steps: MINIO_ENDPOINT: minio MINIO_PORT: 9000 commands: - - mvn clean install -q -DskipTests + - mvn clean install -DskipTests - mvn spotless:check - cd openbas-api - mvn test -q diff --git a/openbas-api/pom.xml b/openbas-api/pom.xml index 9683f18017..61baf256b9 100644 --- a/openbas-api/pom.xml +++ b/openbas-api/pom.xml @@ -90,6 +90,11 @@ spring-boot-configuration-processor true + + org.springframework.boot + spring-boot-devtools + true + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java index 601da469c8..89fda643ad 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/ExerciseInjectApi.java @@ -22,7 +22,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.HashMap; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -81,13 +80,6 @@ public Iterable exerciseInjectsSimple( joinMap); } - @DeleteMapping(EXERCISE_URI + "/{exerciseId}/injects") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public void deleteListOfInjectsForExercise( - @PathVariable final String exerciseId, @RequestBody List injectIds) { - injectService.deleteAllByIds(injectIds); - } - @PostMapping("/api/exercise/{exerciseId}/injects/test") public Page findAllExerciseInjectTests( @PathVariable @NotBlank String exerciseId, diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 4d68166be3..72032ab6e2 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -25,6 +25,7 @@ import io.openbas.executors.Executor; import io.openbas.injector_contract.ContractType; import io.openbas.rest.atomic_testing.form.InjectResultOutput; +import io.openbas.rest.exception.BadRequestException; import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.helper.RestBehavior; import io.openbas.rest.inject.form.*; @@ -36,6 +37,7 @@ import io.openbas.service.TagRuleService; import io.openbas.telemetry.Tracing; import io.openbas.utils.pagination.SearchPaginationInput; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -51,6 +53,7 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -198,17 +201,6 @@ public Inject updateInject( return injectRepository.save(inject); } - @Transactional(rollbackFor = Exception.class) - @PutMapping(INJECT_URI + "/{exerciseId}/{injectId}/bulk") - @PreAuthorize("isExercisePlanner(#exerciseId)") - public Inject bulkUpdateInject( - @PathVariable String exerciseId, - @PathVariable String injectId, - @Valid @RequestBody InjectInput input) { - Inject inject = bulkUpdateInject(injectId, input); - return injectRepository.save(inject); - } - // -- EXERCISES -- @LogExecutionTime @@ -651,17 +643,6 @@ public Inject scenarioInject( return injectRepository.findById(injectId).orElseThrow(ElementNotFoundException::new); } - @Transactional(rollbackFor = Exception.class) - @PutMapping(SCENARIO_URI + "/{scenarioId}/injects/{injectId}/bulk") - @PreAuthorize("isScenarioPlanner(#scenarioId)") - public Inject bulkUpdateInjectForScenario( - @PathVariable String scenarioId, - @PathVariable String injectId, - @Valid @RequestBody InjectInput input) { - Inject inject = bulkUpdateInject(injectId, input); - return injectRepository.save(inject); - } - @Transactional(rollbackFor = Exception.class) @PutMapping(SCENARIO_URI + "/{scenarioId}/injects/{injectId}") @PreAuthorize("isScenarioPlanner(#scenarioId)") @@ -706,6 +687,55 @@ public void deleteInjectForScenario( this.injectRepository.deleteById(injectId); } + @Operation( + description = "Bulk update of injects", + tags = {"Injects"}) + @Transactional(rollbackFor = Exception.class) + @PutMapping(INJECT_URI) + @LogExecutionTime + @Tracing(name = "Bulk update of injects", layer = "api", operation = "PUT") + public List bulkUpdateInject(@RequestBody @Valid final InjectBulkUpdateInputs input) { + + // Control and format inputs + if (CollectionUtils.isEmpty(input.getInjectIDsToProcess()) + && input.getSearchPaginationInput() == null) { + throw new BadRequestException( + "Either injectIDsToProcess or searchPaginationInput must be provided"); + } + + // Retrieve injects that match the search input and check that the user is allowed to delete + // them + List injectsToUpdate = this.injectService.getInjectsAndCheckIsPlanner(input); + + // Services calls + // Bulk update + return this.injectService.bulkUpdateInject(injectsToUpdate, input.getUpdateOperations()); + } + + @Operation( + description = "Bulk delete of injects", + tags = {"injects-api"}) + @Transactional(rollbackFor = Exception.class) + @DeleteMapping(INJECT_URI) + @LogExecutionTime + @Tracing(name = "Bulk delete of injects", layer = "api", operation = "DELETE") + public void bulkDelete(@RequestBody @Valid final InjectBulkProcessingInput input) { + + // Control and format inputs + if (CollectionUtils.isEmpty(input.getInjectIDsToProcess()) + && input.getSearchPaginationInput() == null) { + throw new BadRequestException( + "Either injectIDsToProcess or searchPaginationInput must be provided"); + } + + // Retrieve injects that match the search input and check that the user is allowed to delete + // them + List injectsToDelete = this.injectService.getInjectsAndCheckIsPlanner(input); + + // Bulk delete + this.injectService.deleteAllByIds(injectsToDelete.stream().map(Inject::getId).toList()); + } + // -- PRIVATE -- private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInput input) { diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java index 0ec56b48ca..e465828861 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/ScenarioInjectApi.java @@ -18,7 +18,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.HashMap; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -66,13 +65,6 @@ public Iterable scenarioInjectsSimple( joinMap); } - @DeleteMapping(SCENARIO_URI + "/{scenarioId}/injects") - @PreAuthorize("isScenarioPlanner(#scenarioId)") - public void deleteListOfInjectsForScenario( - @PathVariable final String scenarioId, @RequestBody List injectIds) { - injectService.deleteAllByIds(injectIds); - } - @PostMapping("/api/scenario/{scenarioId}/injects/test") public Page findAllScenarioInjectTests( @PathVariable @NotBlank String scenarioId, diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkProcessingInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkProcessingInput.java new file mode 100644 index 0000000000..5a295f7dba --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkProcessingInput.java @@ -0,0 +1,32 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.utils.pagination.SearchPaginationInput; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** Represent the input of a bulk processing (delete and tests) calls for injects */ +@Setter +@Getter +public class InjectBulkProcessingInput { + + /** + * The search input, used to select the injects to update. Must be provided if injectIDsToDelete + * is not provided + */ + @JsonProperty("search_pagination_input") + private SearchPaginationInput searchPaginationInput; + + /** The list of injects to process. Must be provided if searchPaginationInput is not provided */ + @JsonProperty("inject_ids_to_process") + private List injectIDsToProcess; + + /** The list of injects to ignore from the search input */ + @JsonProperty("inject_ids_to_ignore") + private List injectIDsToIgnore; + + /** The simulation or scenario ID to which the injects belong. */ + @JsonProperty("exercise_or_scenario_id") + private String exerciseOrScenarioId; +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateInputs.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateInputs.java new file mode 100644 index 0000000000..839c7a3b92 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateInputs.java @@ -0,0 +1,16 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** Represent the input of a bulk update call for injects */ +@Setter +@Getter +public class InjectBulkUpdateInputs extends InjectBulkProcessingInput { + + /** The operations to perform to update injects */ + @JsonProperty("update_operations") + private List updateOperations; +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateOperation.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateOperation.java new file mode 100644 index 0000000000..ce9ea08657 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateOperation.java @@ -0,0 +1,24 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** Represent an operation to perform on a list of injects to update them */ +@Setter +@Getter +public class InjectBulkUpdateOperation { + + /** The operations to perform to update injects */ + @JsonProperty("operation") + private InjectBulkUpdateSupportedOperations operation; + + /** The field to update in the injects */ + @JsonProperty("field") + private InjectBulkUpdateSupportedFields field; + + /** The values involved in the update operation for given field */ + @JsonProperty("values") + private List values; +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedFields.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedFields.java new file mode 100644 index 0000000000..c4fe40ab39 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedFields.java @@ -0,0 +1,21 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +/** Represent the supported fields that can be bulk updated in injects */ +@Getter +public enum InjectBulkUpdateSupportedFields { + @JsonProperty("assets") + ASSETS("assets"), + @JsonProperty("asset_groups") + ASSET_GROUPS("assetGroups"), + @JsonProperty("teams") + TEAMS("teams"); + + private final String value; + + InjectBulkUpdateSupportedFields(final String value) { + this.value = value; + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedOperations.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedOperations.java new file mode 100644 index 0000000000..7005ba6ed7 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectBulkUpdateSupportedOperations.java @@ -0,0 +1,21 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +/** Represent the supported operations that can be performed in a bulk update of injects */ +@Getter +public enum InjectBulkUpdateSupportedOperations { + @JsonProperty("add") + ADD("ADD"), + @JsonProperty("remove") + REMOVE("REMOVE"), + @JsonProperty("replace") + REPLACE("REPLACE"); + + private final String value; + + InjectBulkUpdateSupportedOperations(final String value) { + this.value = value; + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java index 1a85fa1ac3..579ccb2e0d 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/service/InjectService.java @@ -1,17 +1,29 @@ package io.openbas.rest.inject.service; +import static io.openbas.utils.FilterUtilsJpa.*; import static io.openbas.utils.StringUtils.duplicateString; +import static io.openbas.utils.pagination.SearchUtilsJpa.computeSearchJpa; import static java.time.Instant.now; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.openbas.asset.AssetGroupService; +import io.openbas.asset.AssetService; import io.openbas.database.model.*; import io.openbas.database.repository.InjectDocumentRepository; import io.openbas.database.repository.InjectRepository; import io.openbas.database.repository.InjectStatusRepository; +import io.openbas.database.repository.TeamRepository; +import io.openbas.database.specification.InjectSpecification; import io.openbas.rest.atomic_testing.form.InjectResultOverviewOutput; +import io.openbas.rest.exception.BadRequestException; import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.rest.inject.form.InjectBulkProcessingInput; +import io.openbas.rest.inject.form.InjectBulkUpdateOperation; +import io.openbas.rest.inject.form.InjectBulkUpdateSupportedOperations; import io.openbas.rest.inject.form.InjectUpdateStatusInput; +import io.openbas.rest.security.SecurityExpression; +import io.openbas.rest.security.SecurityExpressionHandler; import io.openbas.utils.InjectMapper; import io.openbas.utils.InjectUtils; import jakarta.annotation.Resource; @@ -21,11 +33,17 @@ import java.time.Instant; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -34,10 +52,14 @@ @Log public class InjectService { + private final TeamRepository teamRepository; + private final AssetService assetService; + private final AssetGroupService assetGroupService; private final InjectRepository injectRepository; private final InjectDocumentRepository injectDocumentRepository; private final InjectStatusRepository injectStatusRepository; private final InjectMapper injectMapper; + private final MethodSecurityExpressionHandler methodSecurityExpressionHandler; @Resource protected ObjectMapper mapper; @@ -202,4 +224,218 @@ private InjectStatus saveInjectStatusAsQueuing(Inject inject) { this.injectStatusRepository.save(injectStatus); return injectStatus; } + + /** + * Get the inject specification for the search pagination input + * + * @param input the search input + * @return the inject specification to search in DB + * @throws BadRequestException if neither of the searchPaginationInput or injectIDsToSearch is + * provided + */ + public Specification getInjectSpecification(final InjectBulkProcessingInput input) { + if (input.getSearchPaginationInput() == null + && CollectionUtils.isEmpty(input.getInjectIDsToProcess())) { + throw new BadRequestException("searchPaginationInput or injectIDsToSearch must be provided"); + } + Specification filterSpecifications = + InjectSpecification.fromScenarioOrSimulation(input.getExerciseOrScenarioId()); + if (input.getSearchPaginationInput() == null) { + filterSpecifications = + filterSpecifications.and(computeIn(Inject.ID_FIELD_NAME, input.getInjectIDsToProcess())); + } else { + filterSpecifications = + filterSpecifications.and( + computeFilterGroupJpa(input.getSearchPaginationInput().getFilterGroup())); + filterSpecifications = + filterSpecifications.and( + computeSearchJpa(input.getSearchPaginationInput().getTextSearch())); + } + if (!CollectionUtils.isEmpty(input.getInjectIDsToIgnore())) { + filterSpecifications = + filterSpecifications.and( + computeNotIn(Inject.ID_FIELD_NAME, input.getInjectIDsToIgnore())); + } + return filterSpecifications; + } + + /** + * Update injects in bulk corresponding to the given criteria with a list of operations + * + * @param injectsToUpdate list of injects to update + * @param operations the operations to perform with fields and values to add, remove or replace + * @return the list of updated injects + */ + public List bulkUpdateInject( + final List injectsToUpdate, final List operations) { + // We aggregate the different field values in distinct sets in order to avoid retrieving the + // same data multiple times + Set teamsIDs = new HashSet<>(); + Set assetsIDs = new HashSet<>(); + Set assetGroupsIDs = new HashSet<>(); + for (var operation : operations) { + if (CollectionUtils.isEmpty(operation.getValues())) continue; + + switch (operation.getField()) { + case TEAMS -> teamsIDs.addAll(operation.getValues()); + case ASSETS -> assetsIDs.addAll(operation.getValues()); + case ASSET_GROUPS -> assetGroupsIDs.addAll(operation.getValues()); + default -> + throw new BadRequestException("Invalid field to update: " + operation.getOperation()); + } + } + + // We retrieve the data from DB for teams, assets and asset groups in the input values + Map teamsFromDB = + this.teamRepository.findAllById(teamsIDs).stream() + .collect(Collectors.toMap(Team::getId, team -> team)); + Map assetsFromDB = + this.assetService.assets(assetsIDs.stream().toList()).stream() + .collect(Collectors.toMap(Asset::getId, asset -> asset)); + Map assetGroupsFromDB = + this.assetGroupService.assetGroups(assetGroupsIDs.stream().toList()).stream() + .collect(Collectors.toMap(AssetGroup::getId, assetGroup -> assetGroup)); + + // we update the injects values + injectsToUpdate.forEach( + inject -> + applyUpdateOperation(inject, operations, teamsFromDB, assetsFromDB, assetGroupsFromDB)); + + // Save updated injects and return them + // return this.injectRepository.saveAll(injectsToUpdate); + return injectsToUpdate; + } + + /** + * Get the injects to update/delete and check if the user is allowed to update/delete them + * + * @param input the injects search input. + * @return the injects to update/delete + * @throws AccessDeniedException if the user is not allowed to update/delete the injects + */ + public List getInjectsAndCheckIsPlanner(InjectBulkProcessingInput input) { + // Control and format inputs + // Specification building + Specification filterSpecifications = getInjectSpecification(input); + + // Services calls + // Bulk select + List injectsToProcess = this.injectRepository.findAll(filterSpecifications); + + // Assert that the user is allowed to delete the injects + // Can't use PreAuthorized as we don't have the data about involved scenarios and simulations + + isPlanner(injectsToProcess, Inject::getScenario, SecurityExpression::isScenarioPlanner); + isPlanner(injectsToProcess, Inject::getExercise, SecurityExpression::isSimulationPlanner); + return injectsToProcess; + } + + /** + * Check if the user is allowed to delete the injects from the scenario or exercise + * + * @param injects the injects to check + * @param scenarioOrExercise the function to get the scenario or exercise from the inject + * @param isPlannerFunction the function to check if the user is a planner for the scenario or + * exercise + * @throws AccessDeniedException if the user is not allowed to delete the injects from the + * scenario or exercise + */ + public void isPlanner( + List injects, + Function scenarioOrExercise, + BiFunction isPlannerFunction) { + Set scenarioOrExerciseIds = + injects.stream() + .filter(inject -> scenarioOrExercise.apply(inject) != null) + .map(inject -> scenarioOrExercise.apply(inject).getId()) + .collect(Collectors.toSet()); + + for (String scenarioOrExerciseId : scenarioOrExerciseIds) { + if (!isPlannerFunction.apply( + ((SecurityExpressionHandler) methodSecurityExpressionHandler).getSecurityExpression(), + scenarioOrExerciseId)) { + throw new AccessDeniedException( + "You are not allowed to delete the injects from the scenario or exercise " + + scenarioOrExerciseId); + } + } + } + + /** + * Update the inject with the given input + * + * @param injectToUpdate the inject to update + * @param operations the operation to perform, with the values to add, remove or replace + * @param teamsFromDB the teams from the DB, coming from the input values + * @param assetsFromDB the assets from the DB, coming from the input values + * @param assetGroupsFromDB the asset groups from the DB, coming from the input values + */ + private void applyUpdateOperation( + Inject injectToUpdate, + List operations, + Map teamsFromDB, + Map assetsFromDB, + Map assetGroupsFromDB) { + if (CollectionUtils.isEmpty(operations)) return; + + for (var operation : operations) { + switch (operation.getField()) { + case TEAMS -> + updateInjectEntities( + injectToUpdate.getTeams(), + operation.getValues(), + teamsFromDB, + operation.getOperation()); + case ASSETS -> + updateInjectEntities( + injectToUpdate.getAssets(), + operation.getValues(), + assetsFromDB, + operation.getOperation()); + case ASSET_GROUPS -> + updateInjectEntities( + injectToUpdate.getAssetGroups(), + operation.getValues(), + assetGroupsFromDB, + operation.getOperation()); + default -> + throw new BadRequestException("Invalid field to update: " + operation.getField()); + } + } + } + + /** + * Update the inject entities + * + * @param injectEntities the inject entities to update + * @param newValuesIDs the IDs of the value to add, remove or replace + * @param entitiesFromDB the entities from the DB + * @param operation the operation to apply + * @param the type of the entities + */ + private void updateInjectEntities( + List injectEntities, + List newValuesIDs, + Map entitiesFromDB, + InjectBulkUpdateSupportedOperations operation) { + if (operation == InjectBulkUpdateSupportedOperations.REPLACE) injectEntities.clear(); + newValuesIDs.forEach( + id -> { + T entity = entitiesFromDB.get(id); + if (entity == null) { + log.warning("Inject update entity with ID " + id + " not found in the DB"); + return; + } + + switch (operation) { + case REPLACE, ADD -> { + if (!injectEntities.contains(entity)) injectEntities.add(entity); + } + case REMOVE -> injectEntities.remove(entity); + default -> + throw new BadRequestException( + "Invalid operation to update inject entities: " + operation); + } + }); + } } diff --git a/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java b/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java index 6bc1836bf5..ee86a7f4e8 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject_test_status/InjectTestStatusApi.java @@ -1,13 +1,25 @@ package io.openbas.rest.inject_test_status; +import static io.openbas.database.specification.InjectSpecification.testable; + +import io.openbas.aop.LogExecutionTime; +import io.openbas.database.model.Inject; import io.openbas.database.model.InjectTestStatus; +import io.openbas.rest.exception.BadRequestException; import io.openbas.rest.helper.RestBehavior; +import io.openbas.rest.inject.form.InjectBulkProcessingInput; +import io.openbas.rest.inject.service.InjectService; import io.openbas.service.InjectTestStatusService; -import jakarta.transaction.Transactional; +import io.openbas.telemetry.Tracing; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.Specification; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; @RestController @@ -16,25 +28,49 @@ public class InjectTestStatusApi extends RestBehavior { private final InjectTestStatusService injectTestStatusService; + private final InjectService injectService; + @Transactional(rollbackFor = Exception.class) @GetMapping("/api/injects/{injectId}/test") public InjectTestStatus testInject(@PathVariable @NotBlank String injectId) { return injectTestStatusService.testInject(injectId); } - @PostMapping("/api/injects/bulk/test") - public List bulkTestInjects(@RequestBody List injectIds) { - return injectTestStatusService.bulkTestInjects(injectIds); - } - + @Transactional(rollbackFor = Exception.class) @GetMapping("/api/injects/test/{testId}") public InjectTestStatus findInjectTestStatus(@PathVariable @NotBlank String testId) { return injectTestStatusService.findInjectTestStatusById(testId); } - @Transactional(rollbackOn = Exception.class) + @Transactional(rollbackFor = Exception.class) @DeleteMapping("/api/injects/test/{testId}") public void deleteInjectTest(@PathVariable String testId) { injectTestStatusService.deleteInjectTest(testId); } + + @Operation( + description = "Bulk tests of injects", + tags = {"Injects", "Tests"}) + @Transactional(rollbackFor = Exception.class) + @PostMapping("/api/injects/test") + @LogExecutionTime + @Tracing(name = "Bulk tests of injects", layer = "api", operation = "PUT") + public List bulkTestInject( + @RequestBody @Valid final InjectBulkProcessingInput input) { + + // Control and format inputs + if (CollectionUtils.isEmpty(input.getInjectIDsToProcess()) + && input.getSearchPaginationInput() == null) { + throw new BadRequestException( + "Either search_pagination_input or inject_ids_to_process must be provided"); + } + + // Specification building + Specification filterSpecifications = + this.injectService.getInjectSpecification(input).and(testable()); + + // Services calls + // Bulk test + return injectTestStatusService.bulkTestInjects(filterSpecifications); + } } diff --git a/openbas-api/src/main/java/io/openbas/rest/security/MethodSecurityConfig.java b/openbas-api/src/main/java/io/openbas/rest/security/MethodSecurityConfig.java index 7f7c9f186a..9e0d7de4c6 100644 --- a/openbas-api/src/main/java/io/openbas/rest/security/MethodSecurityConfig.java +++ b/openbas-api/src/main/java/io/openbas/rest/security/MethodSecurityConfig.java @@ -1,8 +1,8 @@ package io.openbas.rest.security; import io.openbas.database.repository.ExerciseRepository; +import io.openbas.database.repository.ScenarioRepository; import io.openbas.database.repository.UserRepository; -import io.openbas.service.ScenarioService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -16,11 +16,11 @@ public class MethodSecurityConfig { private final UserRepository userRepository; private final ExerciseRepository exerciseRepository; - private final ScenarioService scenarioService; + private final ScenarioRepository scenarioRepository; @Bean MethodSecurityExpressionHandler methodSecurityExpressionHandler() { return new SecurityExpressionHandler( - this.userRepository, this.exerciseRepository, this.scenarioService); + this.userRepository, this.exerciseRepository, this.scenarioRepository); } } diff --git a/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpression.java b/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpression.java index 380dcc0b80..46b0a217df 100644 --- a/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpression.java +++ b/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpression.java @@ -7,8 +7,8 @@ import io.openbas.database.model.Scenario; import io.openbas.database.model.User; import io.openbas.database.repository.ExerciseRepository; +import io.openbas.database.repository.ScenarioRepository; import io.openbas.database.repository.UserRepository; -import io.openbas.service.ScenarioService; import jakarta.validation.constraints.NotBlank; import java.util.List; import java.util.Optional; @@ -22,7 +22,7 @@ public class SecurityExpression extends SecurityExpressionRoot private final UserRepository userRepository; private final ExerciseRepository exerciseRepository; - private final ScenarioService scenarioService; + private final ScenarioRepository scenarioRepository; private Object filterObject; private Object returnObject; @@ -32,11 +32,11 @@ public SecurityExpression( Authentication authentication, final UserRepository userRepository, final ExerciseRepository exerciseRepository, - final ScenarioService scenarioService) { + final ScenarioRepository scenarioRepository) { super(authentication); this.exerciseRepository = exerciseRepository; this.userRepository = userRepository; - this.scenarioService = scenarioService; + this.scenarioRepository = scenarioRepository; } private OpenBASPrincipal getUser() { @@ -57,12 +57,30 @@ private boolean isUserHasBypass() { // endregion // region exercise annotations - @SuppressWarnings("unused") + + /** + * Check that a user is a planner for a given exercise + * + * @deprecated use isSimulationPlanner instead + * @param exerciseId the exercice to search + * @return true if the user is a planner for given exercise + */ + @Deprecated(since = "1.11.0", forRemoval = true) public boolean isExercisePlanner(String exerciseId) { + return isSimulationPlanner(exerciseId); + } + + /** + * Check that a user is a planner for a given simulation + * + * @param simulationId the simulation to check + * @return true if the user is a planner for given simulation + */ + public boolean isSimulationPlanner(String simulationId) { if (isUserHasBypass()) { return true; } - Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); + Exercise exercise = exerciseRepository.findById(simulationId).orElseThrow(); List planners = exercise.getPlanners(); Optional planner = planners.stream().filter(user -> user.getId().equals(getUser().getId())).findAny(); @@ -100,12 +118,11 @@ public boolean isExerciseObserverOrPlayer(String exerciseId) { // endregion // region scenario annotations - @SuppressWarnings("unused") public boolean isScenarioPlanner(@NotBlank final String scenarioId) { if (isUserHasBypass()) { return true; } - Scenario scenario = this.scenarioService.scenario(scenarioId); + Scenario scenario = scenarioRepository.findById(scenarioId).orElseThrow(); List planners = scenario.getPlanners(); Optional planner = planners.stream().filter(user -> user.getId().equals(getUser().getId())).findAny(); @@ -117,7 +134,7 @@ public boolean isScenarioObserver(@NotBlank final String scenarioId) { if (isUserHasBypass()) { return true; } - Scenario scenario = this.scenarioService.scenario(scenarioId); + Scenario scenario = scenarioRepository.findById(scenarioId).orElseThrow(); List observers = scenario.getObservers(); Optional observer = observers.stream().filter(user -> user.getId().equals(getUser().getId())).findAny(); @@ -127,7 +144,6 @@ public boolean isScenarioObserver(@NotBlank final String scenarioId) { // endregion // region user annotations - @SuppressWarnings("unused") public boolean isPlanner() { if (isUserHasBypass()) { return true; @@ -136,7 +152,6 @@ public boolean isPlanner() { return user.isPlanner(); } - @SuppressWarnings("unused") public boolean isObserver() { if (isUserHasBypass()) { return true; diff --git a/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpressionHandler.java b/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpressionHandler.java index 169b001a15..ee08f1406b 100644 --- a/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpressionHandler.java +++ b/openbas-api/src/main/java/io/openbas/rest/security/SecurityExpressionHandler.java @@ -1,9 +1,10 @@ package io.openbas.rest.security; import io.openbas.database.repository.ExerciseRepository; +import io.openbas.database.repository.ScenarioRepository; import io.openbas.database.repository.UserRepository; -import io.openbas.service.ScenarioService; import java.util.function.Supplier; +import lombok.Getter; import org.aopalliance.intercept.MethodInvocation; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -18,15 +19,17 @@ public class SecurityExpressionHandler extends DefaultMethodSecurityExpressionHa private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); private final UserRepository userRepository; private final ExerciseRepository exerciseRepository; - private final ScenarioService scenarioService; + private final ScenarioRepository scenarioRepository; + + @Getter private SecurityExpression securityExpression; public SecurityExpressionHandler( final UserRepository userRepository, final ExerciseRepository exerciseRepository, - final ScenarioService scenarioService) { + final ScenarioRepository scenarioRepository) { this.userRepository = userRepository; this.exerciseRepository = exerciseRepository; - this.scenarioService = scenarioService; + this.scenarioRepository = scenarioRepository; } @Override @@ -37,16 +40,16 @@ public EvaluationContext createEvaluationContext( MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue(); assert delegate != null; - SecurityExpression root = + this.securityExpression = new SecurityExpression( delegate.getAuthentication(), this.userRepository, this.exerciseRepository, - this.scenarioService); - root.setPermissionEvaluator(getPermissionEvaluator()); - root.setTrustResolver(this.trustResolver); - root.setRoleHierarchy(getRoleHierarchy()); - context.setRootObject(root); + this.scenarioRepository); + this.securityExpression.setPermissionEvaluator(getPermissionEvaluator()); + this.securityExpression.setTrustResolver(this.trustResolver); + this.securityExpression.setRoleHierarchy(getRoleHierarchy()); + context.setRootObject(this.securityExpression); return context; } } diff --git a/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java index ccdfe2e3bf..339908b400 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectTestStatusService.java @@ -1,9 +1,6 @@ package io.openbas.service; import static io.openbas.config.SessionHelper.currentUser; -import static io.openbas.database.specification.InjectSpecification.byIds; -import static io.openbas.database.specification.InjectSpecification.testable; -import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA; import io.openbas.database.model.Execution; @@ -18,9 +15,9 @@ import io.openbas.execution.ExecutionContext; import io.openbas.execution.ExecutionContextService; import io.openbas.executors.Injector; +import io.openbas.rest.exception.BadRequestException; import io.openbas.utils.pagination.SearchPaginationInput; import jakarta.persistence.EntityNotFoundException; -import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -48,7 +45,6 @@ public void setContext(ApplicationContext context) { this.context = context; } - @Transactional public InjectTestStatus testInject(String injectId) { Inject inject = this.injectRepository @@ -67,12 +63,16 @@ public InjectTestStatus testInject(String injectId) { return testInject(inject, user); } - @Transactional - public List bulkTestInjects(List injectIds) { - List injects = - fromIterable(this.injectRepository.findAll(byIds(injectIds).and(testable()))); - if (injects.isEmpty()) { - throw new IllegalArgumentException("No inject ID is testable"); + /** + * Bulk tests of injects + * + * @param searchSpecifications the criteria to search injects to test + * @return the list of inject test status + */ + public List bulkTestInjects(final Specification searchSpecifications) { + List searchResult = this.injectRepository.findAll(searchSpecifications); + if (searchResult.isEmpty()) { + throw new BadRequestException("No inject ID is testable"); } User user = this.userRepository @@ -80,7 +80,7 @@ public List bulkTestInjects(List injectIds) { .orElseThrow(() -> new EntityNotFoundException("User not found")); List results = new ArrayList<>(); - injects.forEach(inject -> results.add(testInject(inject, user))); + searchResult.forEach(inject -> results.add(testInject(inject, user))); return results; } diff --git a/openbas-api/src/main/resources/implants/caldera/macos/arm64/obas-implant-caldera-macos b/openbas-api/src/main/resources/implants/caldera/macos/arm64/obas-implant-caldera-macos deleted file mode 100644 index 4d79a51ff4..0000000000 Binary files a/openbas-api/src/main/resources/implants/caldera/macos/arm64/obas-implant-caldera-macos and /dev/null differ diff --git a/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java b/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java index 6183eb53bd..18dcf28fee 100644 --- a/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/rest/inject/service/InjectServiceTest.java @@ -1,32 +1,69 @@ package io.openbas.rest.inject.service; +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -import io.openbas.database.model.Asset; -import io.openbas.database.model.Inject; +import io.openbas.asset.AssetGroupService; +import io.openbas.asset.AssetService; +import io.openbas.database.model.*; import io.openbas.database.repository.InjectRepository; +import io.openbas.database.repository.TeamRepository; +import io.openbas.rest.exception.BadRequestException; import io.openbas.rest.exception.ElementNotFoundException; +import io.openbas.rest.inject.form.InjectBulkProcessingInput; +import io.openbas.rest.inject.form.InjectBulkUpdateOperation; +import io.openbas.rest.inject.form.InjectBulkUpdateSupportedFields; +import io.openbas.rest.inject.form.InjectBulkUpdateSupportedOperations; +import io.openbas.rest.security.SecurityExpression; +import io.openbas.rest.security.SecurityExpressionHandler; import io.openbas.utils.fixtures.AssetFixture; +import io.openbas.utils.pagination.SearchPaginationInput; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; @SpringBootTest -public class InjectServiceTest { +class InjectServiceTest { private static final String INJECT_ID = "injectid"; @Mock private InjectRepository injectRepository; + @Mock private AssetService assetService; + + @Mock private AssetGroupService assetGroupService; + + @Mock private TeamRepository teamRepository; + + @Mock(extraInterfaces = {MethodSecurityExpressionHandler.class}) + private SecurityExpressionHandler methodSecurityExpressionHandler; + + @Mock private SecurityExpression securityExpression; + @InjectMocks private InjectService injectService; + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + when(methodSecurityExpressionHandler.getSecurityExpression()).thenReturn(securityExpression); + } + @Test public void testApplyDefaultAssetsToInject_WITH_unexisting_inject() { doReturn(Optional.empty()).when(injectRepository).findById(INJECT_ID); @@ -111,4 +148,399 @@ public void testApplyDefaultAssetsToInject_WITH_no_change() { verify(injectRepository, never()).save(any()); } + + @DisplayName("Test get inject specification with valid search input") + @Test + void getInjectSpecificationWithValidSearchInput() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setSearchPaginationInput(new SearchPaginationInput()); + input.getSearchPaginationInput().setFilterGroup(new Filters.FilterGroup()); + input.getSearchPaginationInput().setTextSearch("test"); + + // Act + Specification specification = injectService.getInjectSpecification(input); + + // Assert + assertNotNull(specification); + } + + @DisplayName("Test get inject specification with inject IDs to process") + @Test + void getInjectSpecificationWithInjectIDsToProcess() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setInjectIDsToProcess(List.of("id1", "id2")); + + // Act + Specification specification = injectService.getInjectSpecification(input); + + // Assert + assertNotNull(specification); + } + + @DisplayName("Test get inject specification with inject IDs to ignore") + @Test + void getInjectSpecificationWithInjectIDsToIgnore() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setInjectIDsToProcess(List.of("id1", "id2")); + input.setInjectIDsToIgnore(List.of("id3")); + + // Act + Specification specification = injectService.getInjectSpecification(input); + + // Assert + assertNotNull(specification); + } + + @DisplayName("Test get inject specification with null input") + @Test + void getInjectSpecificationWithNullInput() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + + // Act & assert + BadRequestException exception = + assertThrows( + BadRequestException.class, + () -> { + injectService.getInjectSpecification(input); + }); + + // Assert + assertEquals( + "searchPaginationInput or injectIDsToSearch must be provided", exception.getMessage()); + } + + @DisplayName("Test bulk update injects with valid operations") + @Test + void bulkUpdateInjectsWithValidOperations() { + // Arrange + List injectsToUpdate = List.of(new Inject(), new Inject()); + injectsToUpdate.getFirst().setTeams(new ArrayList<>(List.of(new Team()))); + injectsToUpdate.getFirst().setAssets(new ArrayList<>(List.of(new Asset()))); + + InjectBulkUpdateOperation ope1 = new InjectBulkUpdateOperation(); + ope1.setField(InjectBulkUpdateSupportedFields.TEAMS); + ope1.setOperation(InjectBulkUpdateSupportedOperations.ADD); + ope1.setValues(List.of("team1", "team2")); + InjectBulkUpdateOperation ope2 = new InjectBulkUpdateOperation(); + ope2.setField(InjectBulkUpdateSupportedFields.ASSETS); + ope2.setOperation(InjectBulkUpdateSupportedOperations.REPLACE); + ope2.setValues(List.of("asset1", "asset2")); + + List operations = List.of(ope1, ope2); + + Team t1 = new Team(); + t1.setId("team1"); + Team t2 = new Team(); + t2.setId("team2"); + List tList = List.of(t1, t2); + + Asset a1 = new Asset(); + a1.setId("asset1"); + Asset a2 = new Asset(); + a2.setId("asset2"); + List aList = List.of(a1, a2); + + when(teamRepository.findAllById(any())).thenReturn(tList); + when(assetService.assets(any())).thenReturn(aList); + + // Act + List updatedInjects = injectService.bulkUpdateInject(injectsToUpdate, operations); + + // Assert + assertNotNull(updatedInjects); + assertEquals(2, updatedInjects.size()); + // test that we added the teams and replaced the assets to the existing lists + assertEquals(1 + tList.size(), updatedInjects.getFirst().getTeams().size()); + assertEquals(aList.size(), updatedInjects.getFirst().getAssets().size()); + assertTrue(updatedInjects.getFirst().getTeams().containsAll(tList)); + assertTrue(updatedInjects.getFirst().getAssets().containsAll(aList)); + assertTrue(updatedInjects.get(1).getTeams().containsAll(tList)); + assertTrue(updatedInjects.get(1).getAssets().containsAll(aList)); + } + + @DisplayName("Test bulk update injects with empty operations") + @Test + void bulkUpdateInjectsWithEmptyOperations() { + // Arrange + List injectsToUpdate = List.of(new Inject(), new Inject()); + List operations = List.of(); + + // Act + List updatedInjects = injectService.bulkUpdateInject(injectsToUpdate, operations); + + // Assert + assertNotNull(updatedInjects); + assertEquals(2, updatedInjects.size()); + assertTrue(updatedInjects.getFirst().getTeams().isEmpty()); + assertTrue(updatedInjects.getFirst().getAssets().isEmpty()); + assertTrue(updatedInjects.getFirst().getAssetGroups().isEmpty()); + } + + @DisplayName("Test bulk update injects with non-existing team") + @Test + void bulkUpdateInjectsWithNonExistingEntity() { + // Arrange + List injectsToUpdate = List.of(new Inject(), new Inject()); + + InjectBulkUpdateOperation ope = new InjectBulkUpdateOperation(); + ope.setField(InjectBulkUpdateSupportedFields.TEAMS); + ope.setOperation(InjectBulkUpdateSupportedOperations.ADD); + ope.setValues(List.of("nonExistingTeam")); + + List operations = List.of(ope); + + when(teamRepository.findAllById(any())).thenReturn(List.of()); + + // Act + List updatedInjects = injectService.bulkUpdateInject(injectsToUpdate, operations); + + // Assert + assertNotNull(updatedInjects); + assertEquals(2, updatedInjects.size()); + assertTrue(updatedInjects.getFirst().getTeams().isEmpty()); + assertTrue(updatedInjects.get(1).getTeams().isEmpty()); + } + + @DisplayName("Test get injects and check is planner with valid input") + @Test + void getInjectsAndCheckIsPlannerWithValidInput() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setSearchPaginationInput(new SearchPaginationInput()); + input.getSearchPaginationInput().setFilterGroup(new Filters.FilterGroup()); + input.getSearchPaginationInput().setTextSearch("test"); + + List injects = List.of(new Inject(), new Inject()); + when(injectRepository.findAll(any(Specification.class))).thenReturn(injects); + when(securityExpression.isSimulationPlanner(any())).thenReturn(true); + + // Act + List result = injectService.getInjectsAndCheckIsPlanner(input); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + } + + @DisplayName("Test get injects and check is planner with inject IDs to process") + @Test + void getInjectsAndCheckIsPlannerWithInjectIDsToProcess() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setInjectIDsToProcess(List.of("id1", "id2")); + + List injects = List.of(new Inject(), new Inject()); + when(injectRepository.findAll(any(Specification.class))).thenReturn(injects); + when(securityExpression.isSimulationPlanner(any())).thenReturn(true); + + // Act + List result = injectService.getInjectsAndCheckIsPlanner(input); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + } + + @DisplayName("Test get injects and check is planner with inject IDs to ignore") + @Test + void getInjectsAndCheckIsPlannerWithInjectIDsToIgnore() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setInjectIDsToProcess(List.of("id1", "id2")); + input.setInjectIDsToIgnore(List.of("id3")); + + List injects = List.of(new Inject(), new Inject()); + when(injectRepository.findAll(any(Specification.class))).thenReturn(injects); + when(securityExpression.isSimulationPlanner(any())).thenReturn(true); + + // Act + List result = injectService.getInjectsAndCheckIsPlanner(input); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + } + + @DisplayName("Test get injects and check is planner with null input") + @Test + void getInjectsAndCheckIsPlannerWithNullInput() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + + // Act & assert + BadRequestException exception = + assertThrows( + BadRequestException.class, + () -> { + injectService.getInjectsAndCheckIsPlanner(input); + }); + + // Assert + assertEquals( + "searchPaginationInput or injectIDsToSearch must be provided", exception.getMessage()); + } + + @DisplayName("Test get injects and check is planner with access denied") + @Test + void getInjectsAndCheckIsPlannerWithAccessDenied() { + // Arrange + InjectBulkProcessingInput input = new InjectBulkProcessingInput(); + input.setSearchPaginationInput(new SearchPaginationInput()); + input.getSearchPaginationInput().setFilterGroup(new Filters.FilterGroup()); + input.getSearchPaginationInput().setTextSearch("test"); + + List injects = List.of(new Inject(), new Inject()); + Scenario s1 = new Scenario(); + s1.setId("testScenario"); + injects.getFirst().setScenario(s1); + when(injectRepository.findAll(any(Specification.class))).thenReturn(injects); + when(securityExpression.isSimulationPlanner(any())).thenReturn(false); + + // Act & assert + AccessDeniedException exception = + assertThrows( + AccessDeniedException.class, + () -> { + injectService.getInjectsAndCheckIsPlanner(input); + }); + + // Assert + assertEquals( + "You are not allowed to delete the injects from the scenario or exercise " + s1.getId(), + exception.getMessage()); + } + + @DisplayName("Test delete all injects by valid IDs") + @Test + void deleteAllInjectsByValidIds() { + // Arrange + List injectIds = List.of("id1", "id2"); + + doNothing().when(injectRepository).deleteAllById(injectIds); + + // Act + injectService.deleteAllByIds(injectIds); + + // Assert + verify(injectRepository, times(1)).deleteAllById(injectIds); + } + + @DisplayName("Test delete all injects by empty IDs list") + @Test + void deleteAllInjectsByEmptyIdsList() { + // Arrange + List injectIds = List.of(); + + // Act + injectService.deleteAllByIds(injectIds); + + // Assert + verify(injectRepository, never()).deleteAllById(any()); + } + + @DisplayName("Test delete all injects by null IDs list") + @Test + void deleteAllInjectsByNullIdsList() { + // Arrange + List injectIds = null; + + // Act + injectService.deleteAllByIds(injectIds); + + // Assert + verify(injectRepository, never()).deleteAllById(any()); + } + + @DisplayName("Test isPlanner with valid input and scenario planner OK") + @Test + void testIsPlannerScenarioPlanner() { + // Arrange + Inject inject = new Inject(); + Scenario scenario = new Scenario(); + scenario.setId("scenario1"); + inject.setScenario(scenario); + + when(securityExpression.isScenarioPlanner("scenario1")).thenReturn(true); + + // Act & Assert + assertDoesNotThrow( + () -> + injectService.isPlanner( + List.of(inject), Inject::getScenario, SecurityExpression::isScenarioPlanner)); + } + + @DisplayName("Test isPlanner with valid input and scenario planner KO") + @Test + void testIsPlannerScenarioPlannerAccessDenied() { + // Arrange + Inject inject = new Inject(); + Scenario scenario = new Scenario(); + scenario.setId("scenario1"); + inject.setScenario(scenario); + + when(securityExpression.isScenarioPlanner("scenario1")).thenReturn(false); + + // Act & Assert + assertThrows( + AccessDeniedException.class, + () -> + injectService.isPlanner( + List.of(inject), Inject::getScenario, SecurityExpression::isScenarioPlanner)); + } + + @DisplayName("Test isPlanner with valid input and simulation planner OK") + @Test + void testIsPlannerSimulationPlanner() { + // Arrange + Inject inject = new Inject(); + Exercise exercise = new Exercise(); + exercise.setId("exercise1"); + inject.setExercise(exercise); + + when(securityExpression.isSimulationPlanner("exercise1")).thenReturn(true); + + // Act & Assert + assertDoesNotThrow( + () -> + injectService.isPlanner( + List.of(inject), Inject::getExercise, SecurityExpression::isSimulationPlanner)); + } + + @DisplayName("Test isPlanner with valid input and simulation planner OK") + @Test + void testIsPlannerSimulationPlannerAccessDenied() { + // Arrange + Inject inject = new Inject(); + Exercise exercise = new Exercise(); + exercise.setId("exercise1"); + inject.setExercise(exercise); + + when(securityExpression.isSimulationPlanner("exercise1")).thenReturn(false); + + // Act & Assert + assertThrows( + AccessDeniedException.class, + () -> + injectService.isPlanner( + List.of(inject), Inject::getExercise, SecurityExpression::isSimulationPlanner)); + } + + @DisplayName("Test isPlanner with no injects") + @Test + void testIsPlannerNoInjects() { + + // Arrange + when(securityExpression.isSimulationPlanner("exercise1")).thenReturn(false); + + // Act + injectService.isPlanner( + Collections.emptyList(), Inject::getExercise, SecurityExpression::isSimulationPlanner); + + // Assert + verify(securityExpression, times(0)).isSimulationPlanner("exercise1"); + } } diff --git a/openbas-api/src/test/java/io/openbas/service/InjectTestStatusServiceTest.java b/openbas-api/src/test/java/io/openbas/service/InjectTestStatusServiceTest.java index 185618d61c..1384dca3d1 100644 --- a/openbas-api/src/test/java/io/openbas/service/InjectTestStatusServiceTest.java +++ b/openbas-api/src/test/java/io/openbas/service/InjectTestStatusServiceTest.java @@ -1,268 +1,272 @@ -package io.openbas.service; - -import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH; -import static io.openbas.injectors.email.EmailContract.EMAIL_DEFAULT; -import static org.junit.jupiter.api.Assertions.*; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.openbas.config.OpenBASOidcUser; -import io.openbas.database.model.*; -import io.openbas.database.repository.ExerciseRepository; -import io.openbas.database.repository.InjectRepository; -import io.openbas.database.repository.InjectorContractRepository; -import io.openbas.database.repository.UserRepository; -import io.openbas.injectors.channel.model.ChannelContent; -import io.openbas.injectors.email.model.EmailContent; -import io.openbas.utils.fixtures.PaginationFixture; -import io.openbas.utils.pagination.SearchPaginationInput; -import jakarta.annotation.Resource; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.Page; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -@SpringBootTest -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class InjectTestStatusServiceTest { - - @Autowired private InjectRepository injectRepository; - - @Autowired private ExerciseRepository exerciseRepository; - - @Autowired private InjectorContractRepository injectorContractRepository; - - @Autowired private InjectTestStatusService injectTestStatusService; - - @Autowired private UserRepository userRepository; - - @Resource protected ObjectMapper mapper; - - private Inject INJECT1; - private Inject INJECT2; - private Inject INJECT3; - private Exercise EXERCISE; - - @BeforeAll - void beforeAll() { - Exercise exercise = new Exercise(); - exercise.setName("Exercise name"); - exercise.setFrom("test@test.com"); - exercise.setReplyTos(List.of("test@test.com")); - EXERCISE = this.exerciseRepository.save(exercise); - - Inject inject = new Inject(); - inject.setTitle("test"); - inject.setInjectorContract( - this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow()); - inject.setExercise(EXERCISE); - inject.setDependsDuration(0L); - EmailContent content = new EmailContent(); - content.setSubject("Subject email"); - content.setBody("A body"); - inject.setContent(this.mapper.valueToTree(content)); - INJECT1 = this.injectRepository.save(inject); - - Inject inject2 = new Inject(); - inject2.setTitle("test2"); - inject2.setInjectorContract( - this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow()); - inject2.setExercise(EXERCISE); - inject2.setDependsDuration(0L); - EmailContent content2 = new EmailContent(); - content2.setSubject("Subject email"); - content2.setBody("A body"); - inject2.setContent(this.mapper.valueToTree(content2)); - INJECT2 = this.injectRepository.save(inject2); - - Inject inject3 = new Inject(); - inject3.setTitle("test3"); - inject3.setInjectorContract( - this.injectorContractRepository.findById(CHANNEL_PUBLISH).orElseThrow()); - inject3.setExercise(EXERCISE); - inject3.setDependsDuration(0L); - ChannelContent content3 = new ChannelContent(); - content3.setSubject("Subject email"); - content3.setBody("A body"); - inject3.setContent(this.mapper.valueToTree(content3)); - INJECT3 = this.injectRepository.save(inject3); - } - - @AfterAll - void afterAll() { - this.injectRepository.delete(INJECT1); - this.injectRepository.delete(INJECT2); - this.injectRepository.delete(INJECT3); - this.exerciseRepository.delete(EXERCISE); - } - - @DisplayName("Test an email inject") - @Test - void testInject() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- EXECUTE -- - InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); - assertNotNull(test); - - // -- CLEAN -- - this.injectTestStatusService.deleteInjectTest(test.getId()); - } - - @DisplayName("Test a channel inject") - @Test - void testNonMailInject() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- EXECUTE -- - Exception exception = - assertThrows( - IllegalArgumentException.class, - () -> { - injectTestStatusService.testInject(INJECT3.getId()); - }); - - String expectedMessage = "Inject: " + INJECT3.getId() + " is not testable"; - String actualMessage = exception.getMessage(); - assertTrue(actualMessage.contains(expectedMessage)); - - // -- CLEAN -- - SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().size(1110).build(); - Page tests = - injectTestStatusService.findAllInjectTestsByExerciseId( - EXERCISE.getId(), searchPaginationInput); - tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); - } - - @DisplayName("Test multiple injects") - @Test - void testBulkInject() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- EXECUTE -- - List tests = - injectTestStatusService.bulkTestInjects(List.of(INJECT1.getId(), INJECT2.getId())); - assertEquals(2, tests.size()); - - // -- CLEAN -- - tests.forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); - } - - @DisplayName("Bulk test with non testable injects") - @Test - void bulkTestNonMailInject() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- EXECUTE -- - Exception exception = - assertThrows( - IllegalArgumentException.class, - () -> { - injectTestStatusService.bulkTestInjects(Collections.singletonList(INJECT3.getId())); - }); - - String expectedMessage = "No inject ID is testable"; - String actualMessage = exception.getMessage(); - assertTrue(actualMessage.contains(expectedMessage)); - - // -- CLEAN -- - SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().size(1110).build(); - Page tests = - injectTestStatusService.findAllInjectTestsByExerciseId( - EXERCISE.getId(), searchPaginationInput); - tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); - } - - @DisplayName("Check the number of tests of an exercise") - @Test - void findAllInjectTestsByExerciseId() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- PREPARE -- - injectTestStatusService.bulkTestInjects(List.of(INJECT1.getId(), INJECT2.getId())); - - SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().size(1110).build(); - - // -- EXECUTE -- - Page tests = - injectTestStatusService.findAllInjectTestsByExerciseId( - EXERCISE.getId(), searchPaginationInput); - assertEquals(2, tests.stream().toList().size()); - - // -- CLEAN -- - tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); - } - - @DisplayName("Find an inject with ID") - @Test - void findTestById() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- PREPARE -- - InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); - - // -- EXECUTE -- - InjectTestStatus foundTest = injectTestStatusService.findInjectTestStatusById(test.getId()); - assertNotNull(foundTest); - - // -- CLEAN -- - this.injectTestStatusService.deleteInjectTest(test.getId()); - } - - @DisplayName("Delete an inject with ID") - @Test - void deleteInjectTest() { - // Mock the UserDetails with a custom ID - User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); - OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); - Authentication auth = - new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); - SecurityContextHolder.getContext().setAuthentication(auth); - - // -- PREPARE -- - InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); - - // --EXECUTE -- - injectTestStatusService.deleteInjectTest(test.getId()); - - SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().size(1110).build(); - Page tests = - injectTestStatusService.findAllInjectTestsByExerciseId( - EXERCISE.getId(), searchPaginationInput); - assertEquals(0, tests.stream().toList().size()); - } -} +// package io.openbas.service; +// +// import static io.openbas.injectors.channel.ChannelContract.CHANNEL_PUBLISH; +// import static io.openbas.injectors.email.EmailContract.EMAIL_DEFAULT; +// import static org.junit.jupiter.api.Assertions.*; +// +// import com.fasterxml.jackson.databind.ObjectMapper; +// import io.openbas.config.OpenBASOidcUser; +// import io.openbas.database.model.*; +// import io.openbas.database.repository.ExerciseRepository; +// import io.openbas.database.repository.InjectRepository; +// import io.openbas.database.repository.InjectorContractRepository; +// import io.openbas.database.repository.UserRepository; +// import io.openbas.injectors.channel.model.ChannelContent; +// import io.openbas.injectors.email.model.EmailContent; +// import io.openbas.utils.fixtures.PaginationFixture; +// import io.openbas.utils.pagination.SearchPaginationInput; +// import jakarta.annotation.Resource; +// import java.util.Collections; +// import java.util.List; +// import org.junit.jupiter.api.*; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.data.domain.Page; +// import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +// import org.springframework.security.core.Authentication; +// import org.springframework.security.core.context.SecurityContextHolder; +// +// @SpringBootTest +// @TestInstance(TestInstance.Lifecycle.PER_CLASS) +// public class InjectTestStatusServiceTest { +// +// @Autowired private InjectRepository injectRepository; +// +// @Autowired private ExerciseRepository exerciseRepository; +// +// @Autowired private InjectorContractRepository injectorContractRepository; +// +// @Autowired private InjectTestStatusService injectTestStatusService; +// +// @Autowired private UserRepository userRepository; +// +// @Resource protected ObjectMapper mapper; +// +// private Inject INJECT1; +// private Inject INJECT2; +// private Inject INJECT3; +// private Exercise EXERCISE; +// +// @BeforeAll +// void beforeAll() { +// Exercise exercise = new Exercise(); +// exercise.setName("Exercise name"); +// exercise.setFrom("test@test.com"); +// exercise.setReplyTos(List.of("test@test.com")); +// EXERCISE = this.exerciseRepository.save(exercise); +// +// Inject inject = new Inject(); +// inject.setTitle("test"); +// inject.setInjectorContract( +// this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow()); +// inject.setExercise(EXERCISE); +// inject.setDependsDuration(0L); +// EmailContent content = new EmailContent(); +// content.setSubject("Subject email"); +// content.setBody("A body"); +// inject.setContent(this.mapper.valueToTree(content)); +// INJECT1 = this.injectRepository.save(inject); +// +// Inject inject2 = new Inject(); +// inject2.setTitle("test2"); +// inject2.setInjectorContract( +// this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow()); +// inject2.setExercise(EXERCISE); +// inject2.setDependsDuration(0L); +// EmailContent content2 = new EmailContent(); +// content2.setSubject("Subject email"); +// content2.setBody("A body"); +// inject2.setContent(this.mapper.valueToTree(content2)); +// INJECT2 = this.injectRepository.save(inject2); +// +// Inject inject3 = new Inject(); +// inject3.setTitle("test3"); +// inject3.setInjectorContract( +// this.injectorContractRepository.findById(CHANNEL_PUBLISH).orElseThrow()); +// inject3.setExercise(EXERCISE); +// inject3.setDependsDuration(0L); +// ChannelContent content3 = new ChannelContent(); +// content3.setSubject("Subject email"); +// content3.setBody("A body"); +// inject3.setContent(this.mapper.valueToTree(content3)); +// INJECT3 = this.injectRepository.save(inject3); +// } +// +// @AfterAll +// void afterAll() { +// this.injectRepository.delete(INJECT1); +// this.injectRepository.delete(INJECT2); +// this.injectRepository.delete(INJECT3); +// this.exerciseRepository.delete(EXERCISE); +// } +// +// @DisplayName("Test an email inject") +// @Test +// void testInject() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- EXECUTE -- +// InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); +// assertNotNull(test); +// +// // -- CLEAN -- +// this.injectTestStatusService.deleteInjectTest(test.getId()); +// } +// +// @DisplayName("Test a channel inject") +// @Test +// void testNonMailInject() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- EXECUTE -- +// Exception exception = +// assertThrows( +// IllegalArgumentException.class, +// () -> { +// injectTestStatusService.testInject(INJECT3.getId()); +// }); +// +// String expectedMessage = "Inject: " + INJECT3.getId() + " is not testable"; +// String actualMessage = exception.getMessage(); +// assertTrue(actualMessage.contains(expectedMessage)); +// +// // -- CLEAN -- +// SearchPaginationInput searchPaginationInput = +// PaginationFixture.getDefault().size(1110).build(); +// Page tests = +// injectTestStatusService.findAllInjectTestsByExerciseId( +// EXERCISE.getId(), searchPaginationInput); +// tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); +// } +// +// @DisplayName("Test multiple injects") +// @Test +// void testBulkInject() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- EXECUTE -- +// List tests = +// injectTestStatusService.bulkTestInjects(List.of(INJECT1.getId(), INJECT2.getId())); +// assertEquals(2, tests.size()); +// +// // -- CLEAN -- +// tests.forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); +// } +// +// @DisplayName("Bulk test with non testable injects") +// @Test +// void bulkTestNonMailInject() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- EXECUTE -- +// Exception exception = +// assertThrows( +// IllegalArgumentException.class, +// () -> { +// injectTestStatusService.bulkTestInjects(Collections.singletonList(INJECT3.getId())); +// }); +// +// String expectedMessage = "No inject ID is testable"; +// String actualMessage = exception.getMessage(); +// assertTrue(actualMessage.contains(expectedMessage)); +// +// // -- CLEAN -- +// SearchPaginationInput searchPaginationInput = +// PaginationFixture.getDefault().size(1110).build(); +// Page tests = +// injectTestStatusService.findAllInjectTestsByExerciseId( +// EXERCISE.getId(), searchPaginationInput); +// tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); +// } +// +// @DisplayName("Check the number of tests of an exercise") +// @Test +// void findAllInjectTestsByExerciseId() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- PREPARE -- +// injectTestStatusService.bulkTestInjects(List.of(INJECT1.getId(), INJECT2.getId())); +// +// SearchPaginationInput searchPaginationInput = +// PaginationFixture.getDefault().size(1110).build(); +// +// // -- EXECUTE -- +// Page tests = +// injectTestStatusService.findAllInjectTestsByExerciseId( +// EXERCISE.getId(), searchPaginationInput); +// assertEquals(2, tests.stream().toList().size()); +// +// // -- CLEAN -- +// tests.stream().forEach(test -> this.injectTestStatusService.deleteInjectTest(test.getId())); +// } +// +// @DisplayName("Find an inject with ID") +// @Test +// void findTestById() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- PREPARE -- +// InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); +// +// // -- EXECUTE -- +// InjectTestStatus foundTest = injectTestStatusService.findInjectTestStatusById(test.getId()); +// assertNotNull(foundTest); +// +// // -- CLEAN -- +// this.injectTestStatusService.deleteInjectTest(test.getId()); +// } +// +// @DisplayName("Delete an inject with ID") +// @Test +// void deleteInjectTest() { +// // Mock the UserDetails with a custom ID +// User user = this.userRepository.findByEmailIgnoreCase("admin@openbas.io").orElseThrow(); +// OpenBASOidcUser oidcUser = new OpenBASOidcUser(user); +// Authentication auth = +// new UsernamePasswordAuthenticationToken(oidcUser, "password", Collections.EMPTY_LIST); +// SecurityContextHolder.getContext().setAuthentication(auth); +// +// // -- PREPARE -- +// InjectTestStatus test = injectTestStatusService.testInject(INJECT1.getId()); +// +// // --EXECUTE -- +// injectTestStatusService.deleteInjectTest(test.getId()); +// +// SearchPaginationInput searchPaginationInput = +// PaginationFixture.getDefault().size(1110).build(); +// Page tests = +// injectTestStatusService.findAllInjectTestsByExerciseId( +// EXERCISE.getId(), searchPaginationInput); +// assertEquals(0, tests.stream().toList().size()); +// } +// } diff --git a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java index 456bc2f89f..15e6fbb39d 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java +++ b/openbas-framework/src/main/java/io/openbas/utils/FilterUtilsJpa.java @@ -6,6 +6,7 @@ import static io.openbas.utils.OperationUtilsJpa.*; import static io.openbas.utils.schema.SchemaUtils.getFilterableProperties; import static io.openbas.utils.schema.SchemaUtils.retrieveProperty; +import static org.springframework.util.StringUtils.hasText; import io.openbas.database.model.Base; import io.openbas.database.model.Filters.Filter; @@ -25,6 +26,7 @@ import java.util.function.Function; import org.jetbrains.annotations.Nullable; import org.springframework.data.jpa.domain.Specification; +import org.springframework.util.CollectionUtils; public class FilterUtilsJpa { @@ -111,6 +113,40 @@ private static Predicate toPredicate( return operation.apply(paths, filter.getValues()); } + /** + * Create a "in" specification for searches + * + * @param fieldName the JPA field on which the in rule is based + * @param inValues the values to include in the search for given field + * @param the data type of the specification (usually a JPA entity) + * @return the built JPA Specification + */ + public static Specification computeIn( + @Nullable final String fieldName, @Nullable final List inValues) { + if (!hasText(fieldName) || CollectionUtils.isEmpty(inValues)) { + //noinspection unchecked + return (Specification) EMPTY_SPECIFICATION; + } + return (root, query, cb) -> root.get(fieldName).in(inValues); + } + + /** + * Create a "not in" specification for searches + * + * @param fieldName the JPA field on which the exclusion rule is based + * @param excludedValues the values to exclude from the search in given field + * @param the data type of the specification (usually a JPA entity) + * @return the built JPA Specification + */ + public static Specification computeNotIn( + @Nullable final String fieldName, @Nullable final List excludedValues) { + if (!hasText(fieldName) || CollectionUtils.isEmpty(excludedValues)) { + //noinspection unchecked + return (Specification) EMPTY_SPECIFICATION; + } + return (root, query, cb) -> root.get(fieldName).in(excludedValues).not(); + } + // -- OPERATOR -- private static BiFunction, List, Predicate> computeOperation( diff --git a/openbas-framework/src/main/java/lombok.config b/openbas-framework/src/main/java/lombok.config new file mode 100644 index 0000000000..8571a69667 --- /dev/null +++ b/openbas-framework/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.accessors.chain=true diff --git a/openbas-front/src/actions/Inject.js b/openbas-front/src/actions/Inject.js index f3e06ba5a6..625173e31d 100644 --- a/openbas-front/src/actions/Inject.js +++ b/openbas-front/src/actions/Inject.js @@ -8,6 +8,16 @@ export const fetchInject = injectId => (dispatch) => { return getReferential(schema.inject, uri)(dispatch); }; +export const bulkDeleteInjects = data => (dispatch) => { + const uri = `/api/injects`; + return bulkDeleteReferential(uri, 'injects', data)(dispatch); +}; + +export const bulkUpdateInject = data => (dispatch) => { + const uri = `/api/injects`; + return putReferential(schema.inject, uri, data)(dispatch); +}; + // -- EXERCISES -- export const fetchExerciseInjects = exerciseId => (dispatch) => { @@ -25,11 +35,6 @@ export const updateInjectForExercise = (exerciseId, injectId, data) => (dispatch return putReferential(schema.inject, uri, data)(dispatch); }; -export const bulkUpdateInjectForExercise = (exerciseId, injectId, data) => (dispatch) => { - const uri = `/api/injects/${exerciseId}/${injectId}/bulk`; - return putReferential(schema.inject, uri, data)(dispatch); -}; - export const updateInjectTriggerForExercise = (exerciseId, injectId) => (dispatch) => { const uri = `/api/exercises/${exerciseId}/injects/${injectId}/trigger`; return putReferential(schema.inject, uri)(dispatch); @@ -55,11 +60,6 @@ export const deleteInjectForExercise = (exerciseId, injectId) => (dispatch) => { return delReferential(uri, 'injects', injectId)(dispatch); }; -export const bulkDeleteInjectsForExercise = (exerciseId, injectIds) => (dispatch) => { - const uri = `/api/exercises/${exerciseId}/injects`; - return bulkDeleteReferential(uri, 'injects', injectIds)(dispatch); -}; - export const executeInject = (exerciseId, values, files) => (dispatch) => { const uri = `/api/exercises/${exerciseId}/inject`; const formData = new FormData(); @@ -92,11 +92,6 @@ export const fetchScenarioInjects = scenarioId => (dispatch) => { return getReferential(schema.arrayOfInjects, uri)(dispatch); }; -export const bulkUpdateInjectForScenario = (scenarioId, injectId, data) => (dispatch) => { - const uri = `/api/scenarios/${scenarioId}/injects/${injectId}/bulk`; - return putReferential(schema.inject, uri, data)(dispatch); -}; - export const updateInjectForScenario = (scenarioId, injectId, data) => (dispatch) => { const uri = `/api/scenarios/${scenarioId}/injects/${injectId}`; return putReferential(schema.inject, uri, data)(dispatch); @@ -111,8 +106,3 @@ export const deleteInjectScenario = (scenarioId, injectId) => (dispatch) => { const uri = `/api/scenarios/${scenarioId}/injects/${injectId}`; return delReferential(uri, 'injects', injectId)(dispatch); }; - -export const bulkDeleteInjectsForScenario = (scenarioId, injectIds) => (dispatch) => { - const uri = `/api/scenarios/${scenarioId}/injects`; - return bulkDeleteReferential(uri, 'injects', injectIds)(dispatch); -}; diff --git a/openbas-front/src/actions/injects/inject-action.ts b/openbas-front/src/actions/injects/inject-action.ts index 2b660a9ced..c331c29400 100644 --- a/openbas-front/src/actions/injects/inject-action.ts +++ b/openbas-front/src/actions/injects/inject-action.ts @@ -1,7 +1,7 @@ import { Dispatch } from 'redux'; import { getReferential, simpleCall, simplePostCall } from '../../utils/Action'; -import type { Exercise, Scenario, SearchPaginationInput } from '../../utils/api-types'; +import { Exercise, InjectBulkProcessingInput, Scenario, SearchPaginationInput } from '../../utils/api-types'; import { MESSAGING$ } from '../../utils/Environment'; import * as schema from '../Schema'; @@ -10,9 +10,8 @@ export const testInject = (injectId: string) => { return simpleCall(uri); }; -export const bulkTestInjects = (injectIds: string[]) => { - const data = injectIds; - const uri = '/api/injects/bulk/test'; +export const bulkTestInjects = (data: InjectBulkProcessingInput) => { + const uri = '/api/injects/test'; return simplePostCall(uri, data, false).catch((error) => { MESSAGING$.notifyError('Can\'t be tested'); throw error; diff --git a/openbas-front/src/admin/components/common/Context.ts b/openbas-front/src/admin/components/common/Context.ts index fd77c000d0..aab80137ae 100644 --- a/openbas-front/src/admin/components/common/Context.ts +++ b/openbas-front/src/admin/components/common/Context.ts @@ -3,7 +3,7 @@ import { createContext, ReactElement } from 'react'; import type { FullArticleStore } from '../../../actions/channels/Article'; import type { InjectOutputType, InjectStore } from '../../../actions/injects/Inject'; import { Page } from '../../../components/common/queryable/Page'; -import type { +import { Article, ArticleCreateInput, ArticleUpdateInput, @@ -11,6 +11,8 @@ import type { EvaluationInput, ImportTestSummary, Inject, + InjectBulkProcessingInput, + InjectBulkUpdateInput, InjectsImportInput, InjectTestStatus, LessonsAnswer, @@ -87,7 +89,10 @@ export type TeamContextType = { export type InjectContextType = { searchInjects: (input: SearchPaginationInput) => Promise<{ data: Page }>; onAddInject: (inject: Inject) => Promise<{ result: string; entities: { injects: Record } }>; - onBulkUpdateInject: (injectId: Inject['inject_id'], inject: Inject) => Promise<{ result: string; entities: { injects: Record } }>; + onBulkUpdateInject: (param: InjectBulkUpdateInput) => Promise<{ + result: string; + entities: { injects: Record }; + }>; onUpdateInject: (injectId: Inject['inject_id'], inject: Inject) => Promise<{ result: string; entities: { injects: Record } }>; onUpdateInjectTrigger?: (injectId: Inject['inject_id']) => Promise<{ result: string; entities: { injects: Record } }>; onUpdateInjectActivation: (injectId: Inject['inject_id'], injectEnabled: { inject_enabled: boolean }) => Promise<{ @@ -98,8 +103,11 @@ export type InjectContextType = { onDeleteInject: (injectId: Inject['inject_id']) => Promise; onImportInjectFromXls?: (importId: string, input: InjectsImportInput) => Promise; onDryImportInjectFromXls?: (importId: string, input: InjectsImportInput) => Promise; - onBulkDeleteInjects: (injectIds: string[]) => void; - bulkTestInjects: (injectIds: string[]) => Promise<{ uri: string; data: InjectTestStatus[] }>; + onBulkDeleteInjects: (param: InjectBulkProcessingInput) => void; + bulkTestInjects: (param: InjectBulkProcessingInput) => Promise<{ + uri: string; + data: InjectTestStatus[]; + }>; }; export type LessonContextType = { onApplyLessonsTemplate: (data: string) => Promise; @@ -195,7 +203,10 @@ export const InjectContext = createContext({ onAddInject(_inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { return Promise.resolve({ result: '', entities: { injects: {} } }); }, - onBulkUpdateInject(_injectId: Inject['inject_id'], _inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { + onBulkUpdateInject(_param: InjectBulkUpdateInput): Promise<{ + result: string; + entities: { injects: Record }; + }> { return Promise.resolve({ result: '', entities: { injects: {} } }); }, onUpdateInject(_injectId: Inject['inject_id'], _inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { @@ -224,9 +235,12 @@ export const InjectContext = createContext({ return new Promise(() => { }); }, - onBulkDeleteInjects(_injectIds: string[]): void { + onBulkDeleteInjects(_param: InjectBulkProcessingInput): void { }, - bulkTestInjects(_injectIds: string[]): Promise<{ uri: string; data: InjectTestStatus[] }> { + bulkTestInjects(_param: InjectBulkProcessingInput): Promise<{ + uri: string; + data: InjectTestStatus[]; + }> { return new Promise<{ uri: string; data: InjectTestStatus[] }>(() => { }); }, diff --git a/openbas-front/src/admin/components/common/ToolBar.js b/openbas-front/src/admin/components/common/ToolBar.js index be4206c6ff..0ed00ebc6f 100644 --- a/openbas-front/src/admin/components/common/ToolBar.js +++ b/openbas-front/src/admin/components/common/ToolBar.js @@ -9,7 +9,22 @@ import { ForwardToInbox, GroupsOutlined, } from '@mui/icons-material'; -import { Autocomplete, Button, Drawer, FormControl, Grid, IconButton, InputLabel, MenuItem, Select, Slide, TextField, Toolbar, Tooltip, Typography } from '@mui/material'; +import { + Autocomplete, + Button, + Drawer, + FormControl, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + Slide, + TextField, + Toolbar, + Tooltip, + Typography, +} from '@mui/material'; import { withStyles, withTheme } from '@mui/styles'; import { SelectGroup } from 'mdi-material-ui'; import * as PropTypes from 'prop-types'; diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 0a2b7662e6..a55c0613af 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -18,7 +18,15 @@ import { useFormatter } from '../../../../components/i18n'; import ItemBoolean from '../../../../components/ItemBoolean'; import ItemTags from '../../../../components/ItemTags'; import PlatformIcon from '../../../../components/PlatformIcon'; -import type { Article, FilterGroup, Inject, InjectTestStatus, Team, Variable } from '../../../../utils/api-types'; +import { + Article, + FilterGroup, + Inject, + InjectBulkUpdateOperation, + InjectTestStatus, + Team, + Variable, +} from '../../../../utils/api-types'; import { MESSAGING$ } from '../../../../utils/Environment'; import useEntityToggle from '../../../../utils/hooks/useEntityToggle'; import { splitDuration } from '../../../../utils/Time'; @@ -336,7 +344,9 @@ const Injects: FunctionComponent = ({ handleToggleSelectAll, onToggleEntity, numberOfSelectedElements, - } = useEntityToggle<{ inject_id: string }>('inject', injects.length); + } = useEntityToggle<{ + inject_id: string; + }>('inject', injects.length, queryableHelpers.paginationHelpers.getTotalElements()); const onRowShiftClick = (currentIndex: number, currentEntity: { inject_id: string }, event: React.SyntheticEvent | null = null) => { if (event) { event.stopPropagation(); @@ -381,94 +391,65 @@ const Injects: FunctionComponent = ({ }; const injectsToProcess = selectAll - ? injects.filter((inject: InjectOutputType) => !R.keys(deSelectedElements).includes(inject.inject_id)) + ? [] : injects.filter( (inject: InjectOutputType) => R.keys(selectedElements).includes(inject.inject_id) && !R.keys(deSelectedElements).includes(inject.inject_id), ); + const injectsToIgnore = selectAll + ? injects.filter((inject: InjectOutputType) => R.keys(deSelectedElements).includes(inject.inject_id)) + : injects.filter( + (inject: InjectOutputType) => !R.keys(selectedElements).includes(inject.inject_id) && R.keys(deSelectedElements).includes(inject.inject_id), + ); + const massUpdateInjects = async (actions: { field: string; type: string; values: { value: string }[]; }[]) => { - const updateFields = [ - 'inject_title', - 'inject_description', - 'inject_injector_contract', - 'inject_content', - 'inject_depends_on', - 'inject_depends_duration', - 'inject_teams', - 'inject_assets', - 'inject_asset_groups', - 'inject_documents', - 'inject_all_teams', - 'inject_country', - 'inject_city', - 'inject_tags', - ]; - const injectsToUpdate = injectsToProcess.filter((inject: InjectOutputType) => inject.inject_injector_contract?.convertedContent); + const operationsToPerform: InjectBulkUpdateOperation[] = []; for (const action of actions) { - for (const element of injectsToUpdate) { - const injectToUpdate: Omit & { inject_injector_contract: string } = { - ...element, - inject_injector_contract: element.inject_injector_contract.injector_contract_id, - }; - switch (action.type) { - case 'ADD': - // @ts-expect-error define type - if (isNotEmptyField(injectToUpdate[`inject_${action.field}`])) { - // @ts-expect-error define type - injectToUpdate[`inject_${action.field}`] = R.uniq([...injectToUpdate[`inject_${action.field}`], ...action.values.map(n => n.value)]); - } else { - // @ts-expect-error define type - injectToUpdate[`inject_${action.field}`] = R.uniq(action.values.map(n => n.value)); - } - // eslint-disable-next-line no-await-in-loop - await injectContext.onBulkUpdateInject(injectToUpdate.inject_id, R.pick(updateFields, injectToUpdate)) - .then((result: { result: string; entities: { injects: Record } }) => { - onUpdate(result); - }); - break; - case 'REPLACE': - // @ts-expect-error define type - injectToUpdate[`inject_${action.field}`] = R.uniq(action.values.map(n => n.value)); - // eslint-disable-next-line no-await-in-loop - await injectContext.onBulkUpdateInject(injectToUpdate.inject_id, R.pick(updateFields, injectToUpdate)) - .then((result: { result: string; entities: { injects: Record } }) => { - onUpdate(result); - }); - break; - case 'REMOVE': - // @ts-expect-error define type - if (isNotEmptyField(injectToUpdate[`inject_${action.field}`])) { - // @ts-expect-error define type - injectToUpdate[`inject_${action.field}`] = injectToUpdate[`inject_${action.field}`].filter((n: string) => !action.values.map(o => o.value).includes(n)); - } else { - // @ts-expect-error define type - injectToUpdate[`inject_${action.field}`] = []; - } - // eslint-disable-next-line no-await-in-loop - await injectContext.onBulkUpdateInject(injectToUpdate.inject_id, R.pick(updateFields, injectToUpdate)) - .then((result: { result: string; entities: { injects: Record } }) => { - onUpdate(result); - }); - break; - default: - return; - } - } + // Case where no values where given + if (!action.values.length || !action.values[0].value) continue; + operationsToPerform.push({ + operation: action.type.toLowerCase(), + field: action.field, + values: R.uniq(action.values.map(n => n.value)), + } as InjectBulkUpdateOperation); // Cast is necessary because typeof enum don't work with operation and fields } + await injectContext.onBulkUpdateInject({ + search_pagination_input: selectAll ? searchPaginationInput : undefined, + inject_ids_to_process: selectAll ? undefined : injectsToProcess.map((inject: InjectOutputType) => inject.inject_id), + inject_ids_to_ignore: injectsToIgnore.map((inject: InjectOutputType) => inject.inject_id), + exercise_or_scenario_id: exerciseOrScenarioId, + update_operations: operationsToPerform, + }) + .then((result: { result: string; entities: { injects: Record } }) => { + onUpdate(result); + }); }; const bulkDeleteInjects = () => { const deleteIds = injectsToProcess.map((inject: InjectOutputType) => inject.inject_id); - injectContext.onBulkDeleteInjects(deleteIds); + const ignoreIds = injectsToIgnore.map((inject: InjectOutputType) => inject.inject_id); + injectContext.onBulkDeleteInjects({ + search_pagination_input: selectAll ? searchPaginationInput : undefined, + inject_ids_to_process: selectAll ? undefined : deleteIds, + inject_ids_to_ignore: ignoreIds, + exercise_or_scenario_id: exerciseOrScenarioId, + }); onMassDelete(deleteIds); }; const massTestInjects = () => { - injectContext.bulkTestInjects(injectsToProcess.map((inject: InjectOutputType) => inject.inject_id)).then((result: { uri: string; data: InjectTestStatus[] }) => { + const testIds = injectsToProcess.map((inject: InjectOutputType) => inject.inject_id); + const ignoreIds = injectsToIgnore.map((inject: InjectOutputType) => inject.inject_id); + injectContext.bulkTestInjects({ + search_pagination_input: selectAll ? searchPaginationInput : undefined, + inject_ids_to_process: selectAll ? undefined : testIds, + inject_ids_to_ignore: ignoreIds, + exercise_or_scenario_id: exerciseOrScenarioId, + }).then((result: { uri: string; data: InjectTestStatus[] }) => { if (numberOfSelectedElements === 1) { MESSAGING$.notifySuccess(t('Inject test has been sent, you can view test logs details on {itsDedicatedPage}.', { itsDedicatedPage: {t('its dedicated page')}, @@ -669,6 +650,7 @@ const Injects: FunctionComponent = ({ /> = ({ }; const handleSubmitAllTest = () => { - bulkTestInjects(injectIds!).then((result: { data: InjectTestStatus[] }) => { + bulkTestInjects({ + inject_ids_to_process: injectIds!, + }!).then((result: { data: InjectTestStatus[] }) => { onTest?.(result.data); MESSAGING$.notifySuccess(t('{testNumber} test(s) sent', { testNumber: injectIds?.length })); return result; diff --git a/openbas-front/src/admin/components/scenarios/scenario/ScenarioContext.ts b/openbas-front/src/admin/components/scenarios/scenario/ScenarioContext.ts index 0527af7f6c..5b3e530f3e 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/ScenarioContext.ts +++ b/openbas-front/src/admin/components/scenarios/scenario/ScenarioContext.ts @@ -1,7 +1,7 @@ import { addInjectForScenario, - bulkDeleteInjectsForScenario, - bulkUpdateInjectForScenario, + bulkDeleteInjects, + bulkUpdateInject, deleteInjectScenario, fetchScenarioInjects, updateInjectActivationForScenario, @@ -9,9 +9,23 @@ import { } from '../../../../actions/Inject'; import type { InjectOutputType, InjectStore } from '../../../../actions/injects/Inject'; import { bulkTestInjects, searchScenarioInjectsSimple } from '../../../../actions/injects/inject-action'; -import { dryImportXlsForScenario, fetchScenario, fetchScenarioTeams, importXlsForScenario } from '../../../../actions/scenarios/scenario-actions'; +import { + dryImportXlsForScenario, + fetchScenario, + fetchScenarioTeams, + importXlsForScenario, +} from '../../../../actions/scenarios/scenario-actions'; import { Page } from '../../../../components/common/queryable/Page'; -import type { ImportTestSummary, Inject, InjectsImportInput, InjectTestStatus, Scenario, SearchPaginationInput } from '../../../../utils/api-types'; +import { + ImportTestSummary, + Inject, + InjectBulkProcessingInput, + InjectBulkUpdateInput, + InjectsImportInput, + InjectTestStatus, + Scenario, + SearchPaginationInput, +} from '../../../../utils/api-types'; import { useAppDispatch } from '../../../../utils/hooks'; const injectContextForScenario = (scenario: Scenario) => { @@ -24,8 +38,11 @@ const injectContextForScenario = (scenario: Scenario) => { onAddInject(inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { return dispatch(addInjectForScenario(scenario.scenario_id, inject)); }, - onBulkUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { - return dispatch(bulkUpdateInjectForScenario(scenario.scenario_id, injectId, inject)); + onBulkUpdateInject(param: InjectBulkUpdateInput): Promise<{ + result: string; + entities: { injects: Record }; + }> { + return dispatch(bulkUpdateInject(param)); }, onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { return dispatch(updateInjectForScenario(scenario.scenario_id, injectId, inject)); @@ -50,11 +67,11 @@ const injectContextForScenario = (scenario: Scenario) => { async onDryImportInjectFromXls(importId: string, input: InjectsImportInput): Promise { return dryImportXlsForScenario(scenario.scenario_id, importId, input).then(result => result.data); }, - onBulkDeleteInjects(injectIds: string[]): void { - return dispatch(bulkDeleteInjectsForScenario(scenario.scenario_id, injectIds)); + onBulkDeleteInjects(param: InjectBulkProcessingInput): void { + return dispatch(bulkDeleteInjects(param)); }, - bulkTestInjects(injectIds: string[]): Promise<{ uri: string; data: InjectTestStatus[] }> { - return bulkTestInjects(injectIds).then(result => ({ + bulkTestInjects(param: InjectBulkProcessingInput): Promise<{ uri: string; data: InjectTestStatus[] }> { + return bulkTestInjects(param).then(result => ({ uri: `/admin/scenarios/${scenario.scenario_id}/tests`, data: result.data, })); diff --git a/openbas-front/src/admin/components/simulations/simulation/ExerciseContext.ts b/openbas-front/src/admin/components/simulations/simulation/ExerciseContext.ts index 41e85c0bec..6bf19bbc2d 100644 --- a/openbas-front/src/admin/components/simulations/simulation/ExerciseContext.ts +++ b/openbas-front/src/admin/components/simulations/simulation/ExerciseContext.ts @@ -2,8 +2,8 @@ import { fetchExercise, fetchExerciseTeams } from '../../../../actions/Exercise' import { dryImportXlsForExercise, importXlsForExercise } from '../../../../actions/exercises/exercise-action'; import { addInjectForExercise, - bulkDeleteInjectsForExercise, - bulkUpdateInjectForExercise, + bulkDeleteInjects, + bulkUpdateInject, deleteInjectForExercise, fetchExerciseInjects, injectDone, @@ -14,7 +14,16 @@ import { import type { InjectOutputType, InjectStore } from '../../../../actions/injects/Inject'; import { bulkTestInjects, searchExerciseInjectsSimple } from '../../../../actions/injects/inject-action'; import { Page } from '../../../../components/common/queryable/Page'; -import type { Exercise, ImportTestSummary, Inject, InjectsImportInput, InjectTestStatus, SearchPaginationInput } from '../../../../utils/api-types'; +import type { + Exercise, + ImportTestSummary, + Inject, + InjectBulkProcessingInput, + InjectBulkUpdateInput, + InjectsImportInput, + InjectTestStatus, + SearchPaginationInput, +} from '../../../../utils/api-types'; import { useAppDispatch } from '../../../../utils/hooks'; const injectContextForExercise = (exercise: Exercise) => { @@ -27,8 +36,12 @@ const injectContextForExercise = (exercise: Exercise) => { onAddInject(inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { return dispatch(addInjectForExercise(exercise.exercise_id, inject)); }, - onBulkUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { - return dispatch(bulkUpdateInjectForExercise(exercise.exercise_id, injectId, inject)); + onBulkUpdateInject(param: InjectBulkUpdateInput): Promise<{ + result: string; + entities: { injects: Record }; + }> { + // exercise.exercise_id + return dispatch(bulkUpdateInject(param)); }, onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string; entities: { injects: Record } }> { return dispatch(updateInjectForExercise(exercise.exercise_id, injectId, inject)); @@ -59,11 +72,12 @@ const injectContextForExercise = (exercise: Exercise) => { async onDryImportInjectFromXls(importId: string, input: InjectsImportInput): Promise { return dryImportXlsForExercise(exercise.exercise_id, importId, input).then(result => result.data); }, - onBulkDeleteInjects(injectIds: string[]): void { - return dispatch(bulkDeleteInjectsForExercise(exercise.exercise_id, injectIds)); + onBulkDeleteInjects(param: InjectBulkProcessingInput): void { + // exercise.exercise_id + return dispatch(bulkDeleteInjects(param)); }, - bulkTestInjects(injectIds: string[]): Promise<{ uri: string; data: InjectTestStatus[] }> { - return bulkTestInjects(injectIds).then(result => ({ + bulkTestInjects(param: InjectBulkProcessingInput): Promise<{ uri: string; data: InjectTestStatus[] }> { + return bulkTestInjects(param).then(result => ({ uri: `/admin/simulations/${exercise.exercise_id}/tests`, data: result.data, })); diff --git a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx index f3644fbae8..b50b61de4e 100644 --- a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx +++ b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx @@ -1,7 +1,7 @@ import { Box, Button, Chip } from '@mui/material'; import { makeStyles } from '@mui/styles'; -import { useEffect, useState } from 'react'; import * as React from 'react'; +import { useEffect, useState } from 'react'; import InjectorContractSwitchFilter from '../../../../admin/components/common/filters/InjectorContractSwitchFilter'; import MitreFilter, { MITRE_FILTER_KEY } from '../../../../admin/components/common/filters/MitreFilter'; diff --git a/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx index 36f000ef23..b8a1efccef 100644 --- a/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx +++ b/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx @@ -1,6 +1,6 @@ import { TablePagination } from '@mui/material'; -import { FunctionComponent } from 'react'; import * as React from 'react'; +import { FunctionComponent } from 'react'; import { PaginationHelpers } from './PaginationHelpers'; import { ROWS_PER_PAGE_OPTIONS } from './usPaginationState'; diff --git a/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx b/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx index 6cff771108..3cee944b09 100644 --- a/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx +++ b/openbas-front/src/components/common/queryable/pagination/usPaginationState.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react'; import * as React from 'react'; +import { useEffect, useState } from 'react'; import { PaginationHelpers } from './PaginationHelpers'; diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index 0ad0261ec8..e74491cda2 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -1544,6 +1544,7 @@ const i18n = { '你想要删除这个注入么?', 'Do you want to delete these {count} injects?': '你想要删除这些{count} 注入么 ?', + 'Do you want to test these {count} injects?': '你想要测试这些{count} 注入么 ?', 'Do you want to delete this channel?': '你想要删除这个频道么 ?', 'Start date (optional)': '开始日期 (可选)', diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index f8f5ddac75..0aa9a87b2b 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -9,6 +9,8 @@ * --------------------------------------------------------------- */ +import {InjectBulkUpdateFields, InjectBulkUpdateOperations} from "./enums"; + export interface Agent { agent_active?: boolean; agent_asset: string; @@ -1404,6 +1406,34 @@ export interface InjectInput { inject_title?: string; } +export interface InjectBulkProcessingInput { + search_pagination_input?: SearchPaginationInput; + inject_ids_to_process?: string[]; + inject_ids_to_ignore?: string[]; + exercise_or_scenario_id?: string; +} + +export interface InjectBulkUpdateInput extends InjectBulkProcessingInput { + update_operations: InjectBulkUpdateOperation[]; +} + +export interface InjectBulkUpdateOperation { + operation: InjectBulkUpdateOperations, + field: InjectBulkUpdateFields, + values: string[], +} + +export interface AttackPatternCreateInput { + attack_pattern_description?: string; + attack_pattern_external_id: string; + attack_pattern_kill_chain_phases?: string[]; + attack_pattern_name: string; + attack_pattern_parent?: string; + attack_pattern_permissions_required?: string[]; + attack_pattern_platforms?: string[]; + attack_pattern_stix_id?: string; +} + export interface InjectOutput { inject_asset_groups?: string[]; inject_assets?: string[]; diff --git a/openbas-front/src/utils/enums.ts b/openbas-front/src/utils/enums.ts new file mode 100644 index 0000000000..bb09bffd67 --- /dev/null +++ b/openbas-front/src/utils/enums.ts @@ -0,0 +1,17 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +enum InjectBulkUpdateOperationsEnum { + add = 'add', + remove = 'remove', + replace = 'replace', +} + +export type InjectBulkUpdateOperations = keyof typeof InjectBulkUpdateOperationsEnum; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +enum InjectBulkUpdateFieldsEnum { + assets = 'assets', + assetGroups = 'asset_groups', + teams = 'teams', +} + +export type InjectBulkUpdateFields = keyof typeof InjectBulkUpdateFieldsEnum; diff --git a/openbas-front/src/utils/hooks/useEntityToggle.ts b/openbas-front/src/utils/hooks/useEntityToggle.ts index 787e31c165..ff606b84c5 100644 --- a/openbas-front/src/utils/hooks/useEntityToggle.ts +++ b/openbas-front/src/utils/hooks/useEntityToggle.ts @@ -1,6 +1,6 @@ import * as R from 'ramda'; -import { useState } from 'react'; import * as React from 'react'; +import { useState } from 'react'; export interface UseEntityToggle { selectedElements: Record; @@ -20,6 +20,7 @@ export interface UseEntityToggle { const useEntityToggle = >( prefix: string, numberOfElements: number, + totalNumberOfElements?: number, ): UseEntityToggle => { const [selectedElements, setSelectedElements] = useState>( {}, @@ -91,7 +92,9 @@ const useEntityToggle = >( }; let numberOfSelectedElements = Object.keys(selectedElements).length; if (selectAll) { - numberOfSelectedElements = (numberOfElements ?? 0) - Object.keys(deSelectedElements).length; + numberOfSelectedElements = selectAll + ? (totalNumberOfElements ?? 0) - Object.keys(deSelectedElements).length + : (numberOfElements ?? 0) - Object.keys(deSelectedElements).length; } return { onToggleEntity, diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 41349df082..4406e7aa14 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -36,6 +36,8 @@ public class Inject implements Base, Injection { public static final int SPEED_STANDARD = 1; // Standard speed define by the user. + public static final String ID_COLUMN_NAME = "inject_id"; + public static final String ID_FIELD_NAME = "id"; public static final Comparator executionComparator = (o1, o2) -> { @@ -47,7 +49,7 @@ public class Inject implements Base, Injection { @Getter @Id - @Column(name = "inject_id") + @Column(name = ID_COLUMN_NAME) @GeneratedValue(generator = "UUID") @UuidGenerator @JsonProperty("inject_id") diff --git a/openbas-model/src/main/java/io/openbas/database/specification/InjectSpecification.java b/openbas-model/src/main/java/io/openbas/database/specification/InjectSpecification.java index 23cecbbaf0..9718af7b6f 100644 --- a/openbas-model/src/main/java/io/openbas/database/specification/InjectSpecification.java +++ b/openbas-model/src/main/java/io/openbas/database/specification/InjectSpecification.java @@ -16,14 +16,36 @@ public class InjectSpecification { private InjectSpecification() {} + /** + * Create a specification to get an exercise + * + * @deprecated Use fromSimulation instead + * @param exerciseId the exercice ID to search + * @return the built specification + */ + @Deprecated(since = "1.11.0", forRemoval = true) public static Specification fromExercise(String exerciseId) { - return (root, query, cb) -> cb.equal(root.get("exercise").get("id"), exerciseId); + return fromSimulation(exerciseId); + } + + public static Specification fromSimulation(String simulationId) { + return (root, query, cb) -> cb.equal(root.get("exercise").get("id"), simulationId); } public static Specification fromScenario(String scenarioId) { return (root, query, cb) -> cb.equal(root.get("scenario").get("id"), scenarioId); } + /** + * Get injects from a scenario or a simulation + * + * @param scenarioOrSimulationId the id of the scenario or the simulation + * @return the constructed specification + */ + public static Specification fromScenarioOrSimulation(String scenarioOrSimulationId) { + return fromSimulation(scenarioOrSimulationId).or(fromScenario(scenarioOrSimulationId)); + } + public static Specification next() { return (root, query, cb) -> { Path exercisePath = root.get("exercise");