diff --git a/cypress/integration/query_level_monitor_spec.js b/cypress/integration/query_level_monitor_spec.js
index 8995c29d5..0dca7b3a9 100644
--- a/cypress/integration/query_level_monitor_spec.js
+++ b/cypress/integration/query_level_monitor_spec.js
@@ -155,6 +155,78 @@ describe('Query-Level Monitors', () => {
});
});
+ if (Cypress.env('security_enabled')) {
+ describe('can be created with backend roles', () => {
+ before(() => {
+ cy.deleteAllMonitors();
+ });
+
+ it('by extraction query', () => {
+ // mock enable backend roles
+ cy.intercept('GET', '/api/alerting/_settings', {
+ statusCode: 200,
+ body: {
+ ok: true,
+ resp: {
+ persistent: {
+ plugins: {
+ alerting: {
+ filter_by_backend_roles: 'true',
+ },
+ },
+ },
+ transient: {},
+ },
+ },
+ });
+
+ // Confirm we loaded empty monitor list
+ cy.contains('There are no existing monitors');
+
+ // Route us to create monitor page
+ cy.contains('Create monitor').click({ force: true });
+
+ // Select the Query-Level Monitor type
+ cy.get('[data-test-subj="queryLevelMonitorRadioCard"]').click();
+
+ // Select extraction query for method of definition
+ cy.get('[data-test-subj="extractionQueryEditorRadioCard"]').click();
+
+ // Wait for input to load and then type in the monitor name
+ cy.get('input[name="name"]').type(SAMPLE_MONITOR, { force: true });
+
+ // Wait for input to load and then type in the index name
+ cy.get('#index').type('*', { force: true });
+
+ // Wait for input to load and then type in the role
+ cy.get('#roles').click();
+ cy.contains('admin').click({ force: true });
+ cy.get('#roles').blur();
+
+ // Add a trigger
+ cy.contains('Add trigger').click({ force: true });
+
+ // Type in the trigger name
+ cy.get('input[name="triggerDefinitions[0].name"]').type(SAMPLE_TRIGGER, { force: true });
+
+ // Click the create button
+ cy.get('button').contains('Create').click({ force: true });
+
+ // Confirm we can see only one row in the trigger list by checking
element
+ cy.contains('This table contains 1 row');
+
+ // Confirm we can see the new trigger
+ cy.contains(SAMPLE_TRIGGER);
+
+ // Go back to the Monitors list
+ cy.get('a').contains('Monitors').click({ force: true });
+
+ // Confirm we can see the created monitor in the list
+ cy.contains(SAMPLE_MONITOR);
+ });
+ });
+ }
+
describe('can be updated', () => {
beforeEach(() => {
cy.deleteAllMonitors();
diff --git a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
index 3c2c05e33..e6d2ef62b 100644
--- a/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
+++ b/public/components/FeatureAnywhereContextMenu/AddAlertingMonitor/__snapshots__/AddAlertingMonitor.test.js.snap
@@ -56,6 +56,7 @@ exports[`AddAlertingMonitor renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
diff --git a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap
index 18c958869..3eded366f 100644
--- a/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap
+++ b/public/pages/CreateMonitor/containers/AnomalyDetectors/__tests__/__snapshots__/AnomalyDetector.test.js.snap
@@ -52,6 +52,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
@@ -126,6 +127,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
@@ -261,6 +263,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
@@ -354,6 +357,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
@@ -495,6 +499,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
@@ -588,6 +593,7 @@ exports[`AnomalyDetectors renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js
index 353255ada..915f8894a 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/CreateMonitor.js
@@ -28,6 +28,8 @@ import {
getPerformanceModal,
RECOMMENDED_DURATION,
} from '../../components/QueryPerformance/QueryPerformance';
+import MonitorSecurity from '../MonitorSecurity';
+import { FILTER_BY_BACKEND_ROLES_SETTING_PATH } from './utils/constants';
export default class CreateMonitor extends Component {
static defaultProps = {
@@ -57,16 +59,47 @@ export default class CreateMonitor extends Component {
triggerToEdit,
createModalOpen: false,
formikBag: undefined,
+ filterByBackendRolesEnabled: false,
};
this.onCancel = this.onCancel.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.evaluateSubmission = this.evaluateSubmission.bind(this);
+ this.getSettings = this.getSettings.bind(this);
+ }
+
+ async getSettings() {
+ try {
+ const { httpClient } = this.props;
+ const response = await httpClient.get('../api/alerting/_settings');
+ if (response.ok) {
+ const { defaults, transient, persistent } = response.resp;
+ let filterByBackendRolesEnabled = _.get(
+ // If present, take the 'transient' setting.
+ transient,
+ FILTER_BY_BACKEND_ROLES_SETTING_PATH,
+ // Else take the 'persistent' setting.
+ _.get(
+ persistent,
+ FILTER_BY_BACKEND_ROLES_SETTING_PATH,
+ // Else take the 'default' setting.
+ _.get(defaults, FILTER_BY_BACKEND_ROLES_SETTING_PATH, false)
+ )
+ );
+ // Boolean settings are returned as strings (e.g., `"true"`, and `"false"`). Constructing boolean value from the string.
+ if (typeof filterByBackendRolesEnabled === 'string')
+ filterByBackendRolesEnabled = JSON.parse(filterByBackendRolesEnabled);
+ this.setState({ filterByBackendRolesEnabled: filterByBackendRolesEnabled });
+ }
+ } catch (e) {
+ console.log('Error while retrieving settings', e);
+ }
}
componentDidMount() {
const { httpClient } = this.props;
+ this.getSettings();
const updatePlugins = async () => {
const newPlugins = await getPlugins(httpClient);
this.setState({ plugins: newPlugins });
@@ -162,7 +195,7 @@ export default class CreateMonitor extends Component {
isDarkMode,
notificationService,
} = this.props;
- const { createModalOpen, initialValues, plugins } = this.state;
+ const { createModalOpen, initialValues, plugins, filterByBackendRolesEnabled } = this.state;
return (
@@ -207,6 +240,13 @@ export default class CreateMonitor extends Component {
>
) : null}
+ {isComposite && filterByBackendRolesEnabled ? (
+ <>
+
+
+ >
+ ) : null}
+
{values.searchType !== SEARCH_TYPE.AD &&
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
index 598937daf..1917b30ba 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/__snapshots__/CreateMonitor.test.js.snap
@@ -60,6 +60,7 @@ exports[`CreateMonitor renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
index 1456896ef..a02867010 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/__snapshots__/formikToMonitor.test.js.snap
@@ -209,6 +209,90 @@ Object {
}
`;
+exports[`formikToMonitor can build monitor with roles 1`] = `
+Object {
+ "enabled": false,
+ "inputs": Array [
+ Object {
+ "search": Object {
+ "indices": Array [
+ "index1",
+ "index2",
+ ],
+ "query": Object {
+ "aggregations": Object {},
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "range": Object {
+ "": Object {
+ "format": "epoch_millis",
+ "gte": "{{period_end}}||-1h",
+ "lte": "{{period_end}}",
+ },
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ },
+ },
+ ],
+ "monitor_type": "query_level_monitor",
+ "name": "random_name",
+ "rbac_roles": Array [
+ "test_bk_role_1",
+ "test_bk_role_2",
+ ],
+ "schedule": Object {
+ "period": Object {
+ "interval": 1,
+ "unit": "MINUTES",
+ },
+ },
+ "triggers": Array [],
+ "type": "monitor",
+ "ui_metadata": Object {
+ "monitor_type": "query_level_monitor",
+ "schedule": Object {
+ "cronExpression": "0 */1 * * *",
+ "daily": 0,
+ "frequency": "interval",
+ "monthly": Object {
+ "day": 1,
+ "type": "day",
+ },
+ "period": Object {
+ "interval": 1,
+ "unit": "MINUTES",
+ },
+ "timezone": "America/Los_Angeles",
+ "weekly": Object {
+ "fri": false,
+ "mon": false,
+ "sat": false,
+ "sun": false,
+ "thur": false,
+ "tue": false,
+ "wed": false,
+ },
+ },
+ "search": Object {
+ "aggregations": Array [],
+ "bucketUnitOfTime": "h",
+ "bucketValue": 1,
+ "filters": Array [],
+ "groupBy": Array [],
+ "searchType": "graph",
+ "timeField": "",
+ },
+ },
+}
+`;
+
exports[`formikToQuery can build extraction query 1`] = `
Object {
"query": Object {
@@ -240,6 +324,13 @@ Object {
}
`;
+exports[`formikToRoles can build roles 1`] = `
+Array [
+ "test_bk_role_1",
+ "test_bk_role_2",
+]
+`;
+
exports[`formikToUiGraphQuery can build ui graph query 1`] = `
Object {
"aggregations": Object {
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
index 39c2de490..40dd4efb4 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/constants.js
@@ -78,6 +78,7 @@ export const FORMIK_INITIAL_VALUES = {
associatedMonitorsList: [],
associatedMonitorsEditor: '',
preventVisualEditor: false,
+ roles: [],
};
export const FORMIK_INITIAL_AGG_VALUES = {
@@ -112,6 +113,8 @@ export const DEFAULT_DOCUMENT_LEVEL_QUERY = JSON.stringify(
export const DEFAULT_COMPOSITE_AGG_SIZE = 50;
+export const FILTER_BY_BACKEND_ROLES_SETTING_PATH = 'plugins.alerting.filter_by_backend_roles';
+
export const METRIC_TOOLTIP_TEXT = 'Extracted statistics such as simple calculations of data.';
export const TIME_RANGE_TOOLTIP_TEXT = 'The time frame of data the plugin should monitor.';
export const FILTERS_TOOLTIP_TEXT =
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
index 4619c08b2..87c47a5fa 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.js
@@ -63,6 +63,7 @@ export function formikToMonitor(values) {
monitor_type: values.monitor_type,
...monitorUiMetadata(),
},
+ ...(formikToRoles(values).length && { rbac_roles: formikToRoles(values) }),
};
}
@@ -79,6 +80,7 @@ export function formikToMonitor(values) {
monitor_type: values.monitor_type,
...monitorUiMetadata(),
},
+ ...(formikToRoles(values).length && { rbac_roles: formikToRoles(values) }),
};
}
@@ -213,6 +215,10 @@ export function formikToIndices(values) {
return values.index.map(({ label, value }) => (hasRemoteClusters ? value : label));
}
+export function formikToRoles(values) {
+ return values.roles.map(({ label }) => label);
+}
+
export function formikToQuery(values) {
const isGraph = values.searchType === SEARCH_TYPE.GRAPH;
return isGraph ? formikToGraphQuery(values) : formikToExtractionQuery(values);
diff --git a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js
index 6c40965ef..72dcfc27f 100644
--- a/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js
+++ b/public/pages/CreateMonitor/containers/CreateMonitor/utils/formikToMonitor.test.js
@@ -20,6 +20,7 @@ import {
formikToAd,
formikToInputs,
formikToClusterMetricsInput,
+ formikToRoles,
} from './formikToMonitor';
import { FORMIK_INITIAL_VALUES } from './constants';
@@ -32,15 +33,23 @@ jest.mock('moment-timezone', () => {
});
describe('formikToMonitor', () => {
- const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES);
- formikValues.name = 'random_name';
- formikValues.disabled = true;
- formikValues.index = [{ label: 'index1' }, { label: 'index2' }];
- formikValues.fieldName = [{ label: 'bytes' }];
- formikValues.timezone = [{ label: 'America/Los_Angeles' }];
+ let formikValues;
+ beforeEach(() => {
+ formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES);
+ formikValues.name = 'random_name';
+ formikValues.disabled = true;
+ formikValues.index = [{ label: 'index1' }, { label: 'index2' }];
+ formikValues.fieldName = [{ label: 'bytes' }];
+ formikValues.timezone = [{ label: 'America/Los_Angeles' }];
+ formikValues.roles = [];
+ });
test('can build monitor', () => {
expect(formikToMonitor(formikValues)).toMatchSnapshot();
});
+ test('can build monitor with roles', () => {
+ formikValues.roles = [{ label: 'test_bk_role_1' }, { label: 'test_bk_role_2' }];
+ expect(formikToMonitor(formikValues)).toMatchSnapshot();
+ });
});
describe('formikToInputs', () => {
@@ -108,6 +117,14 @@ describe('formikToIndices', () => {
});
});
+describe('formikToRoles', () => {
+ test('can build roles', () => {
+ const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES);
+ formikValues.roles = [{ label: 'test_bk_role_1' }, { label: 'test_bk_role_2' }];
+ expect(formikToRoles(formikValues)).toMatchSnapshot();
+ });
+});
+
describe('formikToQuery', () => {
const formikValues = _.cloneDeep(FORMIK_INITIAL_VALUES);
diff --git a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
index 6a2af98d4..b6b4b1f0b 100644
--- a/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
+++ b/public/pages/CreateMonitor/containers/DefineMonitor/DefineMonitor.js
@@ -27,13 +27,17 @@ import { buildRequest } from './utils/searchRequests';
import { SEARCH_TYPE, OS_AD_PLUGIN, MONITOR_TYPE } from '../../../../utils/constants';
import { backendErrorNotification } from '../../../../utils/helpers';
import DataSource from '../DataSource';
+import MonitorSecurity from '../MonitorSecurity';
import {
buildClusterMetricsRequest,
getApiType,
getApiTypesRequiringPathParams,
} from '../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorHelpers';
import ClusterMetricsMonitor from '../../components/ClusterMetricsMonitor';
-import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants';
+import {
+ FORMIK_INITIAL_VALUES,
+ FILTER_BY_BACKEND_ROLES_SETTING_PATH,
+} from '../CreateMonitor/utils/constants';
import { API_TYPES } from '../../components/ClusterMetricsMonitor/utils/clusterMetricsMonitorConstants';
import ConfigureDocumentLevelQueries from '../../components/DocumentLevelMonitorQueries/ConfigureDocumentLevelQueries';
import FindingsDashboard from '../../../Dashboard/containers/FindingsDashboard';
@@ -78,6 +82,7 @@ class DefineMonitor extends Component {
PanelComponent: props.flyoutMode ? ({ children }) => <>{children}> : ContentPanel,
remoteMonitoringEnabled: false,
canCallGetRemoteIndexes: false,
+ filterByBackendRolesEnabled: false,
};
this.renderGraph = this.renderGraph.bind(this);
@@ -208,6 +213,24 @@ class DefineMonitor extends Component {
if (typeof remoteMonitoringEnabled === 'string') {
remoteMonitoringEnabled = JSON.parse(remoteMonitoringEnabled);
}
+ this.setState({ remoteMonitoringEnabled: remoteMonitoringEnabled });
+
+ let filterByBackendRolesEnabled = _.get(
+ // If present, take the 'transient' setting.
+ transient,
+ FILTER_BY_BACKEND_ROLES_SETTING_PATH,
+ // Else take the 'persistent' setting.
+ _.get(
+ persistent,
+ FILTER_BY_BACKEND_ROLES_SETTING_PATH,
+ // Else take the 'default' setting.
+ _.get(defaults, FILTER_BY_BACKEND_ROLES_SETTING_PATH, false)
+ )
+ );
+ // Boolean settings are returned as strings (e.g., `"true"`, and `"false"`). Constructing boolean value from the string.
+ if (typeof filterByBackendRolesEnabled === 'string')
+ filterByBackendRolesEnabled = JSON.parse(filterByBackendRolesEnabled);
+ this.setState({ filterByBackendRolesEnabled: filterByBackendRolesEnabled });
}
} catch (e) {
console.log('Error while retrieving settings:', e);
@@ -679,7 +702,7 @@ class DefineMonitor extends Component {
isDarkMode,
flyoutMode,
} = this.props;
- const { dataTypes, PanelComponent, canCallGetRemoteIndexes, remoteMonitoringEnabled } =
+ const { dataTypes, PanelComponent, canCallGetRemoteIndexes, remoteMonitoringEnabled, filterByBackendRolesEnabled } =
this.state;
const monitorContent = this.getMonitorContent();
const { searchType } = this.props.values;
@@ -708,6 +731,12 @@ class DefineMonitor extends Component {
)}
+ {filterByBackendRolesEnabled ? (
+
+
+
+
+ ) : null}
({
+ label: role,
+ role,
+ }));
+ return _.sortBy(roles, 'label');
+ }
+ return [];
+ } catch (err) {
+ console.error(err);
+ return [];
+ }
+ }
+
+ async onFetch(query) {
+ this.setState({ isLoading: true });
+ const roles = await this.handleQueryRoles(query);
+ createReasonableWait(() => {
+ // If the search changed, discard this state
+ if (query !== this.lastQuery) {
+ return;
+ }
+ this.setState({ roles, isLoading: false });
+ });
+ }
+
+ render() {
+ const { roles, isLoading } = this.state;
+
+ const visibleOptions = [
+ {
+ label: 'Backend roles',
+ options: roles,
+ },
+ ];
+
+ return (
+
+
+ Backend roles
+ - optional
+
+
+ Specify role-based access control (RBAC) backend roles.{' '}
+
+ Learn more
+
+
+
+ ),
+ style: { paddingLeft: '10px' },
+ }}
+ inputProps={{
+ placeholder: 'Select backend roles',
+ async: true,
+ isLoading,
+ options: visibleOptions,
+ onBlur: (e, field, form) => {
+ form.setFieldTouched('roles', true);
+ },
+ onChange: (options, field, form) => {
+ form.setFieldValue('roles', options);
+ },
+ onSearchChange: this.onSearchChange,
+ isClearable: true,
+ singleSelection: false,
+ 'data-test-subj': 'rolesComboBox',
+ }}
+ />
+ );
+ }
+}
+
+MonitorRoles.propTypes = propTypes;
+
+export default MonitorRoles;
diff --git a/public/pages/CreateMonitor/containers/MonitorRoles/MonitorRoles.test.js b/public/pages/CreateMonitor/containers/MonitorRoles/MonitorRoles.test.js
new file mode 100644
index 000000000..bca3d1197
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorRoles/MonitorRoles.test.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Formik } from 'formik';
+import { mount } from 'enzyme';
+
+import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants';
+import MonitorRoles from './MonitorRoles';
+import * as helpers from '../MonitorIndex//utils/helpers';
+import { httpClientMock } from '../../../../../test/mocks';
+
+helpers.createReasonableWait = jest.fn((cb) => cb());
+httpClientMock.post.mockResolvedValue({ ok: true, resp: [] });
+
+// Enzyme's change event is synchronous and Formik's handlers are asynchronous
+// https://github.com/formium/formik/issues/937, https://www.benmvp.com/blog/asynchronous-testing-with-enzyme-react-jest/
+const runAllPromises = () => new Promise(setImmediate);
+
+function getMountWrapper(customProps = {}) {
+ return mount(
+
+ {() => }
+
+ );
+}
+
+describe('MonitorRoles', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ test('renders', () => {
+ const wrapper = getMountWrapper();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('calls onSearchChange when changing input value', () => {
+ const onSearchChange = jest.spyOn(MonitorRoles.prototype, 'onSearchChange');
+ const wrapper = getMountWrapper();
+ wrapper
+ .find('[data-test-subj="comboBoxSearchInput"]')
+ .hostNodes()
+ .simulate('change', { target: { value: 'random-role' } });
+
+ expect(onSearchChange).toHaveBeenCalled();
+ expect(onSearchChange).toHaveBeenCalledWith('random-role', false);
+ });
+
+ test('searches space normalizes value', () => {
+ const wrapper = getMountWrapper();
+
+ wrapper
+ .find('[data-test-subj="comboBoxSearchInput"]')
+ .hostNodes()
+ .simulate('change', { target: { value: ' ' } })
+ .simulate('keyDown', { key: 'Enter' });
+
+ expect(wrapper.find('.euiComboBoxPill')).toHaveLength(0);
+ });
+
+ test('returns empty array for data.ok = false', async () => {
+ httpClientMock.post.mockResolvedValue({ ok: false });
+ const wrapper = getMountWrapper();
+
+ expect(await wrapper.find(MonitorRoles).instance().handleQueryRoles('random')).toEqual([]);
+ expect(await wrapper.find(MonitorRoles).instance().handleQueryRoles('random')).toEqual([]);
+ });
+
+ test('returns roles', async () => {
+ httpClientMock.post.mockResolvedValue({
+ ok: true,
+ resp: ['logstash'],
+ });
+ const wrapper = getMountWrapper();
+
+ expect(await wrapper.find(MonitorRoles).instance().handleQueryRoles('l')).toEqual([
+ { label: 'logstash', role: 'logstash' },
+ ]);
+ });
+
+ test('sets option when calling onCreateOption', async () => {
+ httpClientMock.post.mockResolvedValue({
+ ok: true,
+ resp: ['logstash'],
+ });
+ const wrapper = getMountWrapper();
+
+ wrapper
+ .find('[data-test-subj="comboBoxSearchInput"]')
+ .hostNodes()
+ .simulate('change', { target: { value: 'logstash' } });
+
+ await runAllPromises();
+
+ wrapper
+ .find('[data-test-subj="comboBoxInput"]')
+ .hostNodes()
+ .simulate('keyDown', { key: 'ArrowDown' })
+ .simulate('keyDown', { key: 'Enter' });
+
+ // Validate the specific role is in the input field
+ expect(wrapper.find('[data-test-subj="comboBoxInput"]').text()).toEqual('logstashEuiIconMock');
+ });
+});
diff --git a/public/pages/CreateMonitor/containers/MonitorRoles/__snapshots__/MonitorRoles.test.js.snap b/public/pages/CreateMonitor/containers/MonitorRoles/__snapshots__/MonitorRoles.test.js.snap
new file mode 100644
index 000000000..f59d30f7e
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorRoles/__snapshots__/MonitorRoles.test.js.snap
@@ -0,0 +1,964 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MonitorRoles renders 1`] = `
+
+
+
+
+
+ Backend roles
+
+
+ - optional
+
+
+
+ Specify role-based access control (RBAC) backend roles.
+
+
+ Learn more
+
+
+ ,
+ "style": Object {
+ "paddingLeft": "10px",
+ },
+ }
+ }
+ >
+
+
+
+
+
+ Backend roles
+
+
+ - optional
+
+
+
+ Specify role-based access control (RBAC) backend roles.
+
+
+ Learn more
+
+
+ ,
+ "style": Object {
+ "paddingLeft": "10px",
+ },
+ }
+ }
+ >
+
+
+
+ Backend roles
+
+
+ - optional
+
+
+
+ Specify role-based access control (RBAC) backend roles.
+
+
+ Learn more
+
+
+
+ }
+ labelType="label"
+ style={
+ Object {
+ "paddingLeft": "10px",
+ }
+ }
+ >
+
+
+
+
+
+
+
+
+`;
diff --git a/public/pages/CreateMonitor/containers/MonitorRoles/index.js b/public/pages/CreateMonitor/containers/MonitorRoles/index.js
new file mode 100644
index 000000000..ebc73abc5
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorRoles/index.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import MonitorRoles from './MonitorRoles';
+
+export default MonitorRoles;
diff --git a/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.js b/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.js
new file mode 100644
index 000000000..97bb0c34e
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { EuiSpacer } from '@elastic/eui';
+import MonitorRoles from '../MonitorRoles';
+import ContentPanel from '../../../../components/ContentPanel';
+
+const propTypes = {
+ httpClient: PropTypes.object.isRequired,
+ isMinimal: PropTypes.bool,
+};
+const defaultProps = {
+ isMinimal: false,
+};
+class MonitorSecurity extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ response: null,
+ };
+ }
+
+ render() {
+ const { isMinimal } = this.props;
+ const monitorRoleDisplay = (
+ <>
+
+ >
+ );
+
+ if (isMinimal) {
+ return { monitorRoleDisplay };
+ }
+ return (
+
+ {monitorRoleDisplay}
+
+ );
+ }
+}
+
+MonitorSecurity.propTypes = propTypes;
+MonitorSecurity.defaultProps = defaultProps;
+
+export default MonitorSecurity;
diff --git a/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.test.js b/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.test.js
new file mode 100644
index 000000000..acdb40782
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorSecurity/MonitorSecurity.test.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { httpClientMock } from '../../../../../test/mocks';
+import MonitorSecurity from './MonitorSecurity';
+import { FORMIK_INITIAL_VALUES } from '../CreateMonitor/utils/constants';
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe('MonitorSecurity', () => {
+ test('renders', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/public/pages/CreateMonitor/containers/MonitorSecurity/__snapshots__/MonitorSecurity.test.js.snap b/public/pages/CreateMonitor/containers/MonitorSecurity/__snapshots__/MonitorSecurity.test.js.snap
new file mode 100644
index 000000000..0ef7200d0
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorSecurity/__snapshots__/MonitorSecurity.test.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MonitorSecurity renders 1`] = `
+
+
+
+`;
diff --git a/public/pages/CreateMonitor/containers/MonitorSecurity/index.js b/public/pages/CreateMonitor/containers/MonitorSecurity/index.js
new file mode 100644
index 000000000..0527b2acd
--- /dev/null
+++ b/public/pages/CreateMonitor/containers/MonitorSecurity/index.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import MonitorSecurity from './MonitorSecurity';
+
+export default MonitorSecurity;
diff --git a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
index 040b38d27..a61803cb7 100644
--- a/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
+++ b/public/pages/Dashboard/components/AcknowledgeAlertsModal/__snapshots__/AcknowledgeAlertsModal.test.js.snap
@@ -81,6 +81,7 @@ exports[`AcknowledgeAlertsModal renders 1`] = `
\\"match_all\\": {}
}
}",
+ "roles": Array [],
"searchType": "graph",
"timeField": "",
"timezone": Array [],
diff --git a/server/routes/opensearch.js b/server/routes/opensearch.js
index 28ade243b..91d0b6a92 100644
--- a/server/routes/opensearch.js
+++ b/server/routes/opensearch.js
@@ -77,4 +77,16 @@ export default function (services, router) {
},
opensearchService.getClusterHealth
);
+
+ router.post(
+ {
+ path: '/api/alerting/_roles',
+ validate: {
+ body: schema.object({
+ role: schema.string(),
+ }),
+ },
+ },
+ opensearchService.getRoles
+ );
}
diff --git a/server/services/OpensearchService.js b/server/services/OpensearchService.js
index feafba9cf..9ae7b18c6 100644
--- a/server/services/OpensearchService.js
+++ b/server/services/OpensearchService.js
@@ -187,4 +187,54 @@ export default class OpensearchService {
});
}
};
+
+ getRoles = async (context, req, res) => {
+ try {
+ const { role } = req.body;
+ const { callAsCurrentUser } = this.esDriver.asScoped(req);
+
+ // Try to get all backend roles in the system if the user has sufficient permission
+ try {
+ const roles_results = await callAsCurrentUser('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/rolesmapping',
+ });
+ let all_backend_roles = new Set();
+
+ for (let key in roles_results) {
+ roles_results[key].backend_roles.forEach((item) => all_backend_roles.add(item));
+ }
+
+ return res.ok({
+ body: {
+ ok: true,
+ resp: Array.from(all_backend_roles).filter((item) => !role || item.includes(role)),
+ },
+ });
+ } catch (err) {
+ console.log('Alerting - OpensearchService - getRoles: get rolemappings:', err.message);
+ }
+
+ // Get users roles
+ const account_results = await callAsCurrentUser('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/account',
+ });
+
+ return res.ok({
+ body: {
+ ok: true,
+ resp: account_results.backend_roles.filter((item) => !role || item.includes(role)),
+ },
+ });
+ } catch (err) {
+ console.error('Alerting - OpensearchService - getRoles:', err);
+ return res.ok({
+ body: {
+ ok: false,
+ resp: err.message,
+ },
+ });
+ }
+ };
}
diff --git a/server/services/OpensearchService.mock.js b/server/services/OpensearchService.mock.js
new file mode 100644
index 000000000..ff300dfec
--- /dev/null
+++ b/server/services/OpensearchService.mock.js
@@ -0,0 +1,8 @@
+export const esDriverMock = jest.fn();
+esDriverMock.call = jest.fn();
+esDriverMock.asScoped = jest.fn().mockReturnValue({
+ callAsCurrentUser: esDriverMock.call,
+});
+
+export const responseMock = jest.fn();
+responseMock.ok = jest.fn().mockImplementation((obj) => obj);
diff --git a/server/services/OpensearchService.test.js b/server/services/OpensearchService.test.js
new file mode 100644
index 000000000..893510c48
--- /dev/null
+++ b/server/services/OpensearchService.test.js
@@ -0,0 +1,359 @@
+import { httpClientMock } from '../../test/mocks';
+import OpensearchService from './OpensearchService';
+
+import { esDriverMock, responseMock } from './OpensearchService.mock';
+
+describe('getIndices', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return all local indices', async () => {
+ const os = new OpensearchService(esDriverMock);
+ const os_resp = [
+ {
+ health: 'green',
+ index: 'test-index-a',
+ status: 'open',
+ },
+ {
+ health: 'yellow',
+ index: 'test-index-b',
+ status: 'open',
+ },
+ {
+ health: 'yellow',
+ index: 'test-index-c',
+ status: 'open',
+ },
+ ];
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ index: '*',
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: os_resp,
+ },
+ };
+ expect(await os.getIndices(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('cat.indices', {
+ format: 'json',
+ h: 'health,index,status',
+ index: '*',
+ });
+ });
+
+ it('should return matching local indices', async () => {
+ const os = new OpensearchService(esDriverMock);
+ const os_resp = [
+ {
+ health: 'green',
+ index: 'test-index-a',
+ status: 'open',
+ },
+ {
+ health: 'yellow',
+ index: 'test-index-b',
+ status: 'open',
+ },
+ ];
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ index: 'test*',
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: os_resp,
+ },
+ };
+ expect(await os.getIndices(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('cat.indices', {
+ format: 'json',
+ h: 'health,index,status',
+ index: 'test*',
+ });
+ });
+});
+
+describe('getAliases', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return all local aliases', async () => {
+ const os = new OpensearchService(esDriverMock);
+ const os_resp = [
+ {
+ alias: 'test_index',
+ index: 'test_index_2',
+ },
+ {
+ alias: 'test_index',
+ index: 'test_index_1',
+ },
+ ];
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ alias: '*',
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: os_resp,
+ },
+ };
+ expect(await os.getAliases(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('cat.aliases', {
+ alias: '*',
+ format: 'json',
+ h: 'alias,index',
+ });
+ });
+
+ it('should return matching local aliases', async () => {
+ const os = new OpensearchService(esDriverMock);
+ const os_resp = [
+ {
+ alias: 'test_index',
+ index: 'test_index_2',
+ },
+ {
+ alias: 'test_index',
+ index: 'test_index_1',
+ },
+ ];
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ alias: 'test*',
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: os_resp,
+ },
+ };
+ expect(await os.getAliases(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('cat.aliases', {
+ alias: 'test*',
+ format: 'json',
+ h: 'alias,index',
+ });
+ });
+});
+
+describe('getMappings', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return local mappings', async () => {
+ const os = new OpensearchService(esDriverMock);
+ const os_resp = {
+ test_index_2: {
+ mappings: { properties: { fieldA: { type: 'text' }, fieldB: { type: 'date' } } },
+ },
+ test_index_1: {
+ mappings: { properties: { fieldA: { type: 'text' }, fieldB: { type: 'date' } } },
+ },
+ };
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ index: ['test*'],
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: os_resp,
+ },
+ };
+ expect(await os.getMappings(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('indices.getMapping', {
+ index: ['test*'],
+ });
+ });
+});
+
+describe('getRoles', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const os_resp = {
+ manage_snapshots: {
+ hosts: [],
+ users: [],
+ reserved: false,
+ hidden: false,
+ backend_roles: ['snapshotrestore'],
+ and_backend_roles: [],
+ },
+ logstash: {
+ hosts: [],
+ users: [],
+ reserved: false,
+ hidden: false,
+ backend_roles: ['logstash'],
+ and_backend_roles: [],
+ },
+ own_index: {
+ hosts: [],
+ users: ['*'],
+ reserved: false,
+ hidden: false,
+ backend_roles: [],
+ and_backend_roles: [],
+ description: 'Allow full access to an index named like the username',
+ },
+ alerting_full_access: {
+ hosts: [],
+ users: [],
+ reserved: false,
+ hidden: false,
+ backend_roles: ['test_bk_role_1', 'test_bk_role_2'],
+ and_backend_roles: [],
+ },
+ all_access: {
+ hosts: [],
+ users: [],
+ reserved: false,
+ hidden: false,
+ backend_roles: ['admin'],
+ and_backend_roles: [],
+ description: 'Maps admin to all_access',
+ },
+ readall: {
+ hosts: [],
+ users: [],
+ reserved: false,
+ hidden: false,
+ backend_roles: ['readall', 'test_bk_role_2'],
+ and_backend_roles: [],
+ },
+ };
+
+ const user_role_os_resp = {
+ user_name: 'test_user_1',
+ is_reserved: false,
+ is_hidden: false,
+ is_internal_user: true,
+ user_requested_tenant: null,
+ backend_roles: ['test_bk_role_2', 'test_bk_role_1'],
+ custom_attribute_names: [],
+ tenants: { test_user_1: true },
+ roles: ['manage_snapshots', 'own_index', 'alerting_full_access', 'readall'],
+ };
+
+ it('should return all cluster roles', async () => {
+ const os = new OpensearchService(esDriverMock);
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ role: [],
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: [
+ 'snapshotrestore',
+ 'logstash',
+ 'test_bk_role_1',
+ 'test_bk_role_2',
+ 'admin',
+ 'readall',
+ ],
+ },
+ };
+ expect(await os.getRoles(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/rolesmapping',
+ });
+ });
+
+ it('should return specific cluster role', async () => {
+ const os = new OpensearchService(esDriverMock);
+ esDriverMock.call.mockReturnValue(os_resp);
+ const requestMock = {
+ body: {
+ role: ['test_bk_role_1'],
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: ['test_bk_role_1'],
+ },
+ };
+ expect(await os.getRoles(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/rolesmapping',
+ });
+ });
+
+ it('should return all user roles', async () => {
+ const os = new OpensearchService(esDriverMock);
+ esDriverMock.call.mockRejectedValueOnce(new Error('Authorization Exception'));
+ esDriverMock.call.mockReturnValueOnce(user_role_os_resp);
+ const requestMock = {
+ body: {
+ role: [],
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: ['test_bk_role_2', 'test_bk_role_1'],
+ },
+ };
+ expect(await os.getRoles(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/rolesmapping',
+ });
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/account',
+ });
+ });
+
+ it('should return specific user role', async () => {
+ const os = new OpensearchService(esDriverMock);
+ esDriverMock.call.mockRejectedValueOnce(new Error('Authorization Exception'));
+ esDriverMock.call.mockReturnValueOnce(user_role_os_resp);
+ const requestMock = {
+ body: {
+ role: ['test_bk_role_1'],
+ },
+ };
+ const resp = {
+ body: {
+ ok: true,
+ resp: ['test_bk_role_1'],
+ },
+ };
+ expect(await os.getRoles(undefined, requestMock, responseMock)).toEqual(resp);
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/rolesmapping',
+ });
+ expect(esDriverMock.call).toHaveBeenCalledWith('transport.request', {
+ method: 'GET',
+ path: '_plugins/_security/api/account',
+ });
+ });
+});