Skip to content

[backend] feat: RBAC update most fetch API calls from frontend for simulations and scenarios to use simulation/scenario specifi endpoints. #3664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 18, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,12 @@ public void deleteDocument(String documentId) {
}
});
}

public List<Document> documentsForScenario(String scenarioId) {
return this.documentRepository.findAllDistinctByScenarioId(scenarioId);
}

public List<Document> documentsForSimulation(String simulationId) {
return this.documentRepository.findAllDistinctBySimulationId(simulationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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<AssetGroup> 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<Channel> 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<Endpoint> 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<Document> documents(@PathVariable String exerciseId) {
return this.documentService.documentsForSimulation(exerciseId);
}
// end region
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<AssetGroup> 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<Channel> 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<Endpoint> 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<Document> documents(@PathVariable String scenarioId) {
return this.documentService.documentsForScenario(scenarioId);
}
// end region
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ public List<AssetGroup> assetGroups(@NotBlank final List<String> assetGroupIds)
return computeDynamicAssets(assetGroups);
}

public List<AssetGroup> assetGroupsForSimulation(@NotBlank final String simulationId) {
List<AssetGroup> assetGroups =
fromIterable(this.assetGroupRepository.findDistinctByInjectsSimulationId(simulationId));
return computeDynamicAssets(assetGroups);
}

public List<AssetGroup> assetGroupsForScenario(@NotBlank final String scenarioId) {
List<AssetGroup> assetGroups =
fromIterable(this.assetGroupRepository.findDistinctByInjectsScenarioId(scenarioId));
return computeDynamicAssets(assetGroups);
}

public AssetGroup assetGroup(@NotBlank final String assetGroupId) {
AssetGroup assetGroup =
this.assetGroupRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -153,4 +154,12 @@ private void processByValidationType(
isaNewExpectationResult, channelExpectations, parentExpectations, playerByTeam);
injectExpectationExecutionRepository.saveAll(toUpdate);
}

public List<Channel> channelsForSimulation(@NotBlank final String simulationId) {
return fromIterable(this.channelRepository.findDistinctByArticlesExerciseId(simulationId));
}

public List<Channel> channelsForScenario(@NotBlank final String scenarioId) {
return fromIterable(this.channelRepository.findDistinctByArticlesScenarioId(scenarioId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,14 @@ public String generateUpgradeCommand(
platform, upgradeName, adminToken, installationDir, serviceNameOrPrefix);
}

public List<Endpoint> endpointsForScenario(String scenarioId) {
return this.endpointRepository.findDistinctByInjectsScenarioId(scenarioId);
}

public List<Endpoint> endpointsForSimulation(String simulationId) {
return this.endpointRepository.findDistinctByInjectsExerciseId(simulationId);
}

// -- OPTIONS --
public List<FilterUtilsJpa.Option> getOptionsByNameLinkedToFindings(
String searchText, String sourceId, Pageable pageable) {
Expand Down
14 changes: 14 additions & 0 deletions openbas-front/src/actions/asset_groups/assetgroup-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
14 changes: 14 additions & 0 deletions openbas-front/src/actions/assets/endpoint-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
14 changes: 14 additions & 0 deletions openbas-front/src/actions/channels/channel-action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
18 changes: 18 additions & 0 deletions openbas-front/src/actions/documents/documents-actions.ts
Original file line number Diff line number Diff line change
@@ -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);
};
47 changes: 46 additions & 1 deletion openbas-front/src/admin/components/common/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -16,6 +51,8 @@ export type PermissionsContextType = {

export type ArticleContextType = {
previewArticleUrl: (article: FullArticleStore) => string;
fetchChannels: () => Promise<Channel[]>;
fetchDocuments: () => Promise<Document[]>;
onAddArticle: (data: ArticleCreateInput) => Promise<{ result: string }>;
onUpdateArticle: (article: Article, data: ArticleUpdateInput) => string;
onDeleteArticle: (article: Article) => string;
Expand Down Expand Up @@ -156,6 +193,14 @@ export const PermissionsContext = createContext<PermissionsContextType>({
},
});
export const ArticleContext = createContext<ArticleContextType>({
fetchChannels(): Promise<Channel[]> {
return new Promise<Channel[]>(() => {
});
},
fetchDocuments(): Promise<Document[]> {
return new Promise<Document[]>(() => {
});
},
onAddArticle(_data: ArticleCreateInput): Promise<{ result: string }> {
return Promise.resolve({ result: '' });
},
Expand Down
10 changes: 4 additions & 6 deletions openbas-front/src/admin/components/common/articles/Articles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -49,6 +47,10 @@ const useStyles = makeStyles()(() => ({
interface Props { articles: Article[] }

const Articles: FunctionComponent<Props> = ({ articles }) => {
// Context
const { previewArticleUrl, fetchChannels, fetchDocuments } = useContext(ArticleContext);
const { permissions } = useContext(PermissionsContext);

// Standard hooks
const { classes } = useStyles();
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -90,10 +92,6 @@ const Articles: FunctionComponent<Props> = ({ articles }) => {
filtering.filterAndSort(fullArticles),
);

// Context
const { previewArticleUrl } = useContext(ArticleContext);
const { permissions } = useContext(PermissionsContext);

return (
<div>
<Typography variant="h4" gutterBottom style={{ float: 'left' }}>
Expand Down
Loading