diff --git a/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java b/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java index 8da6432f0a..311fa36dfb 100644 --- a/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java +++ b/openbas-api/src/main/java/io/openbas/rest/document/DocumentService.java @@ -102,4 +102,12 @@ public void deleteDocument(String documentId) { } }); } + + public List documentsForScenario(String scenarioId) { + return this.documentRepository.findAllDistinctByScenarioId(scenarioId); + } + + public List documentsForSimulation(String simulationId) { + return this.documentRepository.findAllDistinctBySimulationId(simulationId); + } } diff --git a/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java b/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java index 7b94a3f390..78a57b65cf 100644 --- a/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/exercise/ExerciseApi.java @@ -21,6 +21,7 @@ import io.openbas.database.repository.*; import io.openbas.database.specification.*; import io.openbas.rest.custom_dashboard.CustomDashboardService; +import io.openbas.rest.document.DocumentService; import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.exception.InputValidationException; import io.openbas.rest.exercise.exports.ExportOptions; @@ -96,12 +97,16 @@ public class ExerciseApi extends RestBehavior { // endregion // region services + private final AssetGroupService assetGroupService; + private final EndpointService endpointService; private final FileService fileService; private final InjectService injectService; private final ExerciseService exerciseService; private final TeamService teamService; private final ExportService exportService; private final ActionMetricCollector actionMetricCollector; + private final ChannelService channelService; + private final DocumentService documentService; // endregion @@ -903,4 +908,42 @@ public CheckExerciseRulesOutput checkIfRuleApplies( } // endregion + + // region asset groups, endpoints, documents and channels + @GetMapping(EXERCISE_URI + "/{exerciseId}/asset_groups") + @Operation( + summary = + "Get asset groups. Can only be called if the user has access to the given simulation.", + description = "Get all asset groups used by injects for a given simulation") + @PreAuthorize("isObserver()") + public List assetGroups(@PathVariable String exerciseId) { + return this.assetGroupService.assetGroupsForSimulation(exerciseId); + } + + @GetMapping(EXERCISE_URI + "/{exerciseId}/channels") + @Operation( + summary = "Get channels. Can only be called if the user has access to the given simulation.", + description = "Get all channels used by articles for a given simulation") + @PreAuthorize("isObserver()") + public Iterable channels(@PathVariable String exerciseId) { + return this.channelService.channelsForSimulation(exerciseId); + } + + @GetMapping(EXERCISE_URI + "/{exerciseId}/endpoints") + @Operation( + summary = "Get endpoints. Can only be called if the user has access to the given simulation.", + description = "Get all endpoints used by injects for a given simulation") + @PreAuthorize("isObserver()") + public List endpoints(@PathVariable String exerciseId) { + return this.endpointService.endpointsForSimulation(exerciseId); + } + + @GetMapping(EXERCISE_URI + "/{exerciseId}/documents") + @Operation( + summary = "Get documents. Can only be called if the user has access to the given simulation.", + description = "Get all documents used by injects for a given simulation") + public List documents(@PathVariable String exerciseId) { + return this.documentService.documentsForSimulation(exerciseId); + } + // end region } diff --git a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioApi.java b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioApi.java index 5db5412786..5c08379613 100644 --- a/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/scenario/ScenarioApi.java @@ -16,16 +16,14 @@ import io.openbas.database.raw.RawPaginationScenario; import io.openbas.database.repository.*; import io.openbas.rest.custom_dashboard.CustomDashboardService; +import io.openbas.rest.document.DocumentService; import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.rest.exercise.form.LessonsInput; import io.openbas.rest.exercise.form.ScenarioTeamPlayersEnableInput; import io.openbas.rest.helper.RestBehavior; import io.openbas.rest.scenario.form.*; import io.openbas.rest.team.output.TeamOutput; -import io.openbas.service.ImportService; -import io.openbas.service.ScenarioService; -import io.openbas.service.ScenarioToExerciseService; -import io.openbas.service.TeamService; +import io.openbas.service.*; import io.openbas.utils.FilterUtilsJpa; import io.openbas.utils.pagination.SearchPaginationInput; import io.swagger.v3.oas.annotations.Operation; @@ -66,6 +64,10 @@ public class ScenarioApi extends RestBehavior { private final ImportService importService; private final ScenarioService scenarioService; private final TeamService teamService; + private final AssetGroupService assetGroupService; + private final EndpointService endpointService; + private final ChannelService channelService; + private final DocumentService documentService; @PostMapping(SCENARIO_URI) @RBAC(actionPerformed = Action.CREATE, resourceType = ResourceType.SCENARIO) @@ -393,4 +395,42 @@ public CheckScenarioRulesOutput checkIfRuleApplies( .rulesFound(this.scenarioService.checkIfTagRulesApplies(scenario, input.getNewTags())) .build(); } + + // region asset groups, endpoints, documents and channels + @GetMapping(SCENARIO_URI + "/{scenarioId}/asset_groups") + @Operation( + summary = + "Get asset groups. Can only be called if the user has access to the given scenario.", + description = "Get all asset groups used by injects for a given scenario") + @PreAuthorize("isObserver()") + public List assetGroups(@PathVariable String scenarioId) { + return this.assetGroupService.assetGroupsForScenario(scenarioId); + } + + @GetMapping(SCENARIO_URI + "/{scenarioId}/channels") + @Operation( + summary = "Get channels. Can only be called if the user has access to the given scenario.", + description = "Get all channels used by articles for a given scenario") + @PreAuthorize("isObserver()") + public Iterable channels(@PathVariable String scenarioId) { + return this.channelService.channelsForScenario(scenarioId); + } + + @GetMapping(SCENARIO_URI + "/{scenarioId}/endpoints") + @Operation( + summary = "Get endpoints. Can only be called if the user has access to the given scenario.", + description = "Get all endpoints used by injects for a given scenario") + @PreAuthorize("isObserver()") + public List endpoints(@PathVariable String scenarioId) { + return this.endpointService.endpointsForScenario(scenarioId); + } + + @GetMapping(SCENARIO_URI + "/{scenarioId}/documents") + @Operation( + summary = "Get documents. Can only be called if the user has access to the given scenario.", + description = "Get all documents used by injects for a given scenario") + public List documents(@PathVariable String scenarioId) { + return this.documentService.documentsForScenario(scenarioId); + } + // end region } diff --git a/openbas-api/src/main/java/io/openbas/service/AssetGroupService.java b/openbas-api/src/main/java/io/openbas/service/AssetGroupService.java index 5664f4ce9b..ab26aee93a 100644 --- a/openbas-api/src/main/java/io/openbas/service/AssetGroupService.java +++ b/openbas-api/src/main/java/io/openbas/service/AssetGroupService.java @@ -52,6 +52,18 @@ public List assetGroups(@NotBlank final List assetGroupIds) return computeDynamicAssets(assetGroups); } + public List assetGroupsForSimulation(@NotBlank final String simulationId) { + List assetGroups = + fromIterable(this.assetGroupRepository.findDistinctByInjectsSimulationId(simulationId)); + return computeDynamicAssets(assetGroups); + } + + public List assetGroupsForScenario(@NotBlank final String scenarioId) { + List assetGroups = + fromIterable(this.assetGroupRepository.findDistinctByInjectsScenarioId(scenarioId)); + return computeDynamicAssets(assetGroups); + } + public AssetGroup assetGroup(@NotBlank final String assetGroupId) { AssetGroup assetGroup = this.assetGroupRepository diff --git a/openbas-api/src/main/java/io/openbas/service/ChannelService.java b/openbas-api/src/main/java/io/openbas/service/ChannelService.java index 931d231ea4..c820f99b40 100644 --- a/openbas-api/src/main/java/io/openbas/service/ChannelService.java +++ b/openbas-api/src/main/java/io/openbas/service/ChannelService.java @@ -16,6 +16,7 @@ import io.openbas.rest.exception.ElementNotFoundException; import io.openbas.utils.ExpectationUtils; import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotBlank; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -153,4 +154,12 @@ private void processByValidationType( isaNewExpectationResult, channelExpectations, parentExpectations, playerByTeam); injectExpectationExecutionRepository.saveAll(toUpdate); } + + public List channelsForSimulation(@NotBlank final String simulationId) { + return fromIterable(this.channelRepository.findDistinctByArticlesExerciseId(simulationId)); + } + + public List channelsForScenario(@NotBlank final String scenarioId) { + return fromIterable(this.channelRepository.findDistinctByArticlesScenarioId(scenarioId)); + } } diff --git a/openbas-api/src/main/java/io/openbas/service/EndpointService.java b/openbas-api/src/main/java/io/openbas/service/EndpointService.java index fd98ae6006..24f09b5471 100644 --- a/openbas-api/src/main/java/io/openbas/service/EndpointService.java +++ b/openbas-api/src/main/java/io/openbas/service/EndpointService.java @@ -639,6 +639,14 @@ public String generateUpgradeCommand( platform, upgradeName, adminToken, installationDir, serviceNameOrPrefix); } + public List endpointsForScenario(String scenarioId) { + return this.endpointRepository.findDistinctByInjectsScenarioId(scenarioId); + } + + public List endpointsForSimulation(String simulationId) { + return this.endpointRepository.findDistinctByInjectsExerciseId(simulationId); + } + // -- OPTIONS -- public List getOptionsByNameLinkedToFindings( String searchText, String sourceId, Pageable pageable) { diff --git a/openbas-front/src/actions/asset_groups/assetgroup-action.ts b/openbas-front/src/actions/asset_groups/assetgroup-action.ts index f19de7423d..85a9be7bd1 100644 --- a/openbas-front/src/actions/asset_groups/assetgroup-action.ts +++ b/openbas-front/src/actions/asset_groups/assetgroup-action.ts @@ -83,3 +83,17 @@ export const searchAssetGroupLinkedToFindingsAsOption = (searchText: string = '' export const searchAssetGroupByIdAsOption = (ids: string[]) => { return simplePostCall(`${ASSET_GROUP_URI}/options`, ids); }; + +// -- SIMULATIONS -- + +export const fetchSimulationAssetGroups = (simulationId: string) => (dispatch: Dispatch) => { + const uri = `/api/exercises/${simulationId}/asset_groups`; + return getReferential(arrayOfAssetGroups, uri)(dispatch); +}; + +// -- SCENARIOS -- + +export const fetchScenarioAssetGroups = (scenarioId: string) => (dispatch: Dispatch) => { + const uri = `/api/scenarios/${scenarioId}/asset_groups`; + return getReferential(arrayOfAssetGroups, uri)(dispatch); +}; diff --git a/openbas-front/src/actions/assets/endpoint-actions.ts b/openbas-front/src/actions/assets/endpoint-actions.ts index 0d9cc6ba82..3ea617509f 100644 --- a/openbas-front/src/actions/assets/endpoint-actions.ts +++ b/openbas-front/src/actions/assets/endpoint-actions.ts @@ -68,3 +68,17 @@ export const searchEndpointLinkedToFindingsAsOption = (searchText: string = '', export const importEndpoints = (file: FormData, targetType: string) => { return simplePostCall(`/api/mappers/import/csv?targetType=` + targetType, file); }; + +// -- SIMULATIONS -- + +export const fetchSimulationEndpoints = (simulationId: string) => (dispatch: Dispatch) => { + const uri = `/api/exercises/${simulationId}/endpoints`; + return getReferential(arrayOfEndpoints, uri)(dispatch); +}; + +// -- SCENARIOS -- + +export const fetchScenarioEndpoints = (scenarioId: string) => (dispatch: Dispatch) => { + const uri = `/api/scenarios/${scenarioId}/endpoints`; + return getReferential(arrayOfEndpoints, uri)(dispatch); +}; diff --git a/openbas-front/src/actions/channels/channel-action.js b/openbas-front/src/actions/channels/channel-action.js index e7cc07a6ed..74d2f93675 100644 --- a/openbas-front/src/actions/channels/channel-action.js +++ b/openbas-front/src/actions/channels/channel-action.js @@ -32,3 +32,17 @@ export const fetchObserverChannel = (exerciseId, channelId) => (dispatch) => { const uri = `/api/observer/channels/${exerciseId}/${channelId}`; return getReferential(channelReader, uri)(dispatch); }; + +// -- SIMULATIONS -- + +export const fetchSimulationChannels = simulationId => (dispatch) => { + const uri = `/api/exercises/${simulationId}/channels`; + return getReferential(arrayOfChannels, uri)(dispatch); +}; + +// -- SCENARIOS -- + +export const fetchScenarioChannels = scenarioId => (dispatch) => { + const uri = `/api/scenarios/${scenarioId}/channels`; + return getReferential(arrayOfChannels, uri)(dispatch); +}; diff --git a/openbas-front/src/actions/documents/documents-actions.ts b/openbas-front/src/actions/documents/documents-actions.ts new file mode 100644 index 0000000000..556c7deaf7 --- /dev/null +++ b/openbas-front/src/actions/documents/documents-actions.ts @@ -0,0 +1,18 @@ +import { type Dispatch } from 'redux'; + +import { getReferential } from '../../utils/Action'; +import { arrayOfDocuments } from '../Schema'; + +// -- EXERCISES -- + +export const fetchExerciseDocuments = (exerciseId: string) => (dispatch: Dispatch) => { + const uri = `/api/exercises/${exerciseId}/documents`; + return getReferential(arrayOfDocuments, uri)(dispatch); +}; + +// -- SCENARIOS -- + +export const fetchScenarioDocuments = (scenarioId: string) => (dispatch: Dispatch) => { + const uri = `/api/scenarios/${scenarioId}/documents`; + return getReferential(arrayOfDocuments, uri)(dispatch); +}; diff --git a/openbas-front/src/admin/components/common/Context.ts b/openbas-front/src/admin/components/common/Context.ts index 80867f0821..5c047b808f 100644 --- a/openbas-front/src/admin/components/common/Context.ts +++ b/openbas-front/src/admin/components/common/Context.ts @@ -3,7 +3,42 @@ import { createContext, type ReactElement } from 'react'; import { type FullArticleStore } from '../../../actions/channels/Article'; import { type InjectOutputType, type InjectStore } from '../../../actions/injects/Inject'; import { type Page } from '../../../components/common/queryable/Page'; -import { type Article, type ArticleCreateInput, type ArticleUpdateInput, type Evaluation, type EvaluationInput, type ImportTestSummary, type Inject, type InjectBulkProcessingInput, type InjectBulkUpdateInputs, type InjectsImportInput, type InjectTestStatusOutput, type LessonsAnswer, type LessonsAnswerCreateInput, type LessonsCategory, type LessonsCategoryCreateInput, type LessonsCategoryTeamsInput, type LessonsCategoryUpdateInput, type LessonsQuestion, type LessonsQuestionCreateInput, type LessonsQuestionUpdateInput, type LessonsSendInput, type Objective, type ObjectiveInput, type PublicExercise, type PublicScenario, type Report, type ReportInput, type SearchPaginationInput, type Team, type TeamCreateInput, type TeamOutput, type Variable, type VariableInput } from '../../../utils/api-types'; +import { + type Article, + type ArticleCreateInput, + type ArticleUpdateInput, + type Channel, + type Evaluation, + type EvaluationInput, + type ImportTestSummary, + type Inject, + type InjectBulkProcessingInput, + type InjectBulkUpdateInputs, + type InjectsImportInput, + type InjectTestStatusOutput, + type LessonsAnswer, + type LessonsAnswerCreateInput, + type LessonsCategory, + type LessonsCategoryCreateInput, + type LessonsCategoryTeamsInput, + type LessonsCategoryUpdateInput, + type LessonsQuestion, + type LessonsQuestionCreateInput, + type LessonsQuestionUpdateInput, + type LessonsSendInput, + type Objective, + type ObjectiveInput, + type PublicExercise, + type PublicScenario, + type Report, + type ReportInput, + type SearchPaginationInput, + type Team, + type TeamCreateInput, + type TeamOutput, + type Variable, + type VariableInput, +} from '../../../utils/api-types'; import { type UserStore } from '../teams/players/Player'; export type PermissionsContextType = { @@ -16,6 +51,8 @@ export type PermissionsContextType = { export type ArticleContextType = { previewArticleUrl: (article: FullArticleStore) => string; + fetchChannels: () => Promise; + fetchDocuments: () => Promise; onAddArticle: (data: ArticleCreateInput) => Promise<{ result: string }>; onUpdateArticle: (article: Article, data: ArticleUpdateInput) => string; onDeleteArticle: (article: Article) => string; @@ -156,6 +193,14 @@ export const PermissionsContext = createContext({ }, }); export const ArticleContext = createContext({ + fetchChannels(): Promise { + return new Promise(() => { + }); + }, + fetchDocuments(): Promise { + return new Promise(() => { + }); + }, onAddArticle(_data: ArticleCreateInput): Promise<{ result: string }> { return Promise.resolve({ result: '' }); }, diff --git a/openbas-front/src/admin/components/common/articles/Articles.tsx b/openbas-front/src/admin/components/common/articles/Articles.tsx index da70c27241..ac1dcfff31 100644 --- a/openbas-front/src/admin/components/common/articles/Articles.tsx +++ b/openbas-front/src/admin/components/common/articles/Articles.tsx @@ -7,9 +7,7 @@ import { Link } from 'react-router'; import { makeStyles } from 'tss-react/mui'; import { type FullArticleStore } from '../../../../actions/channels/Article'; -import { fetchChannels } from '../../../../actions/channels/channel-action'; import { type ChannelsHelper } from '../../../../actions/channels/channel-helper'; -import { fetchDocuments } from '../../../../actions/Document'; import { type DocumentHelper } from '../../../../actions/helper'; import Empty from '../../../../components/Empty'; import ExpandableMarkdown from '../../../../components/ExpandableMarkdown'; @@ -49,6 +47,10 @@ const useStyles = makeStyles()(() => ({ interface Props { articles: Article[] } const Articles: FunctionComponent = ({ articles }) => { + // Context + const { previewArticleUrl, fetchChannels, fetchDocuments } = useContext(ArticleContext); + const { permissions } = useContext(PermissionsContext); + // Standard hooks const { classes } = useStyles(); const dispatch = useAppDispatch(); @@ -90,10 +92,6 @@ const Articles: FunctionComponent = ({ articles }) => { filtering.filterAndSort(fullArticles), ); - // Context - const { previewArticleUrl } = useContext(ArticleContext); - const { permissions } = useContext(PermissionsContext); - return (
diff --git a/openbas-front/src/admin/components/components/challenges/ChallengePopover.js b/openbas-front/src/admin/components/components/challenges/ChallengePopover.js index 4c741262ce..50bec15c95 100644 --- a/openbas-front/src/admin/components/components/challenges/ChallengePopover.js +++ b/openbas-front/src/admin/components/components/challenges/ChallengePopover.js @@ -1,5 +1,16 @@ import { MoreVert } from '@mui/icons-material'; -import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, Menu, MenuItem, Slide } from '@mui/material'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, + Menu, + MenuItem, + Slide, +} from '@mui/material'; import * as R from 'ramda'; import { forwardRef, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -16,8 +27,9 @@ const Transition = forwardRef((props, ref) => ( )); Transition.displayName = 'TransitionSlide'; -const ChallengePopover = ({ challenge, documents = [], onRemoveChallenge, inline, disabled = false }) => { +const ChallengePopover = ({ challenge, onRemoveChallenge, inline, disabled = false }) => { // utils + const dispatch = useDispatch(); const { t } = useFormatter(); // states @@ -25,6 +37,7 @@ const ChallengePopover = ({ challenge, documents = [], onRemoveChallenge, inline const [openRemove, setOpenRemove] = useState(false); const [openEdit, setOpenEdit] = useState(false); const [anchorEl, setAnchorEl] = useState(null); + // popover management const { tagsMap } = useHelper(helper => ({ tagsMap: helper.getTagsMap() })); const handlePopoverOpen = (event) => { @@ -66,6 +79,7 @@ const ChallengePopover = ({ challenge, documents = [], onRemoveChallenge, inline onRemoveChallenge(challenge.challenge_id); handleCloseRemove(); }; + // Rendering const challengeTags = tagOptions(challenge.challenge_tags, tagsMap); const initialValues = R.pipe( @@ -133,7 +147,7 @@ const ChallengePopover = ({ challenge, documents = [], onRemoveChallenge, inline onSubmit={onSubmitEdit} handleClose={handleCloseEdit} initialValues={initialValues} - documentsIds={(documents || []).map(i => i.document_id)} + documentsIds={challenge.challenge_documents || []} /> @@ -148,7 +162,7 @@ const ChallengePopover = ({ challenge, documents = [], onRemoveChallenge, inline onSubmit={onSubmitEdit} handleClose={handleCloseEdit} initialValues={initialValues} - documentsIds={(documents || []).map(i => i.document_id)} + documentsIds={challenge.challenge_documents || []} /> )} diff --git a/openbas-front/src/admin/components/scenarios/scenario/articles/ScenarioArticles.tsx b/openbas-front/src/admin/components/scenarios/scenario/articles/ScenarioArticles.tsx index 6fe4ec4ba8..b6a90ae069 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/articles/ScenarioArticles.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/articles/ScenarioArticles.tsx @@ -2,6 +2,8 @@ import { useParams } from 'react-router'; import { fetchScenarioArticles } from '../../../../../actions/channels/article-action'; import { type ArticlesHelper } from '../../../../../actions/channels/article-helper'; +import { fetchScenarioChannels } from '../../../../../actions/channels/channel-action'; +import { fetchScenarioDocuments } from '../../../../../actions/documents/documents-actions'; import { useHelper } from '../../../../../store'; import { type Scenario } from '../../../../../utils/api-types'; import { useAppDispatch } from '../../../../../utils/hooks'; @@ -18,6 +20,8 @@ const ScenarioArticles = () => { const { articles } = useHelper((helper: ArticlesHelper) => ({ articles: helper.getScenarioArticles(scenarioId) })); useDataLoader(() => { dispatch(fetchScenarioArticles(scenarioId)); + dispatch(fetchScenarioDocuments(scenarioId)); + dispatch(fetchScenarioChannels(scenarioId)); }); const context = articleContextForScenario(scenarioId); return ( diff --git a/openbas-front/src/admin/components/scenarios/scenario/articles/articleContextForScenario.ts b/openbas-front/src/admin/components/scenarios/scenario/articles/articleContextForScenario.ts index 9e64844072..fd07c465e9 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/articles/articleContextForScenario.ts +++ b/openbas-front/src/admin/components/scenarios/scenario/articles/articleContextForScenario.ts @@ -1,5 +1,7 @@ import { type FullArticleStore } from '../../../../../actions/channels/Article'; import { addScenarioArticle, deleteScenarioArticle, updateScenarioArticle } from '../../../../../actions/channels/article-action'; +import { fetchScenarioChannels } from '../../../../../actions/channels/channel-action'; +import { fetchScenarioDocuments } from '../../../../../actions/documents/documents-actions'; import { type Article, type ArticleCreateInput, type ArticleUpdateInput, type Scenario } from '../../../../../utils/api-types'; import { useAppDispatch } from '../../../../../utils/hooks'; @@ -7,6 +9,8 @@ const articleContextForScenario = (scenarioId: Scenario['scenario_id']) => { const dispatch = useAppDispatch(); return { previewArticleUrl: (article: FullArticleStore) => `/channels/${scenarioId}/${article.article_fullchannel?.channel_id}?preview=true`, + fetchChannels: () => dispatch(fetchScenarioChannels(scenarioId)), + fetchDocuments: () => dispatch(fetchScenarioDocuments(scenarioId)), onAddArticle: (data: ArticleCreateInput) => dispatch(addScenarioArticle(scenarioId, data)), onUpdateArticle: (article: Article, data: ArticleUpdateInput) => dispatch( updateScenarioArticle(scenarioId, article.article_id, data), diff --git a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx index 302d77b0ca..cd65986132 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx @@ -1,8 +1,12 @@ import { type FunctionComponent, useState } from 'react'; import { useParams } from 'react-router'; +import { fetchScenarioAssetGroups } from '../../../../../actions/asset_groups/assetgroup-action'; +import { fetchScenarioEndpoints } from '../../../../../actions/assets/endpoint-actions'; import { fetchScenarioArticles } from '../../../../../actions/channels/article-action'; import { type ArticlesHelper } from '../../../../../actions/channels/article-helper'; +import { fetchScenarioChannels } from '../../../../../actions/channels/channel-action'; +import { fetchScenarioDocuments } from '../../../../../actions/documents/documents-actions'; import { type ChallengeHelper } from '../../../../../actions/helper'; import { testInject } from '../../../../../actions/inject_test/scenario-inject-test-actions'; import { fetchScenarioInjectsSimple } from '../../../../../actions/injects/inject-action'; @@ -42,6 +46,10 @@ const ScenarioInjects: FunctionComponent = () => { dispatch(fetchScenarioTeams(scenarioId)); dispatch(fetchVariablesForScenario(scenarioId)); dispatch(fetchScenarioArticles(scenarioId)); + dispatch(fetchScenarioEndpoints(scenarioId)); + dispatch(fetchScenarioAssetGroups(scenarioId)); + dispatch(fetchScenarioDocuments(scenarioId)); + dispatch(fetchScenarioChannels(scenarioId)); }); const articleContext = articleContextForScenario(scenarioId); diff --git a/openbas-front/src/admin/components/simulations/simulation/articles/ExerciseArticles.tsx b/openbas-front/src/admin/components/simulations/simulation/articles/ExerciseArticles.tsx index 58a3ad916c..60667946a6 100644 --- a/openbas-front/src/admin/components/simulations/simulation/articles/ExerciseArticles.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/articles/ExerciseArticles.tsx @@ -2,6 +2,8 @@ import { useParams } from 'react-router'; import { fetchExerciseArticles } from '../../../../../actions/channels/article-action'; import { type ArticlesHelper } from '../../../../../actions/channels/article-helper'; +import { fetchSimulationChannels } from '../../../../../actions/channels/channel-action'; +import { fetchExerciseDocuments } from '../../../../../actions/documents/documents-actions'; import { useHelper } from '../../../../../store'; import { type Exercise } from '../../../../../utils/api-types'; import { useAppDispatch } from '../../../../../utils/hooks'; @@ -18,6 +20,8 @@ const ExerciseArticles = () => { const { articles } = useHelper((helper: ArticlesHelper) => ({ articles: helper.getExerciseArticles(exerciseId) })); useDataLoader(() => { dispatch(fetchExerciseArticles(exerciseId)); + dispatch(fetchExerciseDocuments(exerciseId)); + dispatch(fetchSimulationChannels(exerciseId)); }); const context = articleContextForExercise(exerciseId); return ( diff --git a/openbas-front/src/admin/components/simulations/simulation/articles/articleContextForExercise.ts b/openbas-front/src/admin/components/simulations/simulation/articles/articleContextForExercise.ts index bb24ca1577..6f70cf5af1 100644 --- a/openbas-front/src/admin/components/simulations/simulation/articles/articleContextForExercise.ts +++ b/openbas-front/src/admin/components/simulations/simulation/articles/articleContextForExercise.ts @@ -1,5 +1,7 @@ import { type FullArticleStore } from '../../../../../actions/channels/Article'; import { addExerciseArticle, deleteExerciseArticle, updateExerciseArticle } from '../../../../../actions/channels/article-action'; +import { fetchSimulationChannels } from '../../../../../actions/channels/channel-action'; +import { fetchExerciseDocuments } from '../../../../../actions/documents/documents-actions'; import { type Article, type ArticleCreateInput, type ArticleUpdateInput, type Exercise } from '../../../../../utils/api-types'; import { useAppDispatch } from '../../../../../utils/hooks'; @@ -7,6 +9,8 @@ const articleContextForExercise = (exerciseId: Exercise['exercise_id']) => { const dispatch = useAppDispatch(); return { previewArticleUrl: (article: FullArticleStore) => `/channels/${exerciseId}/${article.article_fullchannel?.channel_id}?preview=true`, + fetchChannels: () => dispatch(fetchSimulationChannels(exerciseId)), + fetchDocuments: () => dispatch(fetchExerciseDocuments(exerciseId)), onAddArticle: (data: ArticleCreateInput) => dispatch(addExerciseArticle(exerciseId, data)), onUpdateArticle: (article: Article, data: ArticleUpdateInput) => dispatch( updateExerciseArticle(exerciseId, article.article_id, data), diff --git a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx index b62c136bd9..0ccdf64aa9 100644 --- a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx @@ -4,8 +4,12 @@ import { type FunctionComponent, useState } from 'react'; import { useParams } from 'react-router'; import { makeStyles } from 'tss-react/mui'; +import { fetchSimulationAssetGroups } from '../../../../../actions/asset_groups/assetgroup-action'; +import { fetchSimulationEndpoints } from '../../../../../actions/assets/endpoint-actions'; import { fetchExerciseArticles } from '../../../../../actions/channels/article-action'; import { type ArticlesHelper } from '../../../../../actions/channels/article-helper'; +import { fetchSimulationChannels } from '../../../../../actions/channels/channel-action'; +import { fetchExerciseDocuments } from '../../../../../actions/documents/documents-actions'; import { fetchExerciseInjectExpectations, fetchExerciseTeams } from '../../../../../actions/Exercise'; import { type ExercisesHelper } from '../../../../../actions/exercises/exercise-helper'; import { type ChallengeHelper } from '../../../../../actions/helper'; @@ -70,6 +74,10 @@ const ExerciseInjects: FunctionComponent = () => { dispatch(fetchExerciseArticles(exerciseId)); dispatch(fetchVariablesForExercise(exerciseId)); dispatch(fetchExerciseInjectExpectations(exerciseId)); + dispatch(fetchSimulationEndpoints(exerciseId)); + dispatch(fetchSimulationAssetGroups(exerciseId)); + dispatch(fetchExerciseDocuments(exerciseId)); + dispatch(fetchSimulationChannels(exerciseId)); }); const articleContext = articleContextForExercise(exerciseId); diff --git a/openbas-front/src/admin/components/simulations/simulation/timeline/TimelineOverview.tsx b/openbas-front/src/admin/components/simulations/simulation/timeline/TimelineOverview.tsx index 0cf43cc71d..7f53723602 100644 --- a/openbas-front/src/admin/components/simulations/simulation/timeline/TimelineOverview.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/timeline/TimelineOverview.tsx @@ -4,8 +4,12 @@ import { useState } from 'react'; import { Link, useParams } from 'react-router'; import { makeStyles } from 'tss-react/mui'; +import { fetchSimulationAssetGroups } from '../../../../../actions/asset_groups/assetgroup-action'; +import { fetchSimulationEndpoints } from '../../../../../actions/assets/endpoint-actions'; import { fetchExerciseArticles } from '../../../../../actions/channels/article-action'; import type { ArticlesHelper } from '../../../../../actions/channels/article-helper'; +import { fetchSimulationChannels } from '../../../../../actions/channels/channel-action'; +import { fetchExerciseDocuments } from '../../../../../actions/documents/documents-actions'; import { fetchExerciseTeams } from '../../../../../actions/Exercise'; import { type ExercisesHelper } from '../../../../../actions/exercises/exercise-helper'; import { fetchExerciseInjects, updateInjectForExercise } from '../../../../../actions/Inject'; @@ -82,6 +86,10 @@ const TimelineOverview = () => { dispatch(fetchExerciseArticles(exerciseId)); dispatch(fetchVariablesForExercise(exerciseId)); dispatch(fetchExerciseInjects(exerciseId)); + dispatch(fetchSimulationEndpoints(exerciseId)); + dispatch(fetchSimulationAssetGroups(exerciseId)); + dispatch(fetchExerciseDocuments(exerciseId)); + dispatch(fetchSimulationChannels(exerciseId)); }); // Sort diff --git a/openbas-model/src/main/java/io/openbas/database/model/Channel.java b/openbas-model/src/main/java/io/openbas/database/model/Channel.java index b7444dfb1e..d400420a63 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Channel.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Channel.java @@ -93,6 +93,12 @@ public class Channel implements Base { @Transient private final ResourceType resourceType = ResourceType.CHANNEL; + // The following field is not accessed by our code but is necessary for the ORM mapping + // (findDistinctByArticlesExerciseId, findDistinctByArticlesScenarioId...). + @OneToMany(mappedBy = "channel", fetch = FetchType.LAZY) + @JsonIgnore + private List
articles; + @Override public String getId() { return id; diff --git a/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java b/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java index 7d09ccacc1..0abb1cae15 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Endpoint.java @@ -1,5 +1,6 @@ package io.openbas.database.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.hypersistence.utils.hibernate.type.array.StringArrayType; @@ -11,8 +12,7 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.*; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; import org.hibernate.annotations.Type; @EqualsAndHashCode(callSuper = true) @@ -107,6 +107,18 @@ public enum PLATFORM_TYPE { @JsonSerialize(using = MultiModelDeserializer.class) private List agents = new ArrayList<>(); + // -- INJECT -- + + @Getter + @Setter(AccessLevel.NONE) + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "injects_assets", + joinColumns = @JoinColumn(name = "asset_id"), + inverseJoinColumns = @JoinColumn(name = "inject_id")) + @JsonIgnore + private List injects = new ArrayList<>(); + public void setHostname(String hostname) { this.hostname = hostname.toLowerCase(); } diff --git a/openbas-model/src/main/java/io/openbas/database/repository/AssetGroupRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/AssetGroupRepository.java index 706bcb8f18..58ee7252e7 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/AssetGroupRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/AssetGroupRepository.java @@ -40,6 +40,18 @@ public interface AssetGroupRepository Optional findByExternalReference(String externalReference); + @Query( + "SELECT ag FROM AssetGroup ag " + + "WHERE ag.id IN (SELECT DISTINCT ag2.id FROM AssetGroup ag2 " + + "JOIN ag2.injects i WHERE i.scenario.id = :scenarioId)") + List findDistinctByInjectsScenarioId(String scenarioId); + + @Query( + "SELECT ag FROM AssetGroup ag " + + "WHERE ag.id IN (SELECT DISTINCT ag2.id FROM AssetGroup ag2 " + + "JOIN ag2.injects i WHERE i.exercise.id = :simulationId)") + List findDistinctByInjectsSimulationId(String simulationId); + /** * Returns the raw asset group having the ids passed in parameter * diff --git a/openbas-model/src/main/java/io/openbas/database/repository/ChannelRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/ChannelRepository.java index b939c949e2..aa2f6abf95 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/ChannelRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/ChannelRepository.java @@ -16,4 +16,8 @@ public interface ChannelRepository Optional findById(@NotNull String id); List findByNameIgnoreCase(String name); + + List findDistinctByArticlesExerciseId(String simulationId); + + List findDistinctByArticlesScenarioId(String scenarioId); } diff --git a/openbas-model/src/main/java/io/openbas/database/repository/DocumentRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/DocumentRepository.java index b84b844b67..c0e5f4088d 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/DocumentRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/DocumentRepository.java @@ -84,4 +84,60 @@ Optional findByIdGranted( @NotNull @EntityGraph(value = "Document.tags-scenarios-exercises", type = EntityGraph.EntityGraphType.LOAD) Page findAll(@NotNull Specification spec, @NotNull Pageable pageable); + + @Query( + value = + "SELECT DISTINCT d.* FROM documents d " + + "WHERE d.document_id IN (" + + " SELECT ad.document_id FROM articles_documents ad " + + " JOIN articles a ON a.article_id = ad.article_id " + + " WHERE a.article_scenario = :scenarioId " + + " UNION " + + " SELECT id.document_id FROM injects_documents id " + + " JOIN injects i ON i.inject_id = id.inject_id " + + " WHERE i.inject_scenario = :scenarioId " + + " UNION " + + " SELECT cd.document_id FROM challenges_documents cd " + + " WHERE cd.challenge_id IN (" + + " SELECT DISTINCT challenge_id FROM (" + + " SELECT jsonb_array_elements_text(i.inject_content::jsonb->'challenges')::varchar as challenge_id " + + " FROM injects i " + + " WHERE i.inject_scenario = :scenarioId " + + " AND i.inject_content IS NOT NULL " + + " AND (i.inject_content::jsonb->'challenges') IS NOT NULL " // Check if key + // exists + + " AND jsonb_typeof(i.inject_content::jsonb->'challenges') = 'array'" + + " ) challenges" + + " )" + + ")", + nativeQuery = true) + List findAllDistinctByScenarioId(@Param("scenarioId") String scenarioId); + + @Query( + value = + "SELECT DISTINCT d.* FROM documents d " + + "WHERE d.document_id IN (" + + " SELECT ad.document_id FROM articles_documents ad " + + " JOIN articles a ON a.article_id = ad.article_id " + + " WHERE a.article_exercise = :simulationId " + + " UNION " + + " SELECT id.document_id FROM injects_documents id " + + " JOIN injects i ON i.inject_id = id.inject_id " + + " WHERE i.inject_exercise = :simulationId " + + " UNION " + + " SELECT cd.document_id FROM challenges_documents cd " + + " WHERE cd.challenge_id IN (" + + " SELECT DISTINCT challenge_id FROM (" + + " SELECT jsonb_array_elements_text(i.inject_content::jsonb->'challenges')::varchar as challenge_id " + + " FROM injects i " + + " WHERE i.inject_exercise = :simulationId " + + " AND i.inject_content IS NOT NULL " + + " AND (i.inject_content::jsonb->'challenges') IS NOT NULL " // Check if key + // exists + + " AND jsonb_typeof(i.inject_content::jsonb->'challenges') = 'array'" + + " ) challenges" + + " )" + + ")", + nativeQuery = true) + List findAllDistinctBySimulationId(@Param("simulationId") String simulationId); } diff --git a/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java index 550aa0eb06..a285085638 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/EndpointRepository.java @@ -122,4 +122,8 @@ List findAllByNameLinkedToFindingsWithContext( + ";", nativeQuery = true) List findForIndexing(@Param("from") Instant from); + + List findDistinctByInjectsScenarioId(String scenarioId); + + List findDistinctByInjectsExerciseId(String exerciseId); }