diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java b/openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java new file mode 100644 index 0000000000..21ca0f6e99 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java @@ -0,0 +1,58 @@ +package io.openbas.migration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.openbas.database.model.InjectDependencyConditions; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; + +@Component +public class V3_46__Add_table_inject_dependencies extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + Statement select = context.getConnection().createStatement(); + select.execute(""" + CREATE TABLE injects_dependencies ( + inject_parent_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, + inject_children_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, + dependency_condition JSONB, + dependency_created_at TIMESTAMP DEFAULT now(), + dependency_updated_at TIMESTAMP DEFAULT now(), + PRIMARY KEY(inject_parent_id, inject_children_id) + ); + CREATE INDEX idx_injects_dependencies ON injects_dependencies(inject_children_id); + """); + + // Migration datas + ResultSet results = select.executeQuery("SELECT * FROM injects WHERE inject_depends_from_another IS NOT NULL"); + PreparedStatement statement = context.getConnection().prepareStatement( + """ + INSERT INTO injects_dependencies(inject_parent_id, inject_children_id, dependency_condition) + VALUES (?, ?, to_json(?::json)) + """ + ); + while (results.next()) { + String injectId = results.getString("inject_id"); + String parentId = results.getString("inject_depends_from_another"); + InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); + injectDependencyCondition.setMode(InjectDependencyConditions.DependencyMode.and); + InjectDependencyConditions.Condition condition = new InjectDependencyConditions.Condition(); + condition.setKey("Execution"); + condition.setOperator(InjectDependencyConditions.DependencyOperator.eq); + condition.setValue(true); + injectDependencyCondition.setConditions(List.of(condition)); + statement.setString(1, parentId); + statement.setString(2, injectId); + statement.setString(3, mapper.writeValueAsString(injectDependencyCondition)); + statement.addBatch(); + } + statement.executeBatch(); + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java index f650601e82..571fbe4f54 100644 --- a/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/atomic_testing/AtomicTestingApi.java @@ -21,11 +21,13 @@ import java.util.List; @RestController -@RequestMapping("/api/atomic-testings") +@RequestMapping(AtomicTestingApi.ATOMIC_TESTING_URI) @PreAuthorize("isAdmin()") @RequiredArgsConstructor public class AtomicTestingApi extends RestBehavior { + public static final String ATOMIC_TESTING_URI = "/api/atomic-testings"; + private final AtomicTestingService atomicTestingService; private final InjectExpectationService injectExpectationService; diff --git a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java index c9895b6526..e82c916e54 100644 --- a/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/document/DocumentApi.java @@ -50,509 +50,528 @@ @RestController public class DocumentApi extends RestBehavior { - private FileService fileService; - private TagRepository tagRepository; - private DocumentRepository documentRepository; - private ExerciseRepository exerciseRepository; - private ScenarioRepository scenarioRepository; - private InjectService injectService; - private InjectDocumentRepository injectDocumentRepository; - private ChallengeRepository challengeRepository; - private UserRepository userRepository; - private InjectorRepository injectorRepository; - private CollectorRepository collectorRepository; - private SecurityPlatformRepository securityPlatformRepository; - - @Autowired - public void setUserRepository(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Autowired - public void setInjectDocumentRepository(InjectDocumentRepository injectDocumentRepository) { - this.injectDocumentRepository = injectDocumentRepository; - } - - @Autowired - public void setInjectService(InjectService injectService) { - this.injectService = injectService; - } - - @Autowired - public void setExerciseRepository(ExerciseRepository exerciseRepository) { - this.exerciseRepository = exerciseRepository; - } - - @Autowired - public void setScenarioRepository(ScenarioRepository scenarioRepository) { - this.scenarioRepository = scenarioRepository; - } - - @Autowired - public void setTagRepository(TagRepository tagRepository) { - this.tagRepository = tagRepository; - } - - @Autowired - public void setDocumentRepository(DocumentRepository documentRepository) { - this.documentRepository = documentRepository; - } - - @Autowired - public void setChallengeRepository(ChallengeRepository challengeRepository) { - this.challengeRepository = challengeRepository; - } - - @Autowired - public void setInjectorRepository(InjectorRepository injectorRepository) { - this.injectorRepository = injectorRepository; - } - - @Autowired - public void setCollectorRepository(CollectorRepository collectorRepository) { - this.collectorRepository = collectorRepository; - } - - @Autowired - public void setSecurityPlatformRepository(SecurityPlatformRepository securityPlatformRepository) { - this.securityPlatformRepository = securityPlatformRepository; - } - - @Autowired - public void setFileService(FileService fileService) { - this.fileService = fileService; - } - - private Optional resolveDocument(String documentId) { - OpenBASPrincipal user = currentUser(); - if (user.isAdmin()) { - return documentRepository.findById(documentId); - } else { - return documentRepository.findByIdGranted(documentId, user.getId()); + private FileService fileService; + private TagRepository tagRepository; + private DocumentRepository documentRepository; + private ExerciseRepository exerciseRepository; + private ScenarioRepository scenarioRepository; + private InjectService injectService; + private InjectDocumentRepository injectDocumentRepository; + private ChallengeRepository challengeRepository; + private UserRepository userRepository; + private InjectorRepository injectorRepository; + private CollectorRepository collectorRepository; + private SecurityPlatformRepository securityPlatformRepository; + + @Autowired + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Autowired + public void setInjectDocumentRepository(InjectDocumentRepository injectDocumentRepository) { + this.injectDocumentRepository = injectDocumentRepository; + } + + @Autowired + public void setInjectService(InjectService injectService) { + this.injectService = injectService; + } + + @Autowired + public void setExerciseRepository(ExerciseRepository exerciseRepository) { + this.exerciseRepository = exerciseRepository; + } + + @Autowired + public void setScenarioRepository(ScenarioRepository scenarioRepository) { + this.scenarioRepository = scenarioRepository; + } + + @Autowired + public void setTagRepository(TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + @Autowired + public void setDocumentRepository(DocumentRepository documentRepository) { + this.documentRepository = documentRepository; + } + + @Autowired + public void setChallengeRepository(ChallengeRepository challengeRepository) { + this.challengeRepository = challengeRepository; + } + + @Autowired + public void setInjectorRepository(InjectorRepository injectorRepository) { + this.injectorRepository = injectorRepository; + } + + @Autowired + public void setCollectorRepository(CollectorRepository collectorRepository) { + this.collectorRepository = collectorRepository; + } + + @Autowired + public void setSecurityPlatformRepository(SecurityPlatformRepository securityPlatformRepository) { + this.securityPlatformRepository = securityPlatformRepository; + } + + @Autowired + public void setFileService(FileService fileService) { + this.fileService = fileService; + } + + private Optional resolveDocument(String documentId) { + OpenBASPrincipal user = currentUser(); + if (user.isAdmin()) { + return documentRepository.findById(documentId); + } else { + return documentRepository.findByIdGranted(documentId, user.getId()); + } + } + + @PostMapping("/api/documents") + @Transactional(rollbackOn = Exception.class) + public Document uploadDocument(@Valid @RequestPart("input") DocumentCreateInput input, + @RequestPart("file") MultipartFile file) throws Exception { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + String fileTarget = DigestUtils.md5Hex(file.getInputStream()) + "." + extension; + Optional targetDocument = documentRepository.findByTarget(fileTarget); + if (targetDocument.isPresent()) { + Document document = targetDocument.get(); + // Compute exercises + if (!document.getExercises().isEmpty()) { + Set exercises = new HashSet<>(document.getExercises()); + List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); + exercises.addAll(inputExercises); + document.setExercises(exercises); + } + // Compute scenarios + if (!document.getScenarios().isEmpty()) { + Set scenarios = new HashSet<>(document.getScenarios()); + List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); + scenarios.addAll(inputScenarios); + document.setScenarios(scenarios); + } + // Compute tags + Set tags = new HashSet<>(document.getTags()); + List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); + tags.addAll(inputTags); + document.setTags(tags); + return documentRepository.save(document); + } else { + fileService.uploadFile(fileTarget, file); + Document document = new Document(); + document.setTarget(fileTarget); + document.setName(file.getOriginalFilename()); + document.setDescription(input.getDescription()); + if (!input.getExerciseIds().isEmpty()) { + document.setExercises(iterableToSet(exerciseRepository.findAllById(input.getExerciseIds()))); + } + if (!input.getScenarioIds().isEmpty()) { + document.setScenarios(iterableToSet(scenarioRepository.findAllById(input.getScenarioIds()))); + } + document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); + document.setType(file.getContentType()); + return documentRepository.save(document); + } + } + + @PostMapping("/api/documents/upsert") + @Transactional(rollbackOn = Exception.class) + public Document upsertDocument(@Valid @RequestPart("input") DocumentCreateInput input, + @RequestPart("file") MultipartFile file) throws Exception { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + String fileTarget = DigestUtils.md5Hex(file.getInputStream()) + "." + extension; + Optional targetDocument = documentRepository.findByTarget(fileTarget); + // Document already exists by hash + if (targetDocument.isPresent()) { + Document document = targetDocument.get(); + // Compute exercises + if (!document.getExercises().isEmpty()) { + Set exercises = new HashSet<>(document.getExercises()); + List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); + exercises.addAll(inputExercises); + document.setExercises(exercises); + } + // Compute scenarios + if (!document.getScenarios().isEmpty()) { + Set scenarios = new HashSet<>(document.getScenarios()); + List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); + scenarios.addAll(inputScenarios); + document.setScenarios(scenarios); + } + // Compute tags + Set tags = new HashSet<>(document.getTags()); + List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); + tags.addAll(inputTags); + document.setTags(tags); + return documentRepository.save(document); + } else { + Optional existingDocument = documentRepository.findByName(file.getOriginalFilename()); + if (existingDocument.isPresent()) { + Document document = existingDocument.get(); + // Update doc + fileService.uploadFile(fileTarget, file); + document.setDescription(input.getDescription()); + + // Compute exercises + if (!document.getExercises().isEmpty()) { + Set exercises = new HashSet<>(document.getExercises()); + List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); + exercises.addAll(inputExercises); + document.setExercises(exercises); } - } - - @PostMapping("/api/documents") - @Transactional(rollbackOn = Exception.class) - public Document uploadDocument(@Valid @RequestPart("input") DocumentCreateInput input, - @RequestPart("file") MultipartFile file) throws Exception { - String extension = FilenameUtils.getExtension(file.getOriginalFilename()); - String fileTarget = DigestUtils.md5Hex(file.getInputStream()) + "." + extension; - Optional targetDocument = documentRepository.findByTarget(fileTarget); - if (targetDocument.isPresent()) { - Document document = targetDocument.get(); - // Compute exercises - if (!document.getExercises().isEmpty()) { - Set exercises = new HashSet<>(document.getExercises()); - List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); - exercises.addAll(inputExercises); - document.setExercises(exercises); - } - // Compute scenarios - if (!document.getScenarios().isEmpty()) { - Set scenarios = new HashSet<>(document.getScenarios()); - List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); - scenarios.addAll(inputScenarios); - document.setScenarios(scenarios); - } - // Compute tags - Set tags = new HashSet<>(document.getTags()); - List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); - tags.addAll(inputTags); - document.setTags(tags); - return documentRepository.save(document); - } else { - fileService.uploadFile(fileTarget, file); - Document document = new Document(); - document.setTarget(fileTarget); - document.setName(file.getOriginalFilename()); - document.setDescription(input.getDescription()); - if (!input.getExerciseIds().isEmpty()) { - document.setExercises(iterableToSet(exerciseRepository.findAllById(input.getExerciseIds()))); - } - if (!input.getScenarioIds().isEmpty()) { - document.setScenarios(iterableToSet(scenarioRepository.findAllById(input.getScenarioIds()))); - } - document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); - document.setType(file.getContentType()); - return documentRepository.save(document); - } - } - - @PostMapping("/api/documents/upsert") - @Transactional(rollbackOn = Exception.class) - public Document upsertDocument(@Valid @RequestPart("input") DocumentCreateInput input, - @RequestPart("file") MultipartFile file) throws Exception { - String extension = FilenameUtils.getExtension(file.getOriginalFilename()); - String fileTarget = DigestUtils.md5Hex(file.getInputStream()) + "." + extension; - Optional targetDocument = documentRepository.findByTarget(fileTarget); - // Document already exists by hash - if (targetDocument.isPresent()) { - Document document = targetDocument.get(); - // Compute exercises - if (!document.getExercises().isEmpty()) { - Set exercises = new HashSet<>(document.getExercises()); - List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); - exercises.addAll(inputExercises); - document.setExercises(exercises); - } - // Compute scenarios - if (!document.getScenarios().isEmpty()) { - Set scenarios = new HashSet<>(document.getScenarios()); - List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); - scenarios.addAll(inputScenarios); - document.setScenarios(scenarios); - } - // Compute tags - Set tags = new HashSet<>(document.getTags()); - List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); - tags.addAll(inputTags); - document.setTags(tags); - return documentRepository.save(document); - } else { - Optional existingDocument = documentRepository.findByName(file.getOriginalFilename()); - if (existingDocument.isPresent()) { - Document document = existingDocument.get(); - // Update doc - fileService.uploadFile(fileTarget, file); - document.setDescription(input.getDescription()); - - // Compute exercises - if (!document.getExercises().isEmpty()) { - Set exercises = new HashSet<>(document.getExercises()); - List inputExercises = fromIterable(exerciseRepository.findAllById(input.getExerciseIds())); - exercises.addAll(inputExercises); - document.setExercises(exercises); - } - // Compute scenarios - if (!document.getScenarios().isEmpty()) { - Set scenarios = new HashSet<>(document.getScenarios()); - List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); - scenarios.addAll(inputScenarios); - document.setScenarios(scenarios); - } - // Compute tags - Set tags = new HashSet<>(document.getTags()); - List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); - tags.addAll(inputTags); - document.setTags(tags); - return documentRepository.save(document); - } else { - fileService.uploadFile(fileTarget, file); - Document document = new Document(); - document.setTarget(fileTarget); - document.setName(file.getOriginalFilename()); - document.setDescription(input.getDescription()); - if (!input.getExerciseIds().isEmpty()) { - document.setExercises(iterableToSet(exerciseRepository.findAllById(input.getExerciseIds()))); - } - if (!input.getScenarioIds().isEmpty()) { - document.setScenarios(iterableToSet(scenarioRepository.findAllById(input.getScenarioIds()))); - } - document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); - document.setType(file.getContentType()); - return documentRepository.save(document); - } + // Compute scenarios + if (!document.getScenarios().isEmpty()) { + Set scenarios = new HashSet<>(document.getScenarios()); + List inputScenarios = fromIterable(scenarioRepository.findAllById(input.getScenarioIds())); + scenarios.addAll(inputScenarios); + document.setScenarios(scenarios); } - } - - @GetMapping("/api/documents") - public List documents() { - OpenBASPrincipal user = currentUser(); - if (user.isAdmin()) { - return documentRepository.rawAllDocuments(); - } else { - return documentRepository.rawAllDocumentsByAccessLevel(user.getId()); + // Compute tags + Set tags = new HashSet<>(document.getTags()); + List inputTags = fromIterable(tagRepository.findAllById(input.getTagIds())); + tags.addAll(inputTags); + document.setTags(tags); + return documentRepository.save(document); + } else { + fileService.uploadFile(fileTarget, file); + Document document = new Document(); + document.setTarget(fileTarget); + document.setName(file.getOriginalFilename()); + document.setDescription(input.getDescription()); + if (!input.getExerciseIds().isEmpty()) { + document.setExercises(iterableToSet(exerciseRepository.findAllById(input.getExerciseIds()))); } - } - - @PostMapping("/api/documents/search") - public Page searchDocuments(@RequestBody @Valid final SearchPaginationInput searchPaginationInput) { - OpenBASPrincipal user = currentUser(); - if (user.isAdmin()) { - return buildPaginationJPA( - (Specification specification, Pageable pageable) -> this.documentRepository.findAll( - specification, pageable), - searchPaginationInput, - Document.class - ).map(RawPaginationDocument::new); - } else { - return buildPaginationJPA( - (Specification specification, Pageable pageable) -> this.documentRepository.findAll( - findGrantedFor(user.getId()).and(specification), - pageable - ), - searchPaginationInput, - Document.class - ).map(RawPaginationDocument::new); + if (!input.getScenarioIds().isEmpty()) { + document.setScenarios(iterableToSet(scenarioRepository.findAllById(input.getScenarioIds()))); } - } - - @GetMapping("/api/documents/{documentId}") - public Document document(@PathVariable String documentId) { - return resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); - } - - @GetMapping("/api/documents/{documentId}/tags") - public Set documentTags(@PathVariable String documentId) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); - return document.getTags(); - } - - @PutMapping("/api/documents/{documentId}/tags") - public Document documentTags(@PathVariable String documentId, @RequestBody DocumentTagUpdateInput input) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); + document.setType(file.getContentType()); return documentRepository.save(document); - } - - @Transactional(rollbackOn = Exception.class) - @PutMapping("/api/documents/{documentId}") - public Document updateDocumentInformation(@PathVariable String documentId, - @Valid @RequestBody DocumentUpdateInput input) { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); - document.setUpdateAttributes(input); - document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); - - // Get removed exercises - Stream askExerciseIdsStream = document.getExercises() - .stream() - .filter(exercise -> !exercise.isUserHasAccess(userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new))) - .map(Exercise::getId); - List askExerciseIds = Stream.concat(askExerciseIdsStream, input.getExerciseIds().stream()).distinct().toList(); - List removedExercises = document.getExercises().stream() - .filter(exercise -> !askExerciseIds.contains(exercise.getId())).toList(); - document.setExercises(iterableToSet(exerciseRepository.findAllById(askExerciseIds))); - // In case of exercise removal, all inject doc attachment for exercise - removedExercises.forEach(exercise -> injectService.cleanInjectsDocExercise(exercise.getId(), documentId)); - - // Get removed scenarios - Stream askScenarioIdsStream = document.getScenarios().stream() - .filter(scenario -> !scenario.isUserHasAccess(userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new))) - .map(Scenario::getId); - List askScenarioIds = Stream.concat(askScenarioIdsStream, input.getScenarioIds().stream()).distinct().toList(); - List removedScenarios = document.getScenarios().stream() - .filter(scenario -> !askScenarioIds.contains(scenario.getId())).toList(); - document.setScenarios(iterableToSet(scenarioRepository.findAllById(askScenarioIds))); - // In case of scenario removal, all inject doc attachment for scenario - removedScenarios.forEach(scenario -> injectService.cleanInjectsDocScenario(scenario.getId(), documentId)); - - // Save and return - return documentRepository.save(document); - } - - @GetMapping("/api/documents/{documentId}/file") - public void downloadDocument(@PathVariable String documentId, HttpServletResponse response) throws IOException { - Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); - response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + document.getName()); - response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); - response.setStatus(HttpServletResponse.SC_OK); - try (InputStream fileStream = fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { - fileStream.transferTo(response.getOutputStream()); - } - } - - @GetMapping(value = "/api/images/injectors/{injectorType}", produces = MediaType.IMAGE_PNG_VALUE) - public @ResponseBody ResponseEntity getInjectorImage(@PathVariable String injectorType) throws IOException { - Optional fileStream = fileService.getInjectorImage(injectorType); - if (fileStream.isPresent()) { - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) - .body(IOUtils.toByteArray(fileStream.get())); - } - return null; - } - - @GetMapping(value = "/api/images/injectors/id/{injectorId}", produces = MediaType.IMAGE_PNG_VALUE) - public @ResponseBody ResponseEntity getInjectorImageFromId(@PathVariable String injectorId) throws IOException { - Injector injector = this.injectorRepository.findById(injectorId).orElseThrow(ElementNotFoundException::new); - Optional fileStream = fileService.getInjectorImage(injector.getType()); - if (fileStream.isPresent()) { - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) - .body(IOUtils.toByteArray(fileStream.get())); - } - return null; - } - - @GetMapping(value = "/api/images/collectors/{collectorType}", produces = MediaType.IMAGE_PNG_VALUE) - public @ResponseBody ResponseEntity getCollectorImage(@PathVariable String collectorType) throws IOException { - Optional fileStream = fileService.getCollectorImage(collectorType); - if (fileStream.isPresent()) { - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) - .body(IOUtils.toByteArray(fileStream.get())); - } - return null; - } - - public void downloadCollectorImage(@PathVariable String collectorType, HttpServletResponse response) throws IOException { - response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + collectorType + ".png"); - response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE); - response.setStatus(HttpServletResponse.SC_OK); - try (InputStream fileStream = fileService.getCollectorImage(collectorType).orElseThrow(ElementNotFoundException::new)) { - fileStream.transferTo(response.getOutputStream()); - } - } - - @GetMapping(value = "/api/images/collectors/id/{collectorId}", produces = MediaType.IMAGE_PNG_VALUE) - public @ResponseBody ResponseEntity getCollectorImageFromId(@PathVariable String collectorId) throws IOException { - Collector collector = this.collectorRepository.findById(collectorId).orElseThrow(ElementNotFoundException::new); - Optional fileStream = fileService.getCollectorImage(collector.getType()); - if (fileStream.isPresent()) { - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) - .body(IOUtils.toByteArray(fileStream.get())); - } - return null; - } - - @GetMapping(value = "/api/images/security_platforms/id/{assetId}/{theme}") - public void getSecurityPlatformImageFromId(@PathVariable String assetId, @PathVariable String theme, HttpServletResponse response) throws IOException { - SecurityPlatform securityPlatform = this.securityPlatformRepository.findById(assetId).orElseThrow(ElementNotFoundException::new); - if( theme.equals("dark") ) { - if( securityPlatform.getLogoDark() != null ) { - downloadDocument(securityPlatform.getLogoDark().getId(), response); - } - } - if( securityPlatform.getLogoLight() != null ) { - downloadDocument(securityPlatform.getLogoLight().getId(), response); - } - downloadCollectorImage("openbas_fake_detector", response); - } - - @GetMapping(value = "/api/images/executors/{executorId}", produces = MediaType.IMAGE_PNG_VALUE) - public @ResponseBody ResponseEntity getExecutorImage(@PathVariable String executorId) throws IOException { - Optional fileStream = fileService.getExecutorImage(executorId); - if (fileStream.isPresent()) { - return ResponseEntity.ok() - .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) - .body(IOUtils.toByteArray(fileStream.get())); - } - return null; - } - - private List getExercisePlayerDocuments(Exercise exercise) { - List
articles = exercise.getArticles(); - List injects = exercise.getInjects(); - return getPlayerDocuments(articles, injects); - } - - private List getScenarioPlayerDocuments(Scenario scenario) { - List
articles = scenario.getArticles(); - List injects = scenario.getInjects(); - return getPlayerDocuments(articles, injects); - } - - private List getPlayerDocuments(List
articles, List injects) { - Stream channelsDocs = articles.stream() - .map(Article::getChannel) - .flatMap(channel -> channel.getLogos().stream()); - Stream articlesDocs = articles.stream() - .flatMap(article -> article.getDocuments().stream()); - List challenges = injects.stream() - .filter(inject -> inject.getInjectorContract() - .map(contract -> contract.getId().equals(CHALLENGE_PUBLISH)) - .orElse(false)) - .filter(inject -> inject.getContent() != null) - .flatMap(inject -> { - try { - ChallengeContent content = mapper.treeToValue(inject.getContent(), ChallengeContent.class); - return content.getChallenges().stream(); - } catch (JsonProcessingException e) { - return Stream.empty(); - } - }) - .toList(); - Stream challengesDocs = fromIterable(challengeRepository.findAllById(challenges)).stream() - .flatMap(challenge -> challenge.getDocuments().stream()); - return Stream.of(channelsDocs, articlesDocs, challengesDocs).flatMap(documentStream -> documentStream).distinct() - .toList(); - } - - @Transactional(rollbackOn = Exception.class) - @DeleteMapping("/api/documents/{documentId}") - public void deleteDocument(@PathVariable String documentId) { - injectDocumentRepository.deleteDocumentFromAllReferences(documentId); - List documents = documentRepository.removeById(documentId); - documents.forEach(document -> { - try { - fileService.deleteFile(document.getTarget()); - } catch (Exception e) { - // Fail no longer available in the storage. - } - }); - } - - // -- EXERCISE & SENARIO-- - - @GetMapping("/api/player/{exerciseOrScenarioId}/documents") - public List playerDocuments(@PathVariable String exerciseOrScenarioId, @RequestParam Optional userId) { - Optional exerciseOpt = this.exerciseRepository.findById(exerciseOrScenarioId); - Optional scenarioOpt = this.scenarioRepository.findById(exerciseOrScenarioId); - - final User user = impersonateUser(userRepository, userId); - if (user.getId().equals(ANONYMOUS)) { - throw new UnsupportedOperationException("User must be logged or dynamic player is required"); - } - - if (exerciseOpt.isPresent()) { - if (!exerciseOpt.get().isUserHasAccess(user) && !exerciseOpt.get().getUsers().contains(user)) { - throw new UnsupportedOperationException("The given player is not in this exercise"); - } - return getExercisePlayerDocuments(exerciseOpt.get()); - } else if (scenarioOpt.isPresent()) { - if (!scenarioOpt.get().isUserHasAccess(user) && !scenarioOpt.get().getUsers().contains(user)) { - throw new UnsupportedOperationException("The given player is not in this exercise"); - } - return getScenarioPlayerDocuments(scenarioOpt.get()); - } else { - throw new IllegalArgumentException("Exercise or scenario ID not found"); - } - } - - @GetMapping("/api/player/{exerciseOrScenarioId}/documents/{documentId}/file") - public void downloadPlayerDocument( - @PathVariable String exerciseOrScenarioId, - @PathVariable String documentId, - @RequestParam Optional userId, HttpServletResponse response) throws IOException { - Optional exerciseOpt = this.exerciseRepository.findById(exerciseOrScenarioId); - Optional scenarioOpt = this.scenarioRepository.findById(exerciseOrScenarioId); - - final User user = impersonateUser(userRepository, userId); - if (user.getId().equals(ANONYMOUS)) { - throw new UnsupportedOperationException("User must be logged or dynamic player is required"); - } - - Document document = null; - if (exerciseOpt.isPresent()) { - if (!exerciseOpt.get().isUserHasAccess(user) && !exerciseOpt.get().getUsers().contains(user)) { - throw new UnsupportedOperationException("The given player is not in this exercise"); - } - document = getExercisePlayerDocuments(exerciseOpt.get()) - .stream() - .filter(doc -> doc.getId().equals(documentId)) - .findFirst() - .orElseThrow(ElementNotFoundException::new); - } else if (scenarioOpt.isPresent()) { - if (!scenarioOpt.get().isUserHasAccess(user) && !scenarioOpt.get().getUsers().contains(user)) { - throw new UnsupportedOperationException("The given player is not in this exercise"); - } - document = getScenarioPlayerDocuments(scenarioOpt.get()) - .stream() - .filter(doc -> doc.getId().equals(documentId)) - .findFirst() - .orElseThrow(ElementNotFoundException::new); - } - - if (document != null) { - response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + document.getName()); - response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); - response.setStatus(HttpServletResponse.SC_OK); - try (InputStream fileStream = fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { - fileStream.transferTo(response.getOutputStream()); - } - } - } + } + } + } + + @GetMapping("/api/documents") + public List documents() { + OpenBASPrincipal user = currentUser(); + if (user.isAdmin()) { + return documentRepository.rawAllDocuments(); + } else { + return documentRepository.rawAllDocumentsByAccessLevel(user.getId()); + } + } + + @PostMapping("/api/documents/search") + public Page searchDocuments( + @RequestBody @Valid final SearchPaginationInput searchPaginationInput) { + OpenBASPrincipal user = currentUser(); + List securityPlatformLogos = securityPlatformRepository.securityPlatformLogo(); + if (user.isAdmin()) { + return buildPaginationJPA( + (Specification specification, Pageable pageable) -> this.documentRepository.findAll( + specification, pageable), + searchPaginationInput, + Document.class + ).map((document) -> { + var rawPaginationDocument = new RawPaginationDocument(document); + rawPaginationDocument.setDocument_can_be_deleted(!securityPlatformLogos.contains(document)); + return rawPaginationDocument; + }); + } else { + return buildPaginationJPA( + (Specification specification, Pageable pageable) -> this.documentRepository.findAll( + findGrantedFor(user.getId()).and(specification), + pageable + ), + searchPaginationInput, + Document.class + ).map((document) -> { + var rawPaginationDocument = new RawPaginationDocument(document); + rawPaginationDocument.setDocument_can_be_deleted(!securityPlatformLogos.contains(document)); + return rawPaginationDocument; + }); + } + } + + @GetMapping("/api/documents/{documentId}") + public Document document(@PathVariable String documentId) { + return resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + } + + @GetMapping("/api/documents/{documentId}/tags") + public Set documentTags(@PathVariable String documentId) { + Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + return document.getTags(); + } + + @PutMapping("/api/documents/{documentId}/tags") + public Document documentTags(@PathVariable String documentId, @RequestBody DocumentTagUpdateInput input) { + Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); + return documentRepository.save(document); + } + + @Transactional(rollbackOn = Exception.class) + @PutMapping("/api/documents/{documentId}") + public Document updateDocumentInformation(@PathVariable String documentId, + @Valid @RequestBody DocumentUpdateInput input) { + Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + document.setUpdateAttributes(input); + document.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); + + // Get removed exercises + Stream askExerciseIdsStream = document.getExercises() + .stream() + .filter(exercise -> !exercise.isUserHasAccess( + userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new))) + .map(Exercise::getId); + List askExerciseIds = Stream.concat(askExerciseIdsStream, input.getExerciseIds().stream()).distinct() + .toList(); + List removedExercises = document.getExercises().stream() + .filter(exercise -> !askExerciseIds.contains(exercise.getId())).toList(); + document.setExercises(iterableToSet(exerciseRepository.findAllById(askExerciseIds))); + // In case of exercise removal, all inject doc attachment for exercise + removedExercises.forEach(exercise -> injectService.cleanInjectsDocExercise(exercise.getId(), documentId)); + + // Get removed scenarios + Stream askScenarioIdsStream = document.getScenarios().stream() + .filter(scenario -> !scenario.isUserHasAccess( + userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new))) + .map(Scenario::getId); + List askScenarioIds = Stream.concat(askScenarioIdsStream, input.getScenarioIds().stream()).distinct() + .toList(); + List removedScenarios = document.getScenarios().stream() + .filter(scenario -> !askScenarioIds.contains(scenario.getId())).toList(); + document.setScenarios(iterableToSet(scenarioRepository.findAllById(askScenarioIds))); + // In case of scenario removal, all inject doc attachment for scenario + removedScenarios.forEach(scenario -> injectService.cleanInjectsDocScenario(scenario.getId(), documentId)); + + // Save and return + return documentRepository.save(document); + } + + @GetMapping("/api/documents/{documentId}/file") + public void downloadDocument(@PathVariable String documentId, HttpServletResponse response) throws IOException { + Document document = resolveDocument(documentId).orElseThrow(ElementNotFoundException::new); + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + document.getName()); + response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); + response.setStatus(HttpServletResponse.SC_OK); + try (InputStream fileStream = fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { + fileStream.transferTo(response.getOutputStream()); + } + } + + @GetMapping(value = "/api/images/injectors/{injectorType}", produces = MediaType.IMAGE_PNG_VALUE) + public @ResponseBody ResponseEntity getInjectorImage(@PathVariable String injectorType) throws IOException { + Optional fileStream = fileService.getInjectorImage(injectorType); + if (fileStream.isPresent()) { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) + .body(IOUtils.toByteArray(fileStream.get())); + } + return null; + } + + @GetMapping(value = "/api/images/injectors/id/{injectorId}", produces = MediaType.IMAGE_PNG_VALUE) + public @ResponseBody ResponseEntity getInjectorImageFromId(@PathVariable String injectorId) + throws IOException { + Injector injector = this.injectorRepository.findById(injectorId).orElseThrow(ElementNotFoundException::new); + Optional fileStream = fileService.getInjectorImage(injector.getType()); + if (fileStream.isPresent()) { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) + .body(IOUtils.toByteArray(fileStream.get())); + } + return null; + } + + @GetMapping(value = "/api/images/collectors/{collectorType}", produces = MediaType.IMAGE_PNG_VALUE) + public @ResponseBody ResponseEntity getCollectorImage(@PathVariable String collectorType) throws IOException { + Optional fileStream = fileService.getCollectorImage(collectorType); + if (fileStream.isPresent()) { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) + .body(IOUtils.toByteArray(fileStream.get())); + } + return null; + } + + public void downloadCollectorImage(@PathVariable String collectorType, HttpServletResponse response) + throws IOException { + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + collectorType + ".png"); + response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE); + response.setStatus(HttpServletResponse.SC_OK); + try (InputStream fileStream = fileService.getCollectorImage(collectorType) + .orElseThrow(ElementNotFoundException::new)) { + fileStream.transferTo(response.getOutputStream()); + } + } + + @GetMapping(value = "/api/images/collectors/id/{collectorId}", produces = MediaType.IMAGE_PNG_VALUE) + public @ResponseBody ResponseEntity getCollectorImageFromId(@PathVariable String collectorId) + throws IOException { + Collector collector = this.collectorRepository.findById(collectorId).orElseThrow(ElementNotFoundException::new); + Optional fileStream = fileService.getCollectorImage(collector.getType()); + if (fileStream.isPresent()) { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) + .body(IOUtils.toByteArray(fileStream.get())); + } + return null; + } + + @GetMapping(value = "/api/images/security_platforms/id/{assetId}/{theme}") + public void getSecurityPlatformImageFromId(@PathVariable String assetId, @PathVariable String theme, + HttpServletResponse response) throws IOException { + SecurityPlatform securityPlatform = this.securityPlatformRepository.findById(assetId) + .orElseThrow(ElementNotFoundException::new); + if (theme.equals("dark") && securityPlatform.getLogoDark() != null) { + downloadDocument(securityPlatform.getLogoDark().getId(), response); + } else if (securityPlatform.getLogoLight() != null) { + downloadDocument(securityPlatform.getLogoLight().getId(), response); + } else { + downloadCollectorImage("openbas_fake_detector", response); + } + } + + @GetMapping(value = "/api/images/executors/{executorId}", produces = MediaType.IMAGE_PNG_VALUE) + public @ResponseBody ResponseEntity getExecutorImage(@PathVariable String executorId) throws IOException { + Optional fileStream = fileService.getExecutorImage(executorId); + if (fileStream.isPresent()) { + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES)) + .body(IOUtils.toByteArray(fileStream.get())); + } + return null; + } + + private List getExercisePlayerDocuments(Exercise exercise) { + List
articles = exercise.getArticles(); + List injects = exercise.getInjects(); + return getPlayerDocuments(articles, injects); + } + + private List getScenarioPlayerDocuments(Scenario scenario) { + List
articles = scenario.getArticles(); + List injects = scenario.getInjects(); + return getPlayerDocuments(articles, injects); + } + + private List getPlayerDocuments(List
articles, List injects) { + Stream channelsDocs = articles.stream() + .map(Article::getChannel) + .flatMap(channel -> channel.getLogos().stream()); + Stream articlesDocs = articles.stream() + .flatMap(article -> article.getDocuments().stream()); + List challenges = injects.stream() + .filter(inject -> inject.getInjectorContract() + .map(contract -> contract.getId().equals(CHALLENGE_PUBLISH)) + .orElse(false)) + .filter(inject -> inject.getContent() != null) + .flatMap(inject -> { + try { + ChallengeContent content = mapper.treeToValue(inject.getContent(), ChallengeContent.class); + return content.getChallenges().stream(); + } catch (JsonProcessingException e) { + return Stream.empty(); + } + }) + .toList(); + Stream challengesDocs = fromIterable(challengeRepository.findAllById(challenges)).stream() + .flatMap(challenge -> challenge.getDocuments().stream()); + return Stream.of(channelsDocs, articlesDocs, challengesDocs).flatMap(documentStream -> documentStream).distinct() + .toList(); + } + + @Transactional(rollbackOn = Exception.class) + @DeleteMapping("/api/documents/{documentId}") + public void deleteDocument(@PathVariable String documentId) { + injectDocumentRepository.deleteDocumentFromAllReferences(documentId); + List documents = documentRepository.removeById(documentId); + documents.forEach(document -> { + try { + fileService.deleteFile(document.getTarget()); + } catch (Exception e) { + // Fail no longer available in the storage. + } + }); + } + + // -- EXERCISE & SENARIO-- + + @GetMapping("/api/player/{exerciseOrScenarioId}/documents") + public List playerDocuments(@PathVariable String exerciseOrScenarioId, + @RequestParam Optional userId) { + Optional exerciseOpt = this.exerciseRepository.findById(exerciseOrScenarioId); + Optional scenarioOpt = this.scenarioRepository.findById(exerciseOrScenarioId); + + final User user = impersonateUser(userRepository, userId); + if (user.getId().equals(ANONYMOUS)) { + throw new UnsupportedOperationException("User must be logged or dynamic player is required"); + } + + if (exerciseOpt.isPresent()) { + if (!exerciseOpt.get().isUserHasAccess(user) && !exerciseOpt.get().getUsers().contains(user)) { + throw new UnsupportedOperationException("The given player is not in this exercise"); + } + return getExercisePlayerDocuments(exerciseOpt.get()); + } else if (scenarioOpt.isPresent()) { + if (!scenarioOpt.get().isUserHasAccess(user) && !scenarioOpt.get().getUsers().contains(user)) { + throw new UnsupportedOperationException("The given player is not in this exercise"); + } + return getScenarioPlayerDocuments(scenarioOpt.get()); + } else { + throw new IllegalArgumentException("Exercise or scenario ID not found"); + } + } + + @GetMapping("/api/player/{exerciseOrScenarioId}/documents/{documentId}/file") + public void downloadPlayerDocument( + @PathVariable String exerciseOrScenarioId, + @PathVariable String documentId, + @RequestParam Optional userId, HttpServletResponse response) throws IOException { + Optional exerciseOpt = this.exerciseRepository.findById(exerciseOrScenarioId); + Optional scenarioOpt = this.scenarioRepository.findById(exerciseOrScenarioId); + + final User user = impersonateUser(userRepository, userId); + if (user.getId().equals(ANONYMOUS)) { + throw new UnsupportedOperationException("User must be logged or dynamic player is required"); + } + + Document document = null; + if (exerciseOpt.isPresent()) { + if (!exerciseOpt.get().isUserHasAccess(user) && !exerciseOpt.get().getUsers().contains(user)) { + throw new UnsupportedOperationException("The given player is not in this exercise"); + } + document = getExercisePlayerDocuments(exerciseOpt.get()) + .stream() + .filter(doc -> doc.getId().equals(documentId)) + .findFirst() + .orElseThrow(ElementNotFoundException::new); + } else if (scenarioOpt.isPresent()) { + if (!scenarioOpt.get().isUserHasAccess(user) && !scenarioOpt.get().getUsers().contains(user)) { + throw new UnsupportedOperationException("The given player is not in this exercise"); + } + document = getScenarioPlayerDocuments(scenarioOpt.get()) + .stream() + .filter(doc -> doc.getId().equals(documentId)) + .findFirst() + .orElseThrow(ElementNotFoundException::new); + } + + if (document != null) { + response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + document.getName()); + response.addHeader(HttpHeaders.CONTENT_TYPE, document.getType()); + response.setStatus(HttpServletResponse.SC_OK); + try (InputStream fileStream = fileService.getFile(document).orElseThrow(ElementNotFoundException::new)) { + fileStream.transferTo(response.getOutputStream()); + } + } + } } 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 690e765168..ef1acdf524 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 @@ -41,6 +41,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -297,7 +298,20 @@ public Inject createInjectForExercise(@PathVariable String exerciseId, @Valid @R inject.setUser(userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new)); inject.setExercise(exercise); // Set dependencies - inject.setDependsOn(resolveOptionalRelation(input.getDependsOn(), injectRepository)); + if(input.getDependsOn() != null) { + inject.getDependsOn().addAll( + input.getDependsOn() + .stream() + .map(injectDependencyInput -> { + InjectDependency dependency = new InjectDependency(); + dependency.setInjectDependencyCondition(injectDependencyInput.getConditions()); + dependency.setCompositeId(new InjectDependencyId()); + dependency.getCompositeId().setInjectChildren(inject); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getRelationship().getInjectParentId()).orElse(null)); + return dependency; + }).toList() + ); + } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(assetGroupService.assetGroups(input.getAssetGroups()))); @@ -467,7 +481,20 @@ public Inject createInjectForScenario( inject.setUser(this.userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new)); inject.setScenario(scenario); // Set dependencies - inject.setDependsOn(resolveOptionalRelation(input.getDependsOn(), this.injectRepository)); + if(input.getDependsOn() != null) { + inject.getDependsOn().addAll( + input.getDependsOn() + .stream() + .map(injectDependencyInput -> { + InjectDependency dependency = new InjectDependency(); + dependency.setInjectDependencyCondition(injectDependencyInput.getConditions()); + dependency.setCompositeId(new InjectDependencyId()); + dependency.getCompositeId().setInjectChildren(inject); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getRelationship().getInjectParentId()).orElse(null)); + return dependency; + }).toList() + ); + } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(assetGroupService.assetGroups(input.getAssetGroups()))); @@ -575,7 +602,42 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu inject.setUpdateAttributes(input); // Set dependencies - inject.setDependsOn(updateRelation(input.getDependsOn(), inject.getDependsOn(), this.injectRepository)); + if(input.getDependsOn() != null) { + input.getDependsOn().forEach(entry -> { + Optional existingDependency = inject.getDependsOn().stream() + .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getRelationship().getInjectParentId())) + .findFirst(); + if(existingDependency.isPresent()) { + existingDependency.get().getInjectDependencyCondition().setConditions(entry.getConditions().getConditions()); + existingDependency.get().getInjectDependencyCondition().setMode(entry.getConditions().getMode()); + } else { + InjectDependency injectDependency = new InjectDependency(); + injectDependency.getCompositeId().setInjectChildren(inject); + injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getRelationship().getInjectParentId()).orElse(null)); + injectDependency.setInjectDependencyCondition(new InjectDependencyConditions.InjectDependencyCondition()); + injectDependency.getInjectDependencyCondition().setConditions(entry.getConditions().getConditions()); + injectDependency.getInjectDependencyCondition().setMode(entry.getConditions().getMode()); + inject.getDependsOn().add(injectDependency); + } + }); + } + + List injectDepencyToRemove = new ArrayList<>(); + if(inject.getDependsOn() != null && !inject.getDependsOn().isEmpty()) { + if (input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { + inject.getDependsOn().forEach( + injectDependency -> { + if (!input.getDependsOn().stream().map((injectDependencyInput -> injectDependencyInput.getRelationship().getInjectParentId())).toList().contains(injectDependency.getCompositeId().getInjectParent().getId())) { + injectDepencyToRemove.add(injectDependency); + } + } + ); + } else { + injectDepencyToRemove.addAll(inject.getDependsOn()); + } + inject.getDependsOn().removeAll(injectDepencyToRemove); + } + inject.setTeams(fromIterable(this.teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(this.assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(this.assetGroupService.assetGroups(input.getAssetGroups()))); diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java new file mode 100644 index 0000000000..3103102bc3 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java @@ -0,0 +1,25 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectDependencyConditions; +import io.openbas.database.model.InjectDependencyId; +import io.openbas.helper.MonoIdDeserializer; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class InjectDependencyIdInput { + + @JsonProperty("inject_parent_id") + private String injectParentId; + + @JsonProperty("inject_children_id") + private String injectChildrenId; + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java new file mode 100644 index 0000000000..8ce475cb98 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java @@ -0,0 +1,22 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.database.model.*; +import jakarta.persistence.EmbeddedId; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +public class InjectDependencyInput { + + @JsonProperty("dependency_relationship") + private InjectDependencyIdInput relationship; + + @JsonProperty("dependency_condition") + private InjectDependencyConditions.InjectDependencyCondition conditions; + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java index 8571fc4003..ad663f90f1 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectDependency; import io.openbas.database.model.InjectorContract; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -10,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; @Setter @Getter @@ -28,7 +30,7 @@ public class InjectInput { private ObjectNode content; @JsonProperty("inject_depends_on") - private String dependsOn; + private List dependsOn = new ArrayList<>(); @JsonProperty("inject_depends_duration") private Long dependsDuration; diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index 38278ae5df..8f02397141 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.openbas.database.model.InjectDependency; import io.openbas.database.model.InjectorContract; import io.openbas.helper.InjectModelHelper; import io.openbas.injectors.email.EmailContract; @@ -12,6 +13,8 @@ import lombok.Data; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Data public class InjectOutput { @@ -39,7 +42,7 @@ public class InjectOutput { private Long dependsDuration; @JsonProperty("inject_depends_on") - private String dependsOn; + private List dependsOn; @JsonProperty("inject_injector_contract") private InjectorContract injectorContract; @@ -79,20 +82,19 @@ public InjectOutput( String exerciseId, String scenarioId, Long dependsDuration, - String dependsOn, InjectorContract injectorContract, String[] tags, String[] teams, String[] assets, String[] assetGroups, - String injectType) { + String injectType, + InjectDependency injectDependency) { this.id = id; this.title = title; this.enabled = enabled; this.exercise = exerciseId; this.scenario = scenarioId; this.dependsDuration = dependsDuration; - this.dependsOn = dependsOn; this.injectorContract = injectorContract; this.tags = tags != null ? new HashSet<>(Arrays.asList(tags)) : new HashSet<>(); @@ -111,5 +113,10 @@ public InjectOutput( this.injectType = injectType; this.teams = teams != null ? new ArrayList<>(Arrays.asList(teams)) : new ArrayList<>(); this.content = content; + + if (injectDependency != null) { + this.dependsOn = List.of(injectDependency); + } + } } diff --git a/openbas-api/src/main/java/io/openbas/rest/team/AtomicTestingTeamApi.java b/openbas-api/src/main/java/io/openbas/rest/team/AtomicTestingTeamApi.java new file mode 100644 index 0000000000..b6419cc853 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/team/AtomicTestingTeamApi.java @@ -0,0 +1,38 @@ +package io.openbas.rest.team; + +import io.openbas.database.model.Team; +import io.openbas.rest.helper.RestBehavior; +import io.openbas.rest.team.output.TeamOutput; +import io.openbas.service.TeamService; +import io.openbas.telemetry.Tracing; +import io.openbas.utils.pagination.SearchPaginationInput; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static io.openbas.database.model.User.ROLE_USER; +import static io.openbas.database.specification.TeamSpecification.contextual; +import static io.openbas.rest.atomic_testing.AtomicTestingApi.ATOMIC_TESTING_URI; + +@RequiredArgsConstructor +@RestController +@Secured(ROLE_USER) +public class AtomicTestingTeamApi extends RestBehavior { + + private final TeamService teamService; + + @PostMapping(ATOMIC_TESTING_URI + "/teams/search") + @Transactional(readOnly = true) + @Tracing(name = "Paginate teams for atomic testings", layer = "api", operation = "POST") + public Page searchTeams(@RequestBody @Valid SearchPaginationInput searchPaginationInput) { + final Specification teamSpecification = contextual(false); + return this.teamService.teamPagination(searchPaginationInput, teamSpecification); + } + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/team/ExerciseTeamApi.java b/openbas-api/src/main/java/io/openbas/rest/team/ExerciseTeamApi.java index 87d6d3ba4f..1b4c780085 100644 --- a/openbas-api/src/main/java/io/openbas/rest/team/ExerciseTeamApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/team/ExerciseTeamApi.java @@ -14,10 +14,7 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static io.openbas.database.model.User.ROLE_USER; import static io.openbas.database.specification.TeamSpecification.contextual; @@ -37,8 +34,14 @@ public class ExerciseTeamApi extends RestBehavior { @Tracing(name = "Paginate teams for exercise", layer = "api", operation = "POST") public Page searchTeams( @PathVariable @NotBlank final String exerciseId, - @RequestBody @Valid SearchPaginationInput searchPaginationInput) { - final Specification teamSpecification = contextual(false).or(fromExercise(exerciseId).and(contextual(true))); + @RequestBody @Valid SearchPaginationInput searchPaginationInput, + @RequestParam final boolean contextualOnly) { + Specification teamSpecification; + if (!contextualOnly) { + teamSpecification = contextual(false).or(fromExercise(exerciseId).and(contextual(true))); + } else { + teamSpecification = fromExercise(exerciseId); + } return this.teamService.teamPagination(searchPaginationInput, teamSpecification); } diff --git a/openbas-api/src/main/java/io/openbas/rest/team/ScenarioTeamApi.java b/openbas-api/src/main/java/io/openbas/rest/team/ScenarioTeamApi.java index 6a3a06c478..ae9f672445 100644 --- a/openbas-api/src/main/java/io/openbas/rest/team/ScenarioTeamApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/team/ScenarioTeamApi.java @@ -14,14 +14,10 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static io.openbas.database.model.User.ROLE_USER; -import static io.openbas.database.specification.TeamSpecification.contextual; -import static io.openbas.database.specification.TeamSpecification.fromScenario; +import static io.openbas.database.specification.TeamSpecification.*; import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI; @RequiredArgsConstructor @@ -37,8 +33,14 @@ public class ScenarioTeamApi extends RestBehavior { @Tracing(name = "Paginate teams for scenario", layer = "api", operation = "POST") public Page teams( @PathVariable @NotBlank final String scenarioId, - @RequestBody @Valid SearchPaginationInput searchPaginationInput) { - final Specification teamSpecification = contextual(false).or(fromScenario(scenarioId).and(contextual(true))); + @RequestBody @Valid SearchPaginationInput searchPaginationInput, + @RequestParam final boolean contextualOnly) { + Specification teamSpecification; + if (!contextualOnly) { + teamSpecification = contextual(false).or(fromScenario(scenarioId).and(contextual(true))); + } else { + teamSpecification = fromScenario(scenarioId); + } return this.teamService.teamPagination(searchPaginationInput, teamSpecification); } diff --git a/openbas-api/src/main/java/io/openbas/rest/team/TeamApi.java b/openbas-api/src/main/java/io/openbas/rest/team/TeamApi.java index 9d8bee886f..12ebd44564 100644 --- a/openbas-api/src/main/java/io/openbas/rest/team/TeamApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/team/TeamApi.java @@ -17,7 +17,6 @@ import io.openbas.rest.team.form.UpdateUsersTeamInput; import io.openbas.rest.team.output.TeamOutput; import io.openbas.service.TeamService; -import io.openbas.telemetry.Tracing; import io.openbas.utils.pagination.SearchPaginationInput; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -34,7 +33,8 @@ import static io.openbas.config.SessionHelper.currentUser; import static io.openbas.database.model.User.ROLE_USER; -import static io.openbas.database.specification.TeamSpecification.*; +import static io.openbas.database.specification.TeamSpecification.contextual; +import static io.openbas.database.specification.TeamSpecification.fromIds; import static io.openbas.helper.DatabaseHelper.updateRelation; import static io.openbas.helper.StreamHelper.fromIterable; import static io.openbas.helper.StreamHelper.iterableToSet; @@ -83,7 +83,6 @@ public Iterable getTeams() { @PostMapping("/api/teams/search") @PreAuthorize("isObserver()") @Transactional(readOnly = true) - @Tracing(name = "Paginate teams", layer = "api", operation = "POST") public Page searchTeams(@RequestBody @Valid SearchPaginationInput searchPaginationInput) { final Specification teamSpecification = contextual(false); return this.teamService.teamPagination(searchPaginationInput, teamSpecification); @@ -92,7 +91,7 @@ public Page searchTeams(@RequestBody @Valid SearchPaginationInput se @PostMapping("/api/teams/find") @PreAuthorize("isObserver()") @Transactional(readOnly = true) - @Tracing(name = "Find teams", layer = "api", operation = "POST") + @LogExecutionTime public List findTeams(@RequestBody @Valid @NotNull final List teamIds) { return this.teamService.find(fromIds(teamIds)); } diff --git a/openbas-api/src/main/java/io/openbas/rest/team/TeamQueryHelper.java b/openbas-api/src/main/java/io/openbas/rest/team/TeamQueryHelper.java index 4c2e762d7b..b2fc6a0803 100644 --- a/openbas-api/src/main/java/io/openbas/rest/team/TeamQueryHelper.java +++ b/openbas-api/src/main/java/io/openbas/rest/team/TeamQueryHelper.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import static io.openbas.utils.JpaUtils.createJoinArrayAggOnId; +import static io.openbas.utils.JpaUtils.createLeftJoin; public class TeamQueryHelper { @@ -29,6 +30,7 @@ public static void select(CriteriaBuilder cb, CriteriaQuery cq, Root tagIdsExpression = createJoinArrayAggOnId(cb, teamRoot, "tags"); Expression userIdsExpression = createJoinArrayAggOnId(cb, teamRoot, "users"); + Expression organizationIdExpression = createLeftJoin(teamRoot, "organization").get("id"); // Multiselect cq.multiselect( @@ -38,7 +40,8 @@ public static void select(CriteriaBuilder cb, CriteriaQuery cq, Root execution(TypedQuery query) { .updatedAt(tuple.get("team_updated_at", Instant.class)) .tags(Arrays.stream(tuple.get("team_tags", String[].class)).collect(Collectors.toSet())) .users(Arrays.stream(tuple.get("team_users", String[].class)).collect(Collectors.toSet())) + .organization(tuple.get("team_organization", String.class)) .build()) .toList(); } diff --git a/openbas-api/src/main/java/io/openbas/rest/team/output/TeamOutput.java b/openbas-api/src/main/java/io/openbas/rest/team/output/TeamOutput.java index c532706d45..4df9f2e1e5 100644 --- a/openbas-api/src/main/java/io/openbas/rest/team/output/TeamOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/team/output/TeamOutput.java @@ -33,6 +33,9 @@ public class TeamOutput { @JsonProperty("team_users") private Set users; + @JsonProperty("team_organization") + private String organization; + @JsonProperty("team_updated_at") @NotNull private Instant updatedAt; diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 784c17e599..01431f6e31 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -12,17 +12,21 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.stereotype.Component; import java.time.Instant; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -47,10 +51,14 @@ public class InjectsExecutionJob implements Job { private final QueueService queueService; private final ExecutionExecutorService executionExecutorService; private final AtomicTestingService atomicTestingService; + private final InjectDependenciesRepository injectDependenciesRepository; + private final InjectExpectationRepository injectExpectationRepository; private final List executionStatusesNotReady = List.of(ExecutionStatus.QUEUING, ExecutionStatus.DRAFT, ExecutionStatus.EXECUTING, ExecutionStatus.PENDING); + private final List expectationStatusesSuccess = List.of(InjectExpectation.EXPECTATION_STATUS.SUCCESS); + @Resource protected ObjectMapper mapper; @@ -148,81 +156,193 @@ private void executeInject(ExecutableInject executableInject) { Inject inject = executableInject.getInjection().getInject(); // We are now checking if we depend on another inject and if it did not failed - if (inject.getDependsOn() != null - && inject.getDependsOn().getStatus().isPresent() - && ( inject.getDependsOn().getStatus().get().getName().equals(ExecutionStatus.ERROR) - || executionStatusesNotReady.contains(inject.getDependsOn().getStatus().get().getName()))) { + Optional> errorMessages = null; + if(executableInject.getExercise() != null) { + errorMessages = getErrorMessagesPreExecution(executableInject.getExercise().getId(), inject); + } + if (errorMessages != null && errorMessages.isPresent()) { InjectStatus status = new InjectStatus(); if (inject.getStatus().isEmpty()) { status.setInject(inject); } else { status = inject.getStatus().get(); } - String errorMsg = inject.getDependsOn().getStatus().get().getName().equals(ExecutionStatus.ERROR) ? - "The inject is depending on another inject that failed" - : "The inject is depending on another inject that is not executed yet"; - status.getTraces().add(InjectStatusExecution.traceError(errorMsg)); - status.setName(ExecutionStatus.ERROR); - status.setTrackingSentDate(Instant.now()); - status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); - injectStatusRepository.save(status); + + InjectStatus finalStatus = status; + errorMessages.get().forEach( + errorMsg -> finalStatus.getTraces().add(InjectStatusExecution.traceError(errorMsg)) + ); + finalStatus.setName(ExecutionStatus.ERROR); + finalStatus.setTrackingSentDate(Instant.now()); + finalStatus.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(finalStatus); } else { - inject.getInjectorContract().ifPresent(injectorContract -> { + setInjectStatusAndExecuteInject(executableInject, inject); + } + } + + private void setInjectStatusAndExecuteInject(ExecutableInject executableInject, Inject inject) { + inject.getInjectorContract().ifPresentOrElse(injectorContract -> { + if (!inject.isReady()) { + // Status + if (inject.getStatus().isEmpty()) { + InjectStatus status = new InjectStatus(); + status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + status.setInject(inject); + status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(status); + } else { + InjectStatus status = inject.getStatus().get(); + status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(status); + } + return; + } - if (!inject.isReady()) { + Injector externalInjector = injectorRepository.findByType(injectorContract.getInjector().getType()).orElseThrow(); + LOGGER.log(Level.INFO, "Executing inject " + inject.getInject().getTitle()); + // Executor logics + ExecutableInject newExecutableInject = executableInject; + if (Boolean.TRUE.equals(injectorContract.getNeedsExecutor())) { + try { // Status if (inject.getStatus().isEmpty()) { InjectStatus status = new InjectStatus(); - status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); - status.setName(ExecutionStatus.ERROR); + status.setName(ExecutionStatus.EXECUTING); status.setTrackingSentDate(Instant.now()); status.setInject(inject); status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); injectStatusRepository.save(status); } else { InjectStatus status = inject.getStatus().get(); - status.getTraces().add(InjectStatusExecution.traceError("The inject is not ready to be executed (missing mandatory fields)")); - status.setName(ExecutionStatus.ERROR); + status.setName(ExecutionStatus.EXECUTING); status.setTrackingSentDate(Instant.now()); status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); injectStatusRepository.save(status); } - return; + newExecutableInject = this.executionExecutorService.launchExecutorContext(executableInject, inject); + } catch (InterruptedException e) { + throw new RuntimeException(e); } + } + if (externalInjector.isExternal()) { + executeExternal(newExecutableInject); + } else { + executeInternal(newExecutableInject); + } + }, () -> setInjectStatusWhenNoInjectorContractExists(inject)); + } - Injector externalInjector = injectorRepository.findByType(injectorContract.getInjector().getType()).orElseThrow(); - LOGGER.log(Level.INFO, "Executing inject " + inject.getInject().getTitle()); - // Executor logics - ExecutableInject newExecutableInject = executableInject; - if (Boolean.TRUE.equals(injectorContract.getNeedsExecutor())) { - try { - // Status - if (inject.getStatus().isEmpty()) { - InjectStatus status = new InjectStatus(); - status.setName(ExecutionStatus.EXECUTING); - status.setTrackingSentDate(Instant.now()); - status.setInject(inject); - status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); - injectStatusRepository.save(status); - } else { - InjectStatus status = inject.getStatus().get(); - status.setName(ExecutionStatus.EXECUTING); - status.setTrackingSentDate(Instant.now()); - status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); - injectStatusRepository.save(status); - } - newExecutableInject = this.executionExecutorService.launchExecutorContext(executableInject, inject); - } catch (InterruptedException e) { - throw new RuntimeException(e); + private void setInjectStatusWhenNoInjectorContractExists(Inject inject) { + if (inject.getStatus().isEmpty()) { + InjectStatus status = new InjectStatus(); + status.getTraces().add(InjectStatusExecution.traceError("Inject does not have a contract")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + status.setInject(inject); + status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(status); + } else { + InjectStatus status = inject.getStatus().get(); + status.getTraces().add(InjectStatusExecution.traceError("Inject does not have a contract")); + status.setName(ExecutionStatus.ERROR); + status.setTrackingSentDate(Instant.now()); + status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(status); + } + } + + /** + * Get error messages if pre execution conditions are not met + * @param exerciseId the id of the exercise + * @param inject the inject to check + * @return an optional of list of error message + */ + private Optional> getErrorMessagesPreExecution(String exerciseId, Inject inject) { + List injectDependencies = injectDependenciesRepository.findParents(List.of(inject.getId())); + if (!injectDependencies.isEmpty()) { + List parents = injectDependencies.stream() + .map(injectDependency -> injectDependency.getCompositeId().getInjectParent()).toList(); + + Map mapCondition = getStringBooleanMap(parents, exerciseId, injectDependencies); + + List results = null; + + for (InjectDependency injectDependency : injectDependencies) { + String expressionToEvaluate = injectDependency.getInjectDependencyCondition().toString(); + List conditions = injectDependency.getInjectDependencyCondition().getConditions().stream().map(InjectDependencyConditions.Condition::toString).toList(); + for(String condition : conditions) { + expressionToEvaluate = expressionToEvaluate.replaceAll(condition.split("==")[0].trim(), String.format("#this['%s']", condition.split("==")[0].trim())); + } + + ExpressionParser parser = new SpelExpressionParser(); + Expression exp = parser.parseExpression(expressionToEvaluate); + boolean canBeExecuted = Boolean.TRUE.equals(exp.getValue(mapCondition, Boolean.class)); + if (!canBeExecuted) { + if (results == null) { + results = new ArrayList<>(); + results.add("This inject depends on other injects expectations that are not met. The following conditions were not as expected : "); } + results.addAll(labelFromCondition(injectDependency.getCompositeId().getInjectParent(), injectDependency.getInjectDependencyCondition())); + } + } + return results == null ? Optional.empty() : Optional.of(results); + } + return Optional.empty(); + } + + /** + * Get a map containing the expectations and if they are met or not + * @param parents the parents injects + * @param exerciseId the id of the exercise + * @param injectDependencies the list of dependencies + * @return a map of expectations and their value + */ + private @NotNull Map getStringBooleanMap(List parents, String exerciseId, List injectDependencies) { + Map mapCondition = new HashMap<>(); + + injectDependencies.forEach(injectDependency -> { + injectDependency.getInjectDependencyCondition().getConditions().stream().forEach(condition -> { + mapCondition.put(condition.getKey(), false); + }); + }); + + parents.forEach(parent -> { + mapCondition.put("Execution", + parent.getStatus().isPresent() + && !parent.getStatus().get().getName().equals(ExecutionStatus.ERROR) + && !executionStatusesNotReady.contains(parent.getStatus().get().getName())); + + List expectations = injectExpectationRepository.findAllForExerciseAndInject(exerciseId, parent.getId()); + expectations.forEach(injectExpectation -> { + String name = StringUtils.capitalize(injectExpectation.getType().toString().toLowerCase()); + if(injectExpectation.getType().equals(InjectExpectation.EXPECTATION_TYPE.MANUAL)) { + name = injectExpectation.getName(); } - if (externalInjector.isExternal()) { - executeExternal(newExecutableInject); + if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType()) + || InjectExpectation.EXPECTATION_TYPE.ARTICLE.equals(injectExpectation.getType())) { + if(injectExpectation.getUser() == null && injectExpectation.getScore() != null) { + mapCondition.put(name, injectExpectation.getScore() >= injectExpectation.getExpectedScore()); + } } else { - executeInternal(newExecutableInject); + mapCondition.put(name, expectationStatusesSuccess.contains(injectExpectation.getResponse())); } }); + }); + return mapCondition; + } + + private List labelFromCondition(Inject injectParent, InjectDependencyConditions.InjectDependencyCondition condition) { + List result = new ArrayList<>(); + for (InjectDependencyConditions.Condition conditionElement : condition.getConditions()) { + result.add(String.format("Inject '%s' - %s is %s", injectParent.getTitle(), conditionElement.getKey(), conditionElement.isValue())); } + return result; } public void updateExercise(String exerciseId) { diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java index f947391e3c..5b15d10ce2 100644 --- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java +++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.openbas.asset.AssetGroupService; import io.openbas.database.model.*; import io.openbas.database.raw.RawAsset; import io.openbas.database.raw.RawAssetGroup; @@ -75,6 +76,7 @@ public class AtomicTestingService { private final TeamRepository teamRepository; private final TagRepository tagRepository; private final DocumentRepository documentRepository; + private final AssetGroupService assetGroupService; private ApplicationContext context; private static final String PRE_DEFINE_EXPECTATIONS = "predefinedExpectations"; @@ -90,6 +92,12 @@ public void setContext(ApplicationContext context) { public InjectResultDTO findById(String injectId) { Optional inject = injectRepository.findWithStatusById(injectId); + + if(inject.isPresent()) { + List computedAssetGroup = inject.get().getAssetGroups().stream().map(assetGroupService::computeDynamicAssets).toList(); + inject.get().getAssetGroups().clear(); + inject.get().getAssetGroups().addAll(computedAssetGroup); + } InjectResultDTO result = inject .map(AtomicTestingMapper::toDtoWithTargetResults) .orElseThrow(ElementNotFoundException::new); @@ -121,7 +129,6 @@ public InjectResultDTO createOrUpdate(AtomicTestingInput input, String injectId) injectToSave.setExercise(null); // Set dependencies - injectToSave.setDependsOn(null); injectToSave.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); injectToSave.setTags(iterableToSet(tagRepository.findAllById(input.getTagIds()))); injectToSave.setAssets(fromIterable(this.assetRepository.findAllById(input.getAssets()))); @@ -228,7 +235,9 @@ public Inject copyInject(@NotNull Inject injectOrigin, boolean isAtomic) { injectDuplicate.setTeams(injectOrigin.getTeams().stream().toList()); injectDuplicate.setEnabled(injectOrigin.isEnabled()); injectDuplicate.setDependsDuration(injectOrigin.getDependsDuration()); - injectDuplicate.setDependsOn(injectOrigin.getDependsOn()); + if(injectOrigin.getDependsOn() != null) { + injectDuplicate.setDependsOn(injectOrigin.getDependsOn().stream().toList()); + } injectDuplicate.setCountry(injectOrigin.getCountry()); injectDuplicate.setCity(injectOrigin.getCity()); injectDuplicate.setInjectorContract(injectOrigin.getInjectorContract().orElse(null)); diff --git a/openbas-api/src/main/java/io/openbas/service/InjectService.java b/openbas-api/src/main/java/io/openbas/service/InjectService.java index d1c6c905fc..c6e825984a 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectService.java @@ -1199,7 +1199,8 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root injectScenarioJoin = createLeftJoin(injectRoot, "scenario"); Join injectorContractJoin = createLeftJoin(injectRoot, "injectorContract"); Join injectorJoin = injectorContractJoin.join("injector", JoinType.LEFT); - Join injectDependsJoin = createLeftJoin(injectRoot, "dependsOn"); + Join injectDependency = createLeftJoin(injectRoot, "dependsOn"); + // Array aggregations Expression tagIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "tags"); Expression teamIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "teams"); @@ -1216,13 +1217,13 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root cq, Root execInject(TypedQuery query) { tuple.get("inject_exercise", String.class), tuple.get("inject_scenario", String.class), tuple.get("inject_depends_duration", Long.class), - tuple.get("inject_depends_from_another", String.class), tuple.get("inject_injector_contract", InjectorContract.class), tuple.get("inject_tags", String[].class), tuple.get("inject_teams", String[].class), tuple.get("inject_assets", String[].class), tuple.get("inject_asset_groups", String[].class), - tuple.get("inject_type", String.class) + tuple.get("inject_type", String.class), + tuple.get("inject_depends_on", InjectDependency.class) )) .toList(); } diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java index 7669482a2c..c933c5b67a 100644 --- a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java +++ b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java @@ -247,7 +247,15 @@ public Exercise toExercise( scenarioInjects.forEach(scenarioInject -> { if(scenarioInject.getDependsOn() != null) { Inject injectToUpdate = mapExerciseInjectsByScenarioInject.get(scenarioInject.getId()); - injectToUpdate.setDependsOn(mapExerciseInjectsByScenarioInject.get(scenarioInject.getDependsOn().getId())); + injectToUpdate.getDependsOn().clear(); + injectToUpdate.getDependsOn().addAll(scenarioInject.getDependsOn().stream().map((injectDependency -> { + InjectDependency dep = new InjectDependency(); + dep.setCompositeId(injectDependency.getCompositeId()); + dep.setInjectDependencyCondition(injectDependency.getInjectDependencyCondition()); + dep.getCompositeId().setInjectParent(mapExerciseInjectsByScenarioInject.get(dep.getCompositeId().getInjectParent().getId())); + dep.getCompositeId().setInjectChildren(injectToUpdate); + return dep; + })).toList()); this.injectRepository.save(injectToUpdate); } }); diff --git a/openbas-api/src/main/resources/application.properties b/openbas-api/src/main/resources/application.properties index 42ca018215..bae8966c66 100644 --- a/openbas-api/src/main/resources/application.properties +++ b/openbas-api/src/main/resources/application.properties @@ -244,4 +244,7 @@ openbas.expectation.human.expiration-time=86400 #openbas.expectation.article.expiration-time=3600 #openbas.expectation.manual.expiration-time=3600 +# Min value: 1 +# Max value: 100 +# Default value: 50 openbas.expectation.manual.default-score-value=50 diff --git a/openbas-framework/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java b/openbas-framework/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java index fc4c53735a..f34c3434ac 100644 --- a/openbas-framework/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java +++ b/openbas-framework/src/main/java/io/openbas/executors/tanium/service/TaniumExecutorService.java @@ -22,7 +22,10 @@ import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; import java.util.logging.Level; import static java.time.Instant.now; diff --git a/openbas-framework/src/main/java/io/openbas/expectation/ExpectationPropertiesConfig.java b/openbas-framework/src/main/java/io/openbas/expectation/ExpectationPropertiesConfig.java index f48eecbc38..1fb3e02bdf 100644 --- a/openbas-framework/src/main/java/io/openbas/expectation/ExpectationPropertiesConfig.java +++ b/openbas-framework/src/main/java/io/openbas/expectation/ExpectationPropertiesConfig.java @@ -1,6 +1,7 @@ package io.openbas.expectation; import lombok.Setter; +import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -8,6 +9,7 @@ @Component @Setter +@Log public class ExpectationPropertiesConfig { public static long DEFAULT_TECHNICAL_EXPECTATION_EXPIRATION_TIME = 21600L; // 6 hours @@ -73,8 +75,13 @@ public long getManualExpirationTime() { } public int getDefaultExpectationScoreValue() { - return ofNullable(this.defaultManualExpectationScore) - .orElse(DEFAULT_MANUAL_EXPECTATION_SCORE); + if (defaultManualExpectationScore == null || + defaultManualExpectationScore < 1 || + defaultManualExpectationScore > 100) { + log.warning("The provided default score value is invalid. It should be within the acceptable range of 0 to 100. The score will be set to the default of 50."); + return DEFAULT_MANUAL_EXPECTATION_SCORE; + } + return defaultManualExpectationScore; } } diff --git a/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsRuntime.java b/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsRuntime.java index a5870bd5bf..75daec5eff 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsRuntime.java +++ b/openbas-framework/src/main/java/io/openbas/utils/OperationUtilsRuntime.java @@ -24,6 +24,9 @@ public static boolean containsTexts(@NotNull final Object value, @NotNull final } public static boolean containsText(@NotNull final Object value, @NotBlank final String text) { + if(value instanceof Enum) { + return ((Enum) value).name().toLowerCase().contains(text.toLowerCase()); + } return ((String) value).toLowerCase().contains(text.toLowerCase()); } diff --git a/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts b/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts index 65380832bf..41ddf6ea43 100644 --- a/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts +++ b/openbas-front/src/actions/atomic_testings/atomic-testing-actions.ts @@ -45,3 +45,10 @@ export const duplicateAtomicTesting = (injectId: string) => { const uri = `${ATOMIC_TESTING_URI}/${injectId}`; return simplePostCall(uri, null); }; + +// -- TEAMS -- + +export const searchAtomicTestingTeams = (paginationInput: SearchPaginationInput, contextualOnly: boolean = false) => { + const uri = `${ATOMIC_TESTING_URI}/teams/search?contextualOnly=${contextualOnly}`; + return simplePostCall(uri, paginationInput); +}; diff --git a/openbas-front/src/actions/exercises/exercise-teams-action.ts b/openbas-front/src/actions/exercises/exercise-teams-action.ts index 52861a7f3e..b71445e12a 100644 --- a/openbas-front/src/actions/exercises/exercise-teams-action.ts +++ b/openbas-front/src/actions/exercises/exercise-teams-action.ts @@ -4,9 +4,8 @@ import { putReferential, simplePostCall } from '../../utils/Action'; import { EXERCISE_URI } from './exercise-action'; import * as schema from '../Schema'; -// eslint-disable-next-line import/prefer-default-export -export const searchExerciseTeams = (exerciseId: Scenario['scenario_id'], paginationInput: SearchPaginationInput) => { - const uri = `${EXERCISE_URI}/${exerciseId}/teams/search`; +export const searchExerciseTeams = (exerciseId: Scenario['scenario_id'], paginationInput: SearchPaginationInput, contextualOnly: boolean = false) => { + const uri = `${EXERCISE_URI}/${exerciseId}/teams/search?contextualOnly=${contextualOnly}`; return simplePostCall(uri, paginationInput); }; diff --git a/openbas-front/src/actions/injects/Inject.d.ts b/openbas-front/src/actions/injects/Inject.d.ts index 9d70a9a315..f1b3154851 100644 --- a/openbas-front/src/actions/injects/Inject.d.ts +++ b/openbas-front/src/actions/injects/Inject.d.ts @@ -39,3 +39,40 @@ export type InjectExpectationStore = Omit { - const uri = `${SCENARIO_URI}/${scenarioId}/teams/search`; +export const searchScenarioTeams = (scenarioId: Scenario['scenario_id'], paginationInput: SearchPaginationInput, contextualOnly: boolean = false) => { + const uri = `${SCENARIO_URI}/${scenarioId}/teams/search?contextualOnly=${contextualOnly}`; return simplePostCall(uri, paginationInput); }; diff --git a/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx b/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx index 352f69fc23..3c1530e947 100644 --- a/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx +++ b/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx @@ -25,7 +25,7 @@ import ExportButton from '../../../../components/common/ExportButton'; import { useQueryableWithLocalStorage } from '../../../../components/common/queryable/useQueryableWithLocalStorage'; import SortHeadersComponentV2 from '../../../../components/common/queryable/sort/SortHeadersComponentV2'; import { Header } from '../../../../components/common/SortHeadersList'; -import FilterModeChip from '../../../../components/common/queryable/filter/FilterModeChip'; +import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import FilterChipValues from '../../../../components/common/queryable/filter/FilterChipValues'; const useStyles = makeStyles(() => ({ @@ -77,7 +77,7 @@ const computeRuleValues = (assetGroup: AssetGroupOutput, t: (value: string) => s <> {assetGroup.asset_group_dynamic_filter.filters.map((filter, idx) => ( - {idx !== 0 && } + {idx !== 0 && } { @@ -76,13 +78,15 @@ const AtomicTestings = () => { /> {userAdmin && (<> setOpenCreateDrawer(true)} /> - setOpenCreateDrawer(false)} - /> + + setOpenCreateDrawer(false)} + /> + ) } diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingUpdate.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingUpdate.tsx index 0e5096fc58..592838d54d 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingUpdate.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/AtomicTestingUpdate.tsx @@ -1,15 +1,9 @@ import React, { FunctionComponent, useContext } from 'react'; import * as R from 'ramda'; -import type { TeamStore } from '../../../../actions/teams/Team'; import UpdateInject from '../../common/injects/UpdateInject'; import type { Inject, InjectResultDTO } from '../../../../utils/api-types'; import { updateAtomicTesting } from '../../../../actions/atomic_testings/atomic-testing-actions'; import { InjectResultDtoContext, InjectResultDtoContextType } from '../InjectResultDtoContext'; -import { useHelper } from '../../../../store'; -import type { TeamsHelper } from '../../../../actions/teams/team-helper'; -import useDataLoader from '../../../../utils/hooks/useDataLoader'; -import { fetchTeams } from '../../../../actions/teams/team-actions'; -import { useAppDispatch } from '../../../../utils/hooks'; interface Props { atomic: InjectResultDTO; @@ -22,17 +16,6 @@ const AtomicTestingUpdate: FunctionComponent = ({ open, handleClose, }) => { - // Standard hooks - const dispatch = useAppDispatch(); - - // Fetching data - const { teams } = useHelper((helper: TeamsHelper) => ({ - teams: helper.getTeams(), - })); - useDataLoader(() => { - dispatch(fetchTeams()); - }); - const { updateInjectResultDto } = useContext(InjectResultDtoContext); const onUpdateAtomicTesting = async (data: Inject) => { const toUpdate = R.pipe( @@ -63,7 +46,6 @@ const AtomicTestingUpdate: FunctionComponent = ({ onUpdateInject={onUpdateAtomicTesting} injectId={atomic.inject_id} isAtomic - teamsFromExerciseOrScenario={teams?.filter((team: TeamStore) => !team.team_contextual) ?? []} /> ); }; diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/Index.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/Index.tsx index 4b33f0f41b..7e6dc3180d 100644 --- a/openbas-front/src/admin/components/atomic_testings/atomic_testing/Index.tsx +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/Index.tsx @@ -13,6 +13,8 @@ import { fetchInjectResultDto } from '../../../../actions/atomic_testings/atomic import type { InjectResultDTO } from '../../../../utils/api-types'; import { InjectResultDtoContext } from '../InjectResultDtoContext'; import { FIVE_SECONDS } from '../../../../utils/Time'; +import { TeamContext } from '../../common/Context'; +import teamContextForAtomicTesting from './context/TeamContextForAtomicTesting'; const interval$ = interval(FIVE_SECONDS); @@ -71,46 +73,48 @@ const Index = () => { tabValue = `/admin/atomic_testings/${injectResultDto.inject_id}/detail`; } return ( - - - - - - - - - - }> - - - - {/* Not found */} - } /> - - - + + + + + + + + + + + }> + + + + {/* Not found */} + } /> + + + + ); } return ; diff --git a/openbas-front/src/admin/components/atomic_testings/atomic_testing/context/TeamContextForAtomicTesting.tsx b/openbas-front/src/admin/components/atomic_testings/atomic_testing/context/TeamContextForAtomicTesting.tsx new file mode 100644 index 0000000000..5ce4456390 --- /dev/null +++ b/openbas-front/src/admin/components/atomic_testings/atomic_testing/context/TeamContextForAtomicTesting.tsx @@ -0,0 +1,14 @@ +import { TeamContextType } from '../../../common/Context'; +import type { SearchPaginationInput, TeamOutput } from '../../../../../utils/api-types'; +import type { Page } from '../../../../../components/common/queryable/Page'; +import { searchAtomicTestingTeams } from '../../../../../actions/atomic_testings/atomic-testing-actions'; + +const teamContextForAtomicTesting = (): TeamContextType => { + return { + searchTeams(input: SearchPaginationInput, contextualOnly?: boolean): Promise<{ data: Page }> { + return searchAtomicTestingTeams(input, contextualOnly); + }, + }; +}; + +export default teamContextForAtomicTesting; diff --git a/openbas-front/src/admin/components/common/Context.ts b/openbas-front/src/admin/components/common/Context.ts index 3a5e52da81..90739fbe0a 100644 --- a/openbas-front/src/admin/components/common/Context.ts +++ b/openbas-front/src/admin/components/common/Context.ts @@ -71,8 +71,8 @@ export type ReportContextType = { }; export type TeamContextType = { - onAddUsersTeam: (teamId: Team['team_id'], userIds: UserStore['user_id'][]) => Promise, - onRemoveUsersTeam: (teamId: Team['team_id'], userIds: UserStore['user_id'][]) => Promise, + onAddUsersTeam?: (teamId: Team['team_id'], userIds: UserStore['user_id'][]) => Promise, + onRemoveUsersTeam?: (teamId: Team['team_id'], userIds: UserStore['user_id'][]) => Promise, onAddTeam?: (teamId: Team['team_id']) => Promise, onCreateTeam?: (team: TeamCreateInput) => Promise<{ result: string }>, onRemoveTeam?: (teamId: Team['team_id']) => void, @@ -80,7 +80,7 @@ export type TeamContextType = { onToggleUser?: (teamId: Team['team_id'], userId: UserStore['user_id'], userEnabled: boolean) => void, checkUserEnabled?: (teamId: Team['team_id'], userId: UserStore['user_id']) => boolean, computeTeamUsersEnabled?: (teamId: Team['team_id']) => number, - searchTeams: (input: SearchPaginationInput) => Promise<{ data: Page }>, + searchTeams: (input: SearchPaginationInput, contextualOnly?: boolean) => Promise<{ data: Page }>, }; export type InjectContextType = { @@ -181,7 +181,7 @@ export const TeamContext = createContext({ return new Promise(() => { }); }, - searchTeams(_: SearchPaginationInput): Promise<{ data: Page }> { + searchTeams(_: SearchPaginationInput, _contextualOnly?: boolean): Promise<{ data: Page }> { return new Promise<{ data: Page }>(() => { }); }, diff --git a/openbas-front/src/admin/components/common/injects/CreateInjectDetails.js b/openbas-front/src/admin/components/common/injects/CreateInjectDetails.js index 752c8c8c76..9ddf0c027b 100644 --- a/openbas-front/src/admin/components/common/injects/CreateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/CreateInjectDetails.js @@ -9,7 +9,6 @@ import InjectDefinition from './InjectDefinition'; import { PermissionsContext } from '../Context'; import { useHelper } from '../../../../store'; import { useAppDispatch } from '../../../../utils/hooks'; -import { fetchTeams } from '../../../../actions/teams/team-actions'; import useDataLoader from '../../../../utils/hooks/useDataLoader'; import { fetchTags } from '../../../../actions/Tag'; import InjectForm from './InjectForm'; @@ -84,12 +83,10 @@ const CreateInjectDetails = ({ const [openDetails, setOpenDetails] = useState(false); const [injectDetailsState, setInjectDetailsState] = useState({}); const dispatch = useAppDispatch(); - const { tagsMap, teams } = useHelper((helper) => ({ + const { tagsMap } = useHelper((helper) => ({ tagsMap: helper.getTagsMap(), - teams: helper.getTeams(), })); useDataLoader(() => { - dispatch(fetchTeams()); dispatch(fetchTags()); }); const toggleInjectContent = () => { @@ -103,32 +100,6 @@ const CreateInjectDetails = ({ }; const validate = (values) => { const errors = {}; - if (openDetails && contractContent && Array.isArray(contractContent.fields)) { - contractContent.fields - .filter( - (f) => !['teams', 'assets', 'assetgroups', 'articles', 'challenges', 'attachments', 'expectations'].includes( - f.key, - ), - ) - .forEach((field) => { - const value = values[field.key]; - if (field.mandatory && (value === undefined || R.isEmpty(value))) { - errors[field.key] = t('This field is required.'); - } - if (field.mandatoryGroups) { - const { mandatoryGroups } = field; - const conditionOk = mandatoryGroups?.some((mandatoryKey) => { - const v = values[mandatoryKey]; - return v !== undefined && !R.isEmpty(v); - }); - // If condition are not filled - if (!conditionOk) { - const labels = mandatoryGroups.map((key) => contractContent.fields.find((f) => f.key === key).label).join(', '); - errors[field.key] = t(`One of this field is required : ${labels}.`); - } - } - }); - } const requiredFields = [ 'inject_title', 'inject_depends_duration_days', @@ -179,15 +150,9 @@ const CreateInjectDetails = ({ && data[field.key] && data[field.key].length > 0 ) { + const regex = /<#list\s+(\w+)\s+as\s+(\w+)>/g; finalData[field.key] = data[field.key] - .replaceAll( - '<#list challenges as challenge>', - '<#list challenges as challenge>', - ) - .replaceAll( - '<#list articles as article>', - '<#list articles as article>', - ) + .replace(regex, (_, listName, identifier) => `<#list ${listName} as ${identifier}>`) .replaceAll('</#list>', ''); } else if (data[field.key] && field.type === 'tuple') { if (field.cardinality && field.cardinality === '1') { @@ -391,7 +356,6 @@ const CreateInjectDetails = ({ handleClose={handleClose} tagsMap={tagsMap} permissions={permissions} - teamsFromExerciseOrScenario={teams?.filter((team) => !team.team_contextual) ?? []} articlesFromExerciseOrScenario={[]} variablesFromExerciseOrScenario={[]} onCreateInject={onCreateInject} diff --git a/openbas-front/src/admin/components/common/injects/InjectAddTeams.tsx b/openbas-front/src/admin/components/common/injects/InjectAddTeams.tsx index 8feed14ee6..5ff7517a90 100644 --- a/openbas-front/src/admin/components/common/injects/InjectAddTeams.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectAddTeams.tsx @@ -1,35 +1,26 @@ -import React, { FunctionComponent, useContext, useState } from 'react'; -import * as R from 'ramda'; -import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Grid, List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; +import React, { FunctionComponent, useContext, useEffect, useMemo, useState } from 'react'; +import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { ControlPointOutlined, GroupsOutlined } from '@mui/icons-material'; import { makeStyles } from '@mui/styles'; -import SearchFilter from '../../../../components/SearchFilter'; import { useFormatter } from '../../../../components/i18n'; -import { fetchTeams } from '../../../../actions/teams/team-actions'; +import { findTeams } from '../../../../actions/teams/team-actions'; import CreateTeam from '../../components/teams/CreateTeam'; -import { truncate } from '../../../../utils/String'; import Transition from '../../../../components/common/Transition'; -import TagsFilter from '../filters/TagsFilter'; import ItemTags from '../../../../components/ItemTags'; import type { Theme } from '../../../../components/Theme'; import useDataLoader from '../../../../utils/hooks/useDataLoader'; import { useAppDispatch } from '../../../../utils/hooks'; -import type { Option } from '../../../../utils/Option'; import { PermissionsContext, TeamContext } from '../Context'; import type { TeamStore } from '../../../../actions/teams/Team'; -import { useHelper } from '../../../../store'; -import type { TeamsHelper } from '../../../../actions/teams/team-helper'; +import SelectList, { SelectListElements } from '../../../../components/common/SelectList'; +import type { TeamOutput } from '../../../../utils/api-types'; +import type { EndpointStore } from '../../assets/endpoints/Endpoint'; +import PaginationComponentV2 from '../../../../components/common/queryable/pagination/PaginationComponentV2'; +import { useQueryable } from '../../../../components/common/queryable/useQueryableWithLocalStorage'; +import { buildSearchPagination } from '../../../../components/common/queryable/QueryableUtils'; +import { fetchTags } from '../../../../actions/Tag'; const useStyles = makeStyles((theme: Theme) => ({ - box: { - width: '100%', - minHeight: '100%', - padding: 20, - border: '1px dashed rgba(255, 255, 255, 0.3)', - }, - chip: { - margin: '0 10px 10px 0', - }, item: { paddingLeft: 10, height: 50, @@ -43,97 +34,91 @@ const useStyles = makeStyles((theme: Theme) => ({ interface Props { handleAddTeams: (teamIds: string[]) => void; - injectTeamsIds: string[] - teams: TeamStore[] + injectTeamsIds: string[]; } const InjectAddTeams: FunctionComponent = ({ handleAddTeams, injectTeamsIds, - teams, }) => { // Standard hooks - const classes = useStyles(); const { t } = useFormatter(); + const classes = useStyles(); const dispatch = useAppDispatch(); const { permissions } = useContext(PermissionsContext); - const { onAddTeam } = useContext(TeamContext); - - const teamsMap = useHelper((helper: TeamsHelper) => helper.getTeamsMap()); + const { searchTeams } = useContext(TeamContext); + // Fetch datas useDataLoader(() => { - dispatch(fetchTeams()); + dispatch(fetchTags()); }); - const [open, setopen] = useState(false); - const [keyword, setKeyword] = useState(''); - const [teamsIds, setTeamsIds] = useState([]); - const [tags, setTags] = useState([]); + const [teamValues, setTeamValues] = useState([]); + const [selectedTeamValues, setSelectedTeamValues] = useState([]); - const handleOpen = () => setopen(true); + // Dialog + const [open, setOpen] = useState(false); const handleClose = () => { - setopen(false); - setKeyword(''); - setTeamsIds([]); - }; - - const handleSearchTeams = (value?: string) => { - setKeyword(value || ''); - }; - - const handleAddTag = (value: Option) => { - if (value) { - setTags([value]); - } - }; - - const handleClearTag = () => { - setTags([]); - }; - - const addTeam = (teamId: string) => { - setTeamsIds(R.append(teamId, teamsIds)); - }; - - const removeTeam = (teamId: string) => { - setTeamsIds(teamsIds.filter((u) => u !== teamId)); + setOpen(false); + setSelectedTeamValues([]); }; const submitAddTeams = () => { - handleAddTeams(teamsIds); + handleAddTeams(selectedTeamValues.map((v) => v.team_id).filter((id) => !injectTeamsIds.includes(id))); handleClose(); }; - const onCreate = async (result: TeamStore) => { - addTeam(result.team_id); - await onAddTeam?.(result.team_id); - }; + useEffect(() => { + if (open) { + findTeams(injectTeamsIds).then((result) => setSelectedTeamValues(result.data)); + } + }, [open, injectTeamsIds]); + + // Pagination + const addTeam = (_teamId: string, team: TeamOutput) => setSelectedTeamValues([...selectedTeamValues, team]); + const removeTeam = (teamId: string) => setSelectedTeamValues(selectedTeamValues.filter((v) => v.team_id !== teamId)); + + // Headers + const elements: SelectListElements = useMemo(() => ({ + icon: { + value: () => , + }, + headers: [ + { + field: 'team_name', + value: (team: TeamStore) => team.team_name, + width: 70, + }, + { + field: 'team_tags', + value: (team: TeamStore) => , + width: 30, + }, + ], + }), []); + + const availableFilterNames = [ + 'team_tags', + ]; + const { queryableHelpers, searchPaginationInput } = useQueryable(buildSearchPagination({})); + + const paginationComponent = searchTeams(input, true)} + searchPaginationInput={searchPaginationInput} + setContent={setTeamValues} + entityPrefix="team" + availableFilterNames={availableFilterNames} + queryableHelpers={queryableHelpers} + />; - const filterByKeyword = (n: TeamStore) => keyword === '' - || (n.team_name || '').toLowerCase().indexOf(keyword.toLowerCase()) - !== -1 - || (n.team_description || '') - .toLowerCase() - .indexOf(keyword.toLowerCase()) !== -1; - const filteredTeams = R.pipe( - R.filter( - (n: TeamStore) => tags.length === 0 - || R.any( - (filter: string) => R.includes(filter, n.team_tags), - R.pluck('id', tags), - ), - ), - R.filter(filterByKeyword), - R.take(10), - )(teams); return (
setOpen(true)} color="primary" disabled={permissions.readOnly} > @@ -161,81 +146,28 @@ const InjectAddTeams: FunctionComponent = ({ > {t('Add target teams in this inject')} - - - - - - - - - - - - {filteredTeams.map((team: TeamStore) => { - const teamDisabled = teamsIds.includes(team.team_id) - || injectTeamsIds.includes(team.team_id); - return ( - addTeam(team.team_id)} - > - - - - - - - ); - })} - - - - - - {teamsIds.map((teamId) => { - const team = teamsMap[teamId]; - return ( - removeTeam(teamId)} - label={truncate(team?.team_name || '', 22)} - icon={} - classes={{ root: classes.chip }} - /> - ); - })} - - - + + { + setTeamValues([...teamValues, team]); + setSelectedTeamValues([...selectedTeamValues, team]); + }} + />} + /> + - diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js deleted file mode 100644 index 90ee59112e..0000000000 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ /dev/null @@ -1,280 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@mui/styles'; -import { Accordion, AccordionDetails, AccordionSummary, FormControl, IconButton, InputLabel, MenuItem, Select, Tooltip, Typography } from '@mui/material'; -import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; -import { useFormatter } from '../../../../components/i18n'; - -const useStyles = makeStyles(() => ({ - container: { - display: 'inline-flex', - alignItems: 'center', - }, - importerStyle: { - display: 'flex', - alignItems: 'center', - marginTop: 20, - }, -})); - -const InjectForm = ({ - values, - form, - injects, -}) => { - const classes = useStyles(); - const { t } = useFormatter(); - - const [parents, setParents] = useState( - injects.filter((currentInject) => currentInject.inject_id === values.inject_depends_on) - .map((inject, index) => { - return { inject, index }; - }), - ); - const [childrens, setChildrens] = useState( - injects.filter((currentInject) => currentInject.inject_depends_on === values.inject_id) - .map((inject, index) => { - return { inject, index }; - }), - ); - - const handleChangeParent = (_event, parent) => { - const rx = /\.\$select-parent-(.*)-inject-(.*)/g; - const arr = rx.exec(parent.key); - - const newParents = parents - .map((element) => { - if (element.index === parseInt(arr[1], 10)) { - return { - inject: injects.find((currentInject) => currentInject.inject_id === arr[2]), - index: element.index, - }; - } - return element; - }); - - setParents(newParents); - - // We take any parent that is not undefined or undefined (since there is only one parent for now, this'll - // be changed when we allow for multiple parents) - const anyParent = newParents.find((inject) => inject !== undefined); - - form.mutators.setValue( - 'inject_depends_on', - anyParent?.inject.inject_id || null, - ); - }; - - const addParent = () => { - setParents([...parents, { inject: undefined, index: parents.length }]); - }; - - const handleChangeChildren = (_event, child) => { - const rx = /\.\$select-children-(.*)-inject-(.*)/g; - const arr = rx.exec(child.key); - - const newChildrens = childrens - .map((element) => { - if (element.index === parseInt(arr[1], 10)) { - return { - inject: injects.find((currentInject) => currentInject.inject_id === arr[2]), - index: element.index, - }; - } - return element; - }); - - setChildrens(newChildrens); - - form.mutators.setValue('inject_depends_to', newChildrens.map((inject) => inject.inject?.inject_id)); - }; - - const addChildren = () => { - setChildrens([...childrens, { inject: undefined, index: childrens.length }]); - }; - - const deleteParent = (parent) => { - const parentIndexInArray = parents.findIndex((currentParent) => currentParent.index === parent.index); - - if (parentIndexInArray > -1) { - const newParents = [ - ...parents.slice(0, parentIndexInArray), - ...parents.slice(parentIndexInArray + 1), - ]; - setParents(newParents); - const anyParent = newParents.find((inject) => inject !== undefined); - - form.mutators.setValue( - 'inject_depends_on', - anyParent?.inject.inject_id || null, - ); - } - }; - - const deleteChildren = (children) => { - const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.index === children.index); - - if (childrenIndexInArray > -1) { - const newChildrens = [ - ...childrens.slice(0, childrenIndexInArray), - ...childrens.slice(childrenIndexInArray + 1), - ]; - setChildrens(newChildrens); - - form.mutators.setValue('inject_depends_to', newChildrens.map((inject) => inject.inject?.inject_id)); - } - }; - - return ( - <> -
- - {t('Parent')} - - 0} - onClick={addParent} - > - - -
- - {parents.map((parent, index) => { - return ( - - } - > -
- - #{index + 1} {parent.inject?.inject_title} - - - { deleteParent(parent); }} - > - - - -
-
- - - {t('Inject')} - - - - {t('Condition')} - - - -
- ); - })} - -
- - {t('Childrens')} - - - - -
- {childrens.map((children, index) => { - return ( - - } - > -
- - #{index + 1} {children.inject?.inject_title} - - - { deleteChildren(children); }} - > - - - -
-
- - - {t('Inject')} - - - - {t('Condition')} - - - -
- ); - })} - - ); -}; - -export default InjectForm; diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx new file mode 100644 index 0000000000..b1050e3c05 --- /dev/null +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -0,0 +1,989 @@ +import React, { ReactElement, ReactNode, useEffect, useState } from 'react'; +import { makeStyles } from '@mui/styles'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Tooltip, + Typography, +} from '@mui/material'; +import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; +import { FormApi } from 'final-form'; +import { Value } from 'classnames'; +import { useFormatter } from '../../../../components/i18n'; +import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; +import ClickableChip from '../../../../components/common/chips/ClickableChip'; +import { capitalize } from '../../../../utils/String'; +import type { Inject, InjectDependency, InjectDependencyCondition, InjectOutput } from '../../../../utils/api-types'; +import type { ConditionElement, ConditionType, Content, ConvertedContentType, Dependency, InjectOutputType } from '../../../../actions/injects/Inject'; +import type { Element } from '../../../../components/common/chips/ClickableChip'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'inline-flex', + alignItems: 'center', + }, + importerStyle: { + display: 'flex', + alignItems: 'center', + marginTop: 20, + }, + labelExecutionCondition: { + color: '#7c8088', + }, +})); + +interface Props { + values: Inject & { inject_depends_to: InjectDependency[]; }, + form: FormApi>, + injects?: InjectOutputType[], +} + +const InjectForm: React.FC = ({ values, form, injects }) => { + const classes = useStyles(); + const { t } = useFormatter(); + + // List of parents + const [parents, setParents] = useState( + () => { + if (values.inject_depends_on) { + return values.inject_depends_on?.filter((searchInject) => searchInject.dependency_relationship?.inject_children_id === values.inject_id) + .map((inject, index) => { + return { + inject: injects?.find((currentInject) => currentInject.inject_id === inject.dependency_relationship?.inject_parent_id), + index, + }; + }); + } + return []; + }, + + ); + + // List of childrens + const [childrens, setChildrens] = useState( + () => { + if (injects !== undefined) { + return injects?.filter( + (searchInject) => searchInject.inject_depends_on?.find( + (dependsOnSearch) => dependsOnSearch.dependency_relationship?.inject_parent_id === values.inject_id, + ) !== undefined, + ) + .map((inject, index) => { + return { + inject, + index, + }; + }); + } + return []; + }, + ); + + // Property to deactivate the add children button if there are no children available anymore + const [addChildrenButtonDisabled, setAddChildrenButtonDisabled] = useState(false); + useEffect(() => { + const availableChildrensNumber = injects ? injects.filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration).length : 0; + setAddChildrenButtonDisabled(childrens ? childrens.length >= availableChildrensNumber : true); + }, [childrens]); + + /** + * Transform an inject dependency into ConditionElement + * @param injectDependsOn an array of injectDependency + */ + const getConditionContentParent = (injectDependsOn: (InjectDependency | undefined)[]) => { + const conditions: ConditionType[] = []; + if (injectDependsOn) { + injectDependsOn.forEach((parent) => { + if (parent !== undefined) { + conditions.push({ + parentId: parent.dependency_relationship?.inject_parent_id, + childrenId: parent.dependency_relationship?.inject_children_id, + mode: parent.dependency_condition?.mode, + conditionElement: parent.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { + return { + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, + }; + }), + }); + } + }); + } + return conditions; + }; + + /** + * Transform an inject dependency into ConditionElement + * @param injectDependsTo an array of injectDependency + */ + const getConditionContentChildren = (injectDependsTo: (InjectDependency | undefined)[]) => { + const conditions: ConditionType[] = []; + injectDependsTo.forEach((children) => { + if (children !== undefined) { + conditions.push({ + parentId: values.inject_id, + childrenId: children.dependency_relationship?.inject_children_id, + mode: children.dependency_condition?.mode, + conditionElement: children.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { + return { + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, + }; + }), + }); + } + }); + return conditions; + }; + + const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on ? values.inject_depends_on : [])); + const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); + + /** + * Get the inject dependency object from dependency ones + * @param deps the inject depencies + */ + const injectDependencyFromDependency = (deps: Dependency[]) => { + return deps.flatMap((dependency) => (dependency.inject?.inject_depends_on !== null ? dependency.inject?.inject_depends_on : [])); + }; + + /** + * Handle the change of the parent + * @param _event the event + * @param parent the parent key + */ + const handleChangeParent = (_event: SelectChangeEvent, parent: ReactNode) => { + const rx = /\.\$select-parent-(.*)-inject-(.*)/g; + if (!parent) return; + let key = ''; + const parentElement = parent as ReactElement; + if ('key' in parentElement && parentElement.key !== null) { + key = parentElement.key; + } + if (key === null) { + return; + } + const arr = rx.exec(key); + + if (parents === undefined || arr === null || injects === undefined) return; + const newInject = injects.find((currentInject) => currentInject.inject_id === arr[2]); + const newParents = parents + .map((element) => { + if (element.index === parseInt(arr[1], 10)) { + const previousInject = injects.find((value) => value.inject_id === element.inject?.inject_id); + if (previousInject?.inject_depends_on !== undefined) { + previousInject!.inject_depends_on = previousInject!.inject_depends_on?.filter( + (dependsOn) => dependsOn.dependency_relationship?.inject_children_id !== values.inject_id, + ); + } + return { + inject: newInject!, + index: element.index, + }; + } + return element; + }); + setParents(newParents); + + const baseInjectDependency: InjectDependency = { + dependency_relationship: { + inject_parent_id: newInject?.inject_id, + inject_children_id: values.inject_id, + }, + dependency_condition: { + conditions: [ + { + key: 'Execution', + operator: 'eq', + value: true, + }, + ], + mode: 'and', + }, + }; + setParentConditions(getConditionContentParent([baseInjectDependency])); + + form.mutators.setValue( + 'inject_depends_on', + [baseInjectDependency], + ); + }; + + /** + * Add a new parent inject + */ + const addParent = () => { + setParents([...parents, { inject: undefined, index: parents.length }]); + }; + + /** + * Handle the change of a children + * @param _event + * @param child + */ + const handleChangeChildren = (_event: SelectChangeEvent, child: ReactNode) => { + const rx = /\.\$select-children-(.*)-inject-(.*)/g; + if (!child) return; + let key = ''; + const childElement = (child as ReactElement); + if ('key' in (childElement as ReactElement) && childElement.key !== null) { + key = childElement.key; + } + if (key === null) { + return; + } + const arr = rx.exec(key); + + if (childrens === undefined || arr === null || injects === undefined) return; + const newInject = injects.find((currentInject) => currentInject.inject_id === arr[2]); + const newChildrens = childrens + .map((element) => { + if (element.index === parseInt(arr[1], 10)) { + const baseInjectDependency: InjectDependency = { + dependency_relationship: { + inject_parent_id: values.inject_id, + inject_children_id: newInject?.inject_id, + }, + dependency_condition: { + conditions: [ + { + key: 'Execution', + operator: 'eq', + value: true, + }, + ], + mode: 'and', + }, + }; + newInject!.inject_depends_on = [baseInjectDependency]; + return { + inject: newInject!, + index: element.index, + }; + } + return element; + }); + + setChildrens(newChildrens); + + const dependsTo = injectDependencyFromDependency(newChildrens); + form.mutators.setValue('inject_depends_to', dependsTo); + + if (newInject!.inject_depends_on !== null) { + setChildrenConditions(getConditionContentChildren(dependsTo.filter((dep) => dep !== undefined))); + } + }; + + /** + * Add a new children inject + */ + const addChildren = () => { + setChildrens([...childrens, { inject: undefined, index: childrens.length }]); + }; + + /** + * Delete a parent inject + * @param parent + */ + const deleteParent = (parent: Dependency) => { + const parentIndexInArray = parents.findIndex((currentParent) => currentParent.index === parent.index); + + if (parentIndexInArray > -1) { + const newParents = [ + ...parents.slice(0, parentIndexInArray), + ...parents.slice(parentIndexInArray + 1), + ]; + setParents(newParents); + + form.mutators.setValue( + 'inject_depends_on', + injectDependencyFromDependency(newParents), + ); + } + }; + + /** + * Delete a children inject + * @param children + */ + const deleteChildren = (children: Dependency) => { + const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); + + if (childrenIndexInArray > -1) { + const newChildrens = [ + ...childrens.slice(0, childrenIndexInArray), + ...childrens.slice(childrenIndexInArray + 1), + ]; + setChildrens(newChildrens); + + form.mutators.setValue('inject_depends_to', injectDependencyFromDependency(newChildrens)); + } + }; + + /** + * Returns an updated depends on from a ConditionType + * @param conditions + * @param switchIds + */ + const updateDependsCondition = (conditions: ConditionType) => { + const result: InjectDependencyCondition = { + mode: conditions.mode === 'and' ? 'and' : 'or', + conditions: conditions.conditionElement?.map((value) => { + return { + value: value.value, + key: value.key, + operator: 'eq', + }; + }), + }; + return result; + }; + + /** + * Returns an updated depends on from a ConditionType + * @param conditions + * @param switchIds + */ + const updateDependsOn = (conditions: ConditionType) => { + const result: InjectDependency = { + dependency_relationship: { + inject_parent_id: conditions.parentId, + inject_children_id: conditions.childrenId, + }, + dependency_condition: updateDependsCondition(conditions), + }; + return result; + }; + + /** + * Get the list of available expectations + * @param inject + */ + const getAvailableExpectations = (inject: InjectOutputType | undefined) => { + if (inject?.inject_content !== null && inject?.inject_content !== undefined && (inject.inject_content as Content).expectations !== undefined) { + const expectations = (inject.inject_content as Content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + return ['Execution', ...expectations]; + } + if (inject?.inject_injector_contract !== undefined + && (inject?.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields.find((field) => field.key === 'expectations')) { + const predefinedExpectations = (inject.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields?.find((field) => field.key === 'expectations') + ?.predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + if (predefinedExpectations !== undefined) { + return ['Execution', ...predefinedExpectations]; + } + } + return ['Execution']; + }; + + /** + * Add a new condition to a parent inject + * @param parent + */ + const addConditionParent = (parent: Dependency) => { + const currentConditions = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId); + + if (parent.inject !== undefined && currentConditions !== undefined) { + let expectationString = 'Execution'; + if (currentConditions?.conditionElement !== undefined) { + expectationString = getAvailableExpectations(parent.inject) + .find((expectation) => !currentConditions?.conditionElement?.find((conditionElement) => conditionElement.key === expectation)); + } + currentConditions.conditionElement?.push({ + key: expectationString, + name: expectationString, + value: true, + index: currentConditions.conditionElement?.length, + }); + + setParentConditions(parentConditions); + + const element = parentConditions.find((conditionElement) => conditionElement.childrenId === values.inject_id); + + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + + form.mutators.setValue( + 'inject_depends_on', + [dep], + ); + } + }; + + /** + * Add a new condition to a children inject + * @param children + */ + const addConditionChildren = (children: Dependency) => { + const currentConditions = childrenConditions.find((currentCondition) => children.inject!.inject_id === currentCondition.childrenId); + + if (children.inject !== undefined && currentConditions !== undefined) { + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); + let expectationString = 'Execution'; + if (currentConditions?.conditionElement !== undefined) { + expectationString = getAvailableExpectations(values as InjectOutput as InjectOutputType) + .find((expectation) => !currentConditions?.conditionElement?.find((conditionElement) => conditionElement.key === expectation)); + } + currentConditions.conditionElement?.push({ + key: expectationString, + name: expectationString, + value: true, + index: currentConditions.conditionElement?.length, + }); + + if (updatedChildren?.inject?.inject_depends_on !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(currentConditions)]; + } + + setChildrenConditions(childrenConditions); + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); + } + }; + + /** + * Handle a change in a condition of a parent element + * @param newElement + * @param conditions + * @param condition + * @param parent + */ + const changeParentElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { + const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { + if (newConditionElement.index === condition.index) { + return { + index: condition.index, + key: newElement.key, + name: `${conditions.parentId}-${newElement.key}-Success`, + value: newElement.value === 'Success', + }; + } + return newConditionElement; + }); + const newParentConditions = parentConditions.map((parentCondition) => { + if (parentCondition.parentId === parent.inject?.inject_id) { + return { + ...parentCondition, + conditionElement: newConditionElements, + }; + } + return parentCondition; + }); + setParentConditions(newParentConditions); + + const element = newParentConditions?.find((conditionElement) => conditionElement.parentId === conditions.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + + form.mutators.setValue( + 'inject_depends_on', + [dep], + ); + }; + + /** + * Handle a change in a condition of a children element + * @param newElement + * @param conditions + * @param condition + * @param children + */ + const changeChildrenElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, children: Dependency) => { + const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { + if (newConditionElement.index === condition.index) { + return { + index: condition.index, + key: newElement.key, + name: `${conditions.childrenId}-${newElement.key}-Success`, + value: newElement.value === 'Success', + }; + } + return newConditionElement; + }); + const newChildrenConditions = childrenConditions.map((childrenCondition) => { + if (childrenCondition.childrenId === children.inject?.inject_id) { + return { + ...childrenCondition, + conditionElement: newConditionElements, + }; + } + return childrenCondition; + }); + setChildrenConditions(newChildrenConditions); + + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); + const newCondition = newChildrenConditions.find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id); + if (updatedChildren?.inject?.inject_depends_on !== undefined && newCondition !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(newCondition)]; + } + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); + }; + + /** + * Changes the mode (AND/OR) in a parent inject + * @param conditions + * @param condition + */ + const changeModeParent = (conditions: ConditionType[] | undefined, condition: ConditionType) => { + const newConditionElements = conditions?.map((currentCondition) => { + if (currentCondition.parentId === condition.parentId) { + return { + ...currentCondition, + mode: currentCondition.mode === 'and' ? 'or' : 'and', + }; + } + return currentCondition; + }); + if (newConditionElements !== undefined) { + setParentConditions(newConditionElements); + } + + const element = newConditionElements?.find((conditionElement) => conditionElement.parentId === condition.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + + form.mutators.setValue( + 'inject_depends_on', + [dep], + ); + }; + + /** + * Changes the mode (AND/OR) in a children inject + * @param conditions + * @param condition + */ + const changeModeChildren = (conditions: ConditionType[] | undefined, condition: ConditionType) => { + const newConditionElements = conditions?.map((currentCondition) => { + if (currentCondition.childrenId === condition.childrenId) { + return { + ...currentCondition, + mode: currentCondition.mode === 'and' ? 'or' : 'and', + }; + } + return currentCondition; + }); + if (newConditionElements !== undefined) { + setChildrenConditions(newConditionElements); + } + + const newCurrentCondition = newConditionElements?.find((currentCondition) => currentCondition.childrenId === condition.childrenId); + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === newCurrentCondition?.childrenId); + if (updatedChildren?.inject?.inject_depends_on !== undefined && newCurrentCondition !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(newCurrentCondition)]; + } + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); + }; + + /** + * Delete a condition from a parent inject + * @param conditions + * @param condition + */ + const deleteConditionParent = (conditions: ConditionType, condition: ConditionElement) => { + const newConditionElements = parentConditions.map((currentCondition) => { + if (currentCondition.parentId === conditions.parentId) { + return { + ...currentCondition, + conditionElement: currentCondition.conditionElement?.filter((element) => element.index !== condition.index), + }; + } + return currentCondition; + }); + setParentConditions(newConditionElements); + + const element = newConditionElements.find((conditionElement) => conditionElement.parentId === conditions.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + + form.mutators.setValue( + 'inject_depends_on', + [dep], + ); + }; + + /** + * Delete a condition from a children inject + * @param conditions + * @param condition + */ + const deleteConditionChildren = (conditions: ConditionType, condition: ConditionElement) => { + const newConditionElements = childrenConditions.map((currentCondition) => { + if (currentCondition.childrenId === conditions.childrenId) { + return { + ...currentCondition, + conditionElement: currentCondition.conditionElement?.filter((element) => element.index !== condition.index), + }; + } + return currentCondition; + }); + setChildrenConditions(newConditionElements); + + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === conditions.childrenId); + if (updatedChildren?.inject?.inject_depends_on !== undefined && conditions !== undefined) { + const newCondition = newConditionElements.find((currentCondition) => currentCondition.childrenId === conditions.childrenId); + if (newCondition !== undefined) updatedChildren.inject.inject_depends_on = [updateDependsOn(newCondition)]; + } + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); + }; + + /** + * Whether or not we can add a new condition + * @param inject + * @param conditions + */ + const canAddConditions = (inject: InjectOutputType, conditions?: ConditionType) => { + const expectationsNumber = getAvailableExpectations(inject).length; + if (conditions === undefined || conditions.conditionElement === undefined) return true; + + return conditions?.conditionElement.length < expectationsNumber; + }; + + /** + * Return a clickable parent chip + * @param parent + */ + const getClickableParentChip = (parent: Dependency) => { + const parentChip = parentConditions.find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); + if (parentChip === undefined || parentChip.conditionElement === undefined) return (<>); + return parentChip.conditionElement.map((condition, conditionIndex) => { + const conditions = parentConditions + .find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); + if (conditions?.conditionElement !== undefined) { + return (
+ 1 ? () => { deleteConditionParent(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeParentElement(newElement, conditions, condition, parent); + }} + /> + {conditionIndex < conditions.conditionElement.length - 1 + && { changeModeParent(parentConditions, conditions); }} + /> + }
); + } + return (<>); + }); + }; + + /** + * Return a clickable children chip + * @param parent + */ + const getClickableChildrenChip = (children: Dependency) => { + const childrenChip = childrenConditions.find((childrenCondition) => children.inject !== undefined && childrenCondition.childrenId === children.inject.inject_id); + if (childrenChip?.conditionElement === undefined) return (<>); + return childrenChip + .conditionElement.map((condition, conditionIndex) => { + const conditions = childrenConditions + .find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id); + if (conditions?.conditionElement !== undefined) { + return (
+ currentInject.inject_id === values.inject_id))} + availableOperators={['is']} + availableValues={['Success', 'Fail']} + onDelete={ + conditions.conditionElement.length > 1 ? () => { deleteConditionChildren(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeChildrenElement(newElement, conditions, condition, children); + }} + /> + {conditionIndex < conditions.conditionElement.length - 1 + && { changeModeChildren(childrenConditions, conditions); }} + /> + }
); + } + return (<>); + }); + }; + + return ( + <> +
+ + {t('Parent')} + + 0 + || injects?.filter((currentInject) => currentInject.inject_depends_duration < values.inject_depends_duration).length === 0} + onClick={addParent} + > + + +
+ + {parents.map((parent, index) => { + return ( + + } + > +
+ + #{index + 1} {parent.inject?.inject_title} + + + { deleteParent(parent); }} + > + + + +
+
+ + + {t('Inject')} + + + + + + {getClickableParentChip(parent)} + +
+ +
+
+
+
+ ); + })} + +
+ + {t('Childrens')} + + + + +
+ {childrens.map((children, index) => { + return ( + + } + > +
+ + #{index + 1} {children.inject?.inject_title} + + + { deleteChildren(children); }} + > + + + +
+
+ + + {t('Inject')} + + + + + + + {getClickableChildrenChip(children)} + +
+ +
+
+
+
+ ); + })} + + ); +}; + +export default InjectForm; diff --git a/openbas-front/src/admin/components/common/injects/InjectDefinition.js b/openbas-front/src/admin/components/common/injects/InjectDefinition.js index 6f449a91e0..014bd5d20d 100644 --- a/openbas-front/src/admin/components/common/injects/InjectDefinition.js +++ b/openbas-front/src/admin/components/common/injects/InjectDefinition.js @@ -357,8 +357,6 @@ class InjectDefinition extends Component { challengesIds: props.inject.inject_content?.challenges || [], documents: props.inject.inject_documents || [], expectations: props.inject.inject_content?.expectations || [], - teamsSortBy: 'team_name', - teamsOrderAsc: true, documentsSortBy: 'document_name', documentsOrderAsc: true, articlesSortBy: 'article_name', @@ -964,14 +962,12 @@ class InjectDefinition extends Component { submitting, inject, injectorContract, - teamsMap, endpointsMap, assetGroupsMap, documentsMap, articlesMap, channelsMap, challengesMap, - teamsFromExerciseOrScenario, articlesFromExerciseOrScenario, isAtomic, } = this.props; @@ -985,8 +981,6 @@ class InjectDefinition extends Component { assetGroupIds, documents, expectations, - teamsSortBy, - teamsOrderAsc, documentsSortBy, documentsOrderAsc, articlesOrderAsc, @@ -998,18 +992,6 @@ class InjectDefinition extends Component { openVariables, } = this.state; // -- TEAMS -- - const teams = teamsIds - .map((a) => teamsMap[a]) - .filter((a) => a !== undefined); - const sortTeams = R.sortWith( - teamsOrderAsc - ? [R.ascend(R.prop(teamsSortBy))] - : [R.descend(R.prop(teamsSortBy))], - ); - const sortedTeams = sortTeams(teams.map((n) => ({ - team_users_enabled_number: this.props.teamsUsers.filter((o) => o.team_id === n.team_id).length, - ...n, - }))); const fieldTeams = injectorContract.fields.filter((n) => n.key === 'teams').at(0); const hasTeams = injectorContract.fields .map((f) => f.key) @@ -1169,11 +1151,10 @@ class InjectDefinition extends Component { ) : ( <> @@ -1559,7 +1540,6 @@ InjectDefinition.propTypes = { injectorContract: PropTypes.object, fetchDocuments: PropTypes.func, tagsMap: PropTypes.object, - teamsFromExerciseOrScenario: PropTypes.array, articlesFromExerciseOrScenario: PropTypes.array, variablesFromExerciseOrScenario: PropTypes.array, permissions: PropTypes.object, @@ -1575,7 +1555,6 @@ InjectDefinition.propTypes = { const select = (state) => { const helper = storeHelper(state); const documentsMap = helper.getDocumentsMap(); - const teamsMap = helper.getTeamsMap(); const endpointsMap = helper.getEndpointsMap(); const assetGroupsMap = helper.getAssetGroupMaps(); const channelsMap = helper.getChannelsMap(); @@ -1583,7 +1562,6 @@ const select = (state) => { const challengesMap = helper.getChallengesMap(); return { documentsMap, - teamsMap, endpointsMap, assetGroupsMap, articlesMap, diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 2dc635a510..9a0f9ec81d 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -49,7 +49,6 @@ const useStyles = makeStyles(() => ({ color: '#00b1ff', border: '1px solid #00b1ff', }, - itemHead: { textTransform: 'uppercase', }, @@ -217,10 +216,13 @@ const Injects: FunctionComponent = ({ setInjects([created as InjectOutputType, ...injects]); } }; + const onUpdate = (result: { result: string, entities: { injects: Record } }) => { if (result.entities) { const updated = result.entities.injects[result.result]; - setInjects(injects.map((i) => (i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType)))); + setInjects(injects.map((i) => { + return (i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType)); + })); } }; @@ -241,10 +243,12 @@ const Injects: FunctionComponent = ({ onCreate(result); }); }; + const onUpdateInject = async (data: Inject) => { if (selectedInjectId) { await injectContext.onUpdateInject(selectedInjectId, data).then((result: { result: string, entities: { injects: Record } }) => { onUpdate(result); + return result; }); } }; @@ -380,7 +384,7 @@ const Injects: FunctionComponent = ({ 'inject_description', 'inject_injector_contract', 'inject_content', - 'inject_depends_from_another', + 'inject_depends_on', 'inject_depends_duration', 'inject_teams', 'inject_assets', @@ -625,7 +629,6 @@ const Injects: FunctionComponent = ({ onUpdateInject={onUpdateInject} massUpdateInject={massUpdateInject} injectId={selectedInjectId} - teamsFromExerciseOrScenario={teams} // @ts-expect-error typing articlesFromExerciseOrScenario={articles} variablesFromExerciseOrScenario={variables} diff --git a/openbas-front/src/admin/components/common/injects/UpdateInject.tsx b/openbas-front/src/admin/components/common/injects/UpdateInject.tsx index bf33037ea8..dc213ebec0 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInject.tsx +++ b/openbas-front/src/admin/components/common/injects/UpdateInject.tsx @@ -6,7 +6,6 @@ import type { Inject } from '../../../../utils/api-types'; import useDataLoader from '../../../../utils/hooks/useDataLoader'; import { useAppDispatch } from '../../../../utils/hooks'; import UpdateInjectDetails from './UpdateInjectDetails'; -import type { TeamStore } from '../../../../actions/teams/Team'; import { fetchInject } from '../../../../actions/Inject'; import { useHelper } from '../../../../store'; import type { InjectHelper } from '../../../../actions/injects/inject-helper'; @@ -20,11 +19,10 @@ interface Props { massUpdateInject?: (data: Inject[]) => Promise; injectId: string; isAtomic?: boolean; - teamsFromExerciseOrScenario: TeamStore[]; injects?: InjectOutputType[]; } -const UpdateInject: React.FC = ({ open, handleClose, onUpdateInject, massUpdateInject, injectId, isAtomic = false, teamsFromExerciseOrScenario, injects, ...props }) => { +const UpdateInject: React.FC = ({ open, handleClose, onUpdateInject, massUpdateInject, injectId, isAtomic = false, injects, ...props }) => { const { t } = useFormatter(); const dispatch = useAppDispatch(); const drawerRef = useRef(null); @@ -81,7 +79,6 @@ const UpdateInject: React.FC = ({ open, handleClose, onUpdateInject, mass handleClose={handleClose} onUpdateInject={onUpdateInject} isAtomic={isAtomic} - teamsFromExerciseOrScenario={teamsFromExerciseOrScenario} {...props} /> )} diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index ecf2905511..cbf139e44e 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -9,7 +9,6 @@ import InjectDefinition from './InjectDefinition'; import { PermissionsContext } from '../Context'; import { useHelper } from '../../../../store'; import { useAppDispatch } from '../../../../utils/hooks'; -import { fetchTeams } from '../../../../actions/teams/team-actions'; import useDataLoader from '../../../../utils/hooks/useDataLoader'; import { fetchTags } from '../../../../actions/Tag'; import InjectForm from './InjectForm'; @@ -58,7 +57,6 @@ const UpdateInjectDetails = ({ onUpdateInject, isAtomic = false, drawerRef, - teamsFromExerciseOrScenario, ...props }) => { const { t, tPick } = useFormatter(); @@ -71,7 +69,6 @@ const UpdateInjectDetails = ({ tagsMap: helper.getTagsMap(), })); useDataLoader(() => { - dispatch(fetchTeams()); dispatch(fetchTags()); }); @@ -86,32 +83,7 @@ const UpdateInjectDetails = ({ }; const validate = (values) => { const errors = {}; - if (openDetails && contractContent && Array.isArray(contractContent.fields)) { - contractContent.fields - .filter( - (f) => !['teams', 'assets', 'assetgroups', 'articles', 'challenges', 'attachments', 'expectations'].includes( - f.key, - ), - ) - .forEach((field) => { - const value = values[field.key]; - if (field.mandatory && (value === undefined || R.isEmpty(value))) { - errors[field.key] = t('This field is required.'); - } - if (field.mandatoryGroups) { - const { mandatoryGroups } = field; - const conditionOk = mandatoryGroups?.some((mandatoryKey) => { - const v = values[mandatoryKey]; - return v !== undefined && !R.isEmpty(v); - }); - // If condition are not filled - if (!conditionOk) { - const labels = mandatoryGroups.map((key) => contractContent.fields.find((f) => f.key === key).label).join(', '); - errors[field.key] = t(`One of this field is required : ${labels}.`); - } - } - }); - } + const requiredFields = [ 'inject_title', 'inject_depends_duration_days', @@ -209,7 +181,7 @@ const UpdateInjectDetails = ({ inject_asset_groups: assetGroupIds, inject_documents: documents, inject_depends_duration, - inject_depends_on: data.inject_depends_on, + inject_depends_on: data.inject_depends_on ? data.inject_depends_on : [], }; await onUpdateInject(values); } @@ -324,7 +296,7 @@ const UpdateInjectDetails = ({
} /> - {tPick(contractContent.label)} + {contractContent !== null ? tPick(contractContent.label) : ''}
({ +const useStyles = makeStyles((theme) => ({ injectorContract: { margin: '10px 0 20px 0', width: '100%', @@ -24,66 +29,85 @@ const useStyles = makeStyles((theme) => ({ }, })); -const UpdateInjectLogicalChains = ({ - inject, - handleClose, - onUpdateInject, - injects, -}) => { +interface Props { + inject: Inject, + handleClose: () => void; + onUpdateInject?: (data: Inject[]) => Promise; + injects?: InjectOutputType[], +} + +const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpdateInject, injects }) => { const { t, tPick } = useFormatter(); const classes = useStyles(); + const { injectsMap } = useHelper((helper: InjectHelper) => ({ + injectsMap: helper.getInjectsMap(), + })); + const initialValues = { ...inject, - inject_depends_to: injects - .filter((currentInject) => currentInject.inject_depends_on === inject.inject_id) - .map((currentInject) => currentInject.inject_id), + inject_depends_to: injects !== undefined ? injects + .filter((currentInject) => currentInject.inject_depends_on !== undefined + && currentInject.inject_depends_on !== null + && currentInject.inject_depends_on + .find((searchInject) => searchInject.dependency_relationship?.inject_parent_id === inject.inject_id) + !== undefined) + .flatMap((currentInject) => { + return currentInject.inject_depends_on; + }) : undefined, + inject_depends_on: inject.inject_depends_on, }; - const onSubmit = async (data) => { + const onSubmit = async (data: Inject & { inject_depends_to: InjectDependency[] }) => { const injectUpdate = { ...data, inject_id: data.inject_id, - inject_injector_contract: data.inject_injector_contract.injector_contract_id, + inject_injector_contract: data.inject_injector_contract?.injector_contract_id, inject_depends_on: data.inject_depends_on, }; - const injectsToUpdate = []; + const injectsToUpdate: Inject[] = []; - const childrenIds = data.inject_depends_to; + const childrenIds = data.inject_depends_to.map((childrenInject: InjectDependency) => childrenInject.dependency_relationship?.inject_children_id); - const injectsWithoutDependencies = injects - .filter((currentInject) => currentInject.inject_depends_on === data.inject_id + const injectsWithoutDependencies = injects ? injects + .filter((currentInject) => currentInject.inject_depends_on !== null + && currentInject.inject_depends_on?.find((searchInject) => searchInject.dependency_relationship?.inject_parent_id === data.inject_id) !== undefined && !childrenIds.includes(currentInject.inject_id)) .map((currentInject) => { return { - ...currentInject, + ...injectsMap[currentInject.inject_id], inject_id: currentInject.inject_id, inject_injector_contract: currentInject.inject_injector_contract.injector_contract_id, inject_depends_on: undefined, - }; - }); + } as unknown as Inject; + }) : []; injectsToUpdate.push(...injectsWithoutDependencies); childrenIds.forEach((childrenId) => { + if (injects === undefined || childrenId === undefined) return; const children = injects.find((currentInject) => currentInject.inject_id === childrenId); if (children !== undefined) { - const injectChildrenUpdate = { - ...children, + const injectDependsOnUpdate = data.inject_depends_to + .find((dependsTo) => dependsTo.dependency_relationship?.inject_children_id === childrenId); + + const injectChildrenUpdate: Inject = { + ...injectsMap[children.inject_id], inject_id: children.inject_id, inject_injector_contract: children.inject_injector_contract.injector_contract_id, - inject_depends_on: inject.inject_id, + inject_depends_on: injectDependsOnUpdate ? [injectDependsOnUpdate] : [], }; injectsToUpdate.push(injectChildrenUpdate); } }); - - await onUpdateInject([injectUpdate, ...injectsToUpdate]); + if (onUpdateInject) { + await onUpdateInject([injectUpdate as Inject, ...injectsToUpdate]); + } handleClose(); }; - const injectorContractContent = JSON.parse(inject.inject_injector_contract.injector_contract_content); + const injectorContractContent = inject.inject_injector_contract?.injector_contract_content ? JSON.parse(inject.inject_injector_contract?.injector_contract_content) : undefined; return ( <> @@ -91,7 +115,7 @@ const UpdateInjectLogicalChains = ({ classes={{ root: classes.injectorContractHeader }} avatar={injectorContractContent?.config?.type ? : } - title={inject?.contract_attack_patterns_external_ids?.join(', ')} + title={inject?.inject_attack_patterns?.map((value) => value.attack_pattern_external_id)?.join(', ')} action={
{inject?.inject_injector_contract?.injector_contract_platforms?.map( (platform) => , @@ -133,7 +157,7 @@ const UpdateInjectLogicalChains = ({ variant="contained" color="secondary" type="submit" - disabled={Object.keys(errors).length > 0 } + disabled={errors !== undefined && Object.keys(errors).length > 0 } > {t('Update')} diff --git a/openbas-front/src/admin/components/common/injects/teams/InjectTeamsList.tsx b/openbas-front/src/admin/components/common/injects/teams/InjectTeamsList.tsx index 1c960866ec..27c48968f4 100644 --- a/openbas-front/src/admin/components/common/injects/teams/InjectTeamsList.tsx +++ b/openbas-front/src/admin/components/common/injects/teams/InjectTeamsList.tsx @@ -1,12 +1,14 @@ import { ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText } from '@mui/material'; import { GroupsOutlined } from '@mui/icons-material'; -import React, { FunctionComponent, useContext } from 'react'; +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; import { makeStyles } from '@mui/styles'; +import * as R from 'ramda'; import ItemTags from '../../../../../components/ItemTags'; import TeamPopover from '../../../components/teams/TeamPopover'; -import type { TeamStore } from '../../../../../actions/teams/Team'; import type { Theme } from '../../../../../components/Theme'; -import { PermissionsContext } from '../../Context'; +import { PermissionsContext, TeamContext } from '../../Context'; +import { findTeams } from '../../../../../actions/teams/team-actions'; +import type { TeamOutput } from '../../../../../utils/api-types'; const useStyles = makeStyles((theme: Theme) => ({ item: { @@ -23,17 +25,26 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface Props { - teams: Array; + teamIds: Array; handleRemoveTeam: (teamId: string) => void; } const InjectTeamsList: FunctionComponent = ({ - teams, + teamIds, handleRemoveTeam, }) => { // Standard hooks const classes = useStyles(); const { permissions } = useContext(PermissionsContext); + const { computeTeamUsersEnabled } = useContext(TeamContext); + + const [teams, setTeams] = useState([]); + const sortTeams = R.sortWith( + [R.ascend(R.prop('team_name'))], + ); + useEffect(() => { + findTeams(teamIds).then((result) => setTeams(sortTeams(result.data))); + }, [teamIds]); return ( <> @@ -56,7 +67,7 @@ const InjectTeamsList: FunctionComponent = ({ {team.team_users_number}
- {team.team_users_enabled_number} + {computeTeamUsersEnabled?.(team.team_id)}
diff --git a/openbas-front/src/admin/components/components/documents/DocumentPopover.js b/openbas-front/src/admin/components/components/documents/DocumentPopover.js index 2302f91c4f..8ddbb8a224 100644 --- a/openbas-front/src/admin/components/components/documents/DocumentPopover.js +++ b/openbas-front/src/admin/components/components/documents/DocumentPopover.js @@ -164,7 +164,7 @@ const DocumentPopover = (props) => { )} {!onRemoveDocument && ( - + {t('Delete')} )} diff --git a/openbas-front/src/admin/components/components/teams/TeamAddPlayers.tsx b/openbas-front/src/admin/components/components/teams/TeamAddPlayers.tsx index 5ced78960f..bbd0211a2a 100644 --- a/openbas-front/src/admin/components/components/teams/TeamAddPlayers.tsx +++ b/openbas-front/src/admin/components/components/teams/TeamAddPlayers.tsx @@ -98,7 +98,7 @@ const TeamAddPlayers: React.FC = ({ addedUsersIds, teamId }) => { )(R.values(usersMap)); const submitAddUsers = async () => { - await onAddUsersTeam(teamId, usersIds); + await onAddUsersTeam?.(teamId, usersIds); setOpen(false); setKeyword(''); setUsersIds([]); diff --git a/openbas-front/src/admin/components/components/teams/TeamPopover.tsx b/openbas-front/src/admin/components/components/teams/TeamPopover.tsx index 7b4371eb3d..c84001e2a5 100644 --- a/openbas-front/src/admin/components/components/teams/TeamPopover.tsx +++ b/openbas-front/src/admin/components/components/teams/TeamPopover.tsx @@ -7,7 +7,7 @@ import TeamForm from './TeamForm'; import { useFormatter } from '../../../../components/i18n'; import { useAppDispatch } from '../../../../utils/hooks'; import Transition from '../../../../components/common/Transition'; -import type { TeamUpdateInput } from '../../../../utils/api-types'; +import type { TeamOutput, TeamUpdateInput } from '../../../../utils/api-types'; import { Option, organizationOption, tagOptions } from '../../../../utils/Option'; import { useHelper } from '../../../../store'; import type { OrganizationHelper, TagHelper } from '../../../../actions/helper'; @@ -17,7 +17,7 @@ import { TeamContext } from '../../common/Context'; import type { ExercisesHelper } from '../../../../actions/exercises/exercise-helper'; interface TeamPopoverProps { - team: TeamStore; + team: TeamStore | TeamOutput; managePlayers?: () => void, disabled?: boolean, openEditOnInit?: boolean, diff --git a/openbas-front/src/admin/components/integrations/injectors/injector_contracts/InjectorContractCustomForm.js b/openbas-front/src/admin/components/integrations/injectors/injector_contracts/InjectorContractCustomForm.js index ddab9d495e..9abbcecec7 100644 --- a/openbas-front/src/admin/components/integrations/injectors/injector_contracts/InjectorContractCustomForm.js +++ b/openbas-front/src/admin/components/integrations/injectors/injector_contracts/InjectorContractCustomForm.js @@ -154,12 +154,14 @@ const InjectorContractForm = (props) => { onClick={handleClose} style={{ marginRight: 10 }} disabled={submitting} + variant="contained" > {t('Cancel')}