diff --git a/CHANGELOG.md b/CHANGELOG.md index d32cab6e8a..d7cd7c5836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added propagation of updates from the table to dashboard visualizations in Endpoints summary [#6460](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6460) - Handle index pattern selector on new discover [#6499](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6499) - Added macOS log collector tab [#6545](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6545) +- Add ability to disable the edition of configuration through API endpoints and UI [#6557](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6557) - Added journald log collector tab [#6572](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6572) ### Changed diff --git a/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx b/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx index 785851b767..0d781b62e7 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx @@ -17,6 +17,7 @@ import '../group-input/group-input.scss'; import { WzRequest } from '../../../../../react-services'; import { ErrorHandler } from '../../../../../react-services/error-management/error-handler/error-handler'; import { WzButtonPermissions } from '../../../../common/permissions/button'; +import { useAppConfig } from '../../../../common/hooks'; interface ServerAddressInputProps { formField: EnhancedFieldConfiguration; @@ -50,6 +51,7 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { const [defaultServerAddress, setDefaultServerAddress] = useState( formField?.initialValue ? formField?.initialValue : '', ); + const appConfig = useAppConfig(); const handleToggleRememberAddress = async event => { setRememberServerAddress(event.target.checked); @@ -146,18 +148,20 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { /> - - - handleToggleRememberAddress(e)} - /> - - + {appConfig?.data?.['configuration.ui_api_editable'] && ( + + + handleToggleRememberAddress(e)} + /> + + + )} ); }; diff --git a/plugins/main/public/components/settings/about/index.tsx b/plugins/main/public/components/settings/about/index.tsx index c8e7c8bb05..a7a71f150e 100644 --- a/plugins/main/public/components/settings/about/index.tsx +++ b/plugins/main/public/components/settings/about/index.tsx @@ -2,24 +2,24 @@ import React from 'react'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { SettingsAboutAppInfo } from './appInfo'; import { SettingsAboutGeneralInfo } from './generalInfo'; +import { PLUGIN_APP_NAME } from '../../../../common/constants'; interface SettingsAboutProps { appInfo?: { 'app-version': string; revision: string; }; - pluginAppName: string; } export const SettingsAbout = (props: SettingsAboutProps) => { - const { appInfo, pluginAppName } = props; + const { appInfo } = props; return ( - + ); diff --git a/plugins/main/public/components/settings/api/api-table.js b/plugins/main/public/components/settings/api/api-table.js index 86e14b00b1..3b18de2d26 100644 --- a/plugins/main/public/components/settings/api/api-table.js +++ b/plugins/main/public/components/settings/api/api-table.js @@ -229,7 +229,7 @@ export const ApiTable = compose( ? error : (error || {}).message || ((error || {}).data || {}).message || - 'Wazuh is not reachable'; + 'API is not reachable'; const status = code === 3099 ? 'down' : 'unknown'; APIconnection.status = { status, downReason }; if (APIconnection.id === this.state.selectedAPIConnection) { @@ -296,7 +296,7 @@ export const ApiTable = compose( ? error : (error || {}).message || ((error || {}).data || {}).message || - 'Wazuh is not reachable'; + 'API is not reachable'; const status = code === 3099 ? 'down' : 'unknown'; entries[idx].status = { status, downReason }; throw error; diff --git a/plugins/main/public/components/settings/configuration/configuration.tsx b/plugins/main/public/components/settings/configuration/configuration.tsx index 60b72ff6fd..90d13ab3da 100644 --- a/plugins/main/public/components/settings/configuration/configuration.tsx +++ b/plugins/main/public/components/settings/configuration/configuration.tsx @@ -341,7 +341,7 @@ const WzConfigurationSettingsProvider = props => { error: { error: error, message: error.message || error, - title: `Error saving the configuration: ${error.message || error}`, + title: 'Error saving the configuration', }, }; diff --git a/plugins/main/public/components/settings/settings.tsx b/plugins/main/public/components/settings/settings.tsx index c6121ccb13..dc9ec142d4 100644 --- a/plugins/main/public/components/settings/settings.tsx +++ b/plugins/main/public/components/settings/settings.tsx @@ -10,20 +10,15 @@ * Find more information about this on the LICENSE file. */ import React from 'react'; -import { EuiProgress } from '@elastic/eui'; -import { Tabs } from '../common/tabs/tabs'; -import { TabNames } from '../../utils/tab-names'; -import { pluginPlatform } from '../../../package.json'; +import { EuiProgress, EuiTabs, EuiTab } from '@elastic/eui'; import { AppState } from '../../react-services/app-state'; -import { WazuhConfig } from '../../react-services/wazuh-config'; import { GenericRequest } from '../../react-services/generic-request'; import { WzMisc } from '../../factories/misc'; import { ApiCheck } from '../../react-services/wz-api-check'; import { SavedObject } from '../../react-services/saved-objects'; import { ErrorHandler } from '../../react-services/error-handler'; -import store from '../../redux/store'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; -import { UI_LOGGER_LEVELS, PLUGIN_APP_NAME } from '../../../common/constants'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; import { getAssetURL } from '../../utils/assets'; @@ -38,349 +33,342 @@ import { serverApis, appSettings, } from '../../utils/applications'; +import { compose } from 'redux'; +import { withReduxProvider } from '../common/hocs'; +import { connect } from 'react-redux'; -export class Settings extends React.Component { - state: { - tab: string; - load: boolean; - loadingLogs: boolean; - settingsTabsProps?; - currentApiEntryIndex; - indexPatterns; - apiEntries; - }; - pluginAppName: string; - pluginPlatformVersion: string | boolean; - genericReq; - wzMisc; - wazuhConfig; - tabNames; - tabsConfiguration; - apiIsDown; - messageError; - messageErrorUpdate; - googleGroupsSVG; - currentDefault; - appInfo; - urlTabRegex; - - constructor(props) { - super(props); - - this.pluginPlatformVersion = (pluginPlatform || {}).version || false; - this.pluginAppName = PLUGIN_APP_NAME; - - this.genericReq = GenericRequest; - this.wzMisc = new WzMisc(); - this.wazuhConfig = new WazuhConfig(); - - if (this.wzMisc.getWizard()) { - window.sessionStorage.removeItem('healthCheck'); - this.wzMisc.setWizard(false); - } - this.urlTabRegex = new RegExp('tab=' + '[^&]*'); - this.tabNames = TabNames; - this.apiIsDown = this.wzMisc.getApiIsDown(); - this.state = { - currentApiEntryIndex: false, - tab: 'api', - load: true, - loadingLogs: true, - indexPatterns: [], - apiEntries: [], +const Views = { + api: () => ( +
+
+ +
+
+ ), + configuration: () => ( +
+ +
+ ), + miscellaneous: () => ( +
+ +
+ ), + about: ({ appInfo }) => ( +
+ +
+ ), + sample_data: () => ( +
+ +
+ ), +}; + +const configurationTabID = 'configuration'; + +const mapStateToProps = state => ({ + configurationUIEditable: + state.appConfig.data['configuration.ui_api_editable'], + configurationIPSelector: state.appConfig.data['ip.selector'], +}); + +const mapDispatchToProps = dispatch => ({ + updateGlobalBreadcrumb: breadcrumb => + dispatch(updateGlobalBreadcrumb(breadcrumb)), +}); + +export const Settings = compose( + withReduxProvider, + connect(mapStateToProps, mapDispatchToProps), +)( + class Settings extends React.Component { + state: { + tab: string; + tabs: { id: string; name: string }[] | null; + load: boolean; + currentApiEntryIndex; + indexPatterns; + apiEntries; }; - - this.googleGroupsSVG = getHttp().basePath.prepend( - getAssetURL('images/icons/google_groups.svg'), - ); - this.tabsConfiguration = [ - { id: 'configuration', name: 'Configuration' }, - { id: 'miscellaneous', name: 'Miscellaneous' }, - ]; - } - - /** - * Parses the tab query param and returns the tab value - * @returns string - */ - _getTabFromUrl() { - const match = window.location.href.match(this.urlTabRegex); - return match?.[0]?.split('=')?.[1] ?? ''; - } - - _setTabFromUrl(tab?) { - window.location.href = window.location.href.replace( - this.urlTabRegex, - tab ? `tab=${tab}` : '', - ); - } - - componentDidMount(): void { - this.onInit(); - } - /** - * On load - */ - async onInit() { - try { - const urlTab = this._getTabFromUrl(); - - if (urlTab) { - const tabActiveName = Applications.find( - ({ id }) => getWzCurrentAppID() === id, - ).breadcrumbLabel; - const breadcrumb = [{ text: tabActiveName }]; - store.dispatch(updateGlobalBreadcrumb(breadcrumb)); - } else { - const breadcrumb = [{ text: serverApis.breadcrumbLabel }]; - store.dispatch(updateGlobalBreadcrumb(breadcrumb)); + wzMisc: WzMisc; + tabsConfiguration: { id: string; name: string }[]; + apiIsDown; + googleGroupsSVG; + currentDefault; + appInfo; + urlTabRegex; + + constructor(props) { + super(props); + + this.wzMisc = new WzMisc(); + + if (this.wzMisc.getWizard()) { + window.sessionStorage.removeItem('healthCheck'); + this.wzMisc.setWizard(false); } - - // Set component props - this.setComponentProps(urlTab); - - // Loading data - await this.getSettings(); - - await this.getAppInfo(); - } catch (error) { - const options = { - context: `${Settings.name}.onInit`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Cannot initialize Settings`, - }, + this.urlTabRegex = new RegExp('tab=' + '[^&]*'); + this.apiIsDown = this.wzMisc.getApiIsDown(); + this.state = { + currentApiEntryIndex: false, + tab: 'api', + tabs: null, + load: true, + indexPatterns: [], + apiEntries: [], }; - getErrorOrchestrator().handleError(options); + + this.googleGroupsSVG = getHttp().basePath.prepend( + getAssetURL('images/icons/google_groups.svg'), + ); + this.tabsConfiguration = [ + { id: configurationTabID, name: 'Configuration' }, + { id: 'miscellaneous', name: 'Miscellaneous' }, + ]; } - } - - /** - * Sets the component props - */ - setComponentProps(currentTab = 'api') { - const settingsTabsProps = { - clickAction: tab => { - this.switchTab(tab); - }, - selectedTab: currentTab, - // Define tabs for Wazuh plugin settings application - tabs: - getWzCurrentAppID() === appSettings.id ? this.tabsConfiguration : null, - wazuhConfig: this.wazuhConfig, - }; - this.setState({ - tab: currentTab, - settingsTabsProps, - }); - } - - /** - * This switch to a selected tab - * @param {Object} tab - */ - switchTab(tab) { - this.setState({ tab }); - this._setTabFromUrl(tab); - } - - // Get current API index - getCurrentAPIIndex() { - if (this.state.apiEntries.length) { - const idx = this.state.apiEntries - .map(entry => entry.id) - .indexOf(this.currentDefault); - this.setState({ currentApiEntryIndex: idx }); + /** + * Parses the tab query param and returns the tab value + * @returns string + */ + _getTabFromUrl() { + const match = window.location.href.match(this.urlTabRegex); + return match?.[0]?.split('=')?.[1] ?? ''; } - } - - /** - * Compare the string param with currentAppID - * @param {string} appToCompare - * It use into plugins/main/public/templates/settings/settings.html to show tabs into expecified App - */ - compareCurrentAppID(appToCompare) { - return getWzCurrentAppID() === appToCompare; - } - - /** - * Returns the index of the API in the entries array - * @param {Object} api - */ - getApiIndex(api) { - return this.state.apiEntries.map(entry => entry.id).indexOf(api.id); - } - - /** - * Checks the API entries status in order to set if there are online, offline or unknown. - */ - async checkApisStatus() { - try { - let numError = 0; - for (let idx in this.state.apiEntries) { - try { - await this.checkManager(this.state.apiEntries[idx], false, true); - this.state.apiEntries[idx].status = 'online'; - } catch (error) { - const code = ((error || {}).data || {}).code; - const downReason = - typeof error === 'string' - ? error - : (error || {}).message || - ((error || {}).data || {}).message || - 'Wazuh is not reachable'; - const status = code === 3099 ? 'down' : 'unknown'; - this.state.apiEntries[idx].status = { status, downReason }; - numError = numError + 1; - if (this.state.apiEntries[idx].id === this.currentDefault) { - // if the selected API is down, we remove it so a new one will selected - AppState.removeCurrentAPI(); - } + + _setTabFromUrl(tab?) { + window.location.href = window.location.href.replace( + this.urlTabRegex, + tab ? `tab=${tab}` : '', + ); + } + + async componentDidMount(): Promise { + try { + const urlTab = this._getTabFromUrl(); + + if (urlTab) { + const tabActiveName = Applications.find( + ({ id }) => getWzCurrentAppID() === id, + ).breadcrumbLabel; + const breadcrumb = [{ text: tabActiveName }]; + this.props.updateGlobalBreadcrumb(breadcrumb); + } else { + const breadcrumb = [{ text: serverApis.breadcrumbLabel }]; + this.props.updateGlobalBreadcrumb(breadcrumb); } + + // Set component props + this.setComponentProps(urlTab); + + // Loading data + await this.getSettings(); + + await this.getAppInfo(); + } catch (error) { + const options = { + context: `${Settings.name}.onInit`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Cannot initialize Settings`, + }, + }; + getErrorOrchestrator().handleError(options); } - return numError; - } catch (error) { - const options = { - context: `${Settings.name}.checkApisStatus`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); } - } - - // Set default API - async setDefault(item) { - try { - await this.checkManager(item, false, true); - const index = this.getApiIndex(item); - const api = this.state.apiEntries[index]; - const { cluster_info, id } = api; - const { manager, cluster, status } = cluster_info; - - // Check the connection before set as default - AppState.setClusterInfo(cluster_info); - const clusterEnabled = status === 'disabled'; - AppState.setCurrentAPI( - JSON.stringify({ - name: clusterEnabled ? manager : cluster, - id: id, - }), - ); - const currentApi = AppState.getCurrentAPI(); - this.currentDefault = JSON.parse(currentApi).id; - const idApi = api.id; - - ErrorHandler.info(`API with id ${idApi} set as default`); - - this.getCurrentAPIIndex(); - - return this.currentDefault; - } catch (error) { - const options = { - context: `${Settings.name}.setDefault`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); + isConfigurationUIEditable() { + return this.props.configurationUIEditable; } - } + /** + * Sets the component props + */ + setComponentProps(currentTab: keyof typeof Views = 'api') { + let tab = currentTab; - // Get settings function - async getSettings() { - try { - try { - this.setState({ - indexPatterns: await SavedObject.getListOfWazuhValidIndexPatterns(), - }); - } catch (error) { - this.wzMisc.setBlankScr('Sorry but no valid index patterns were found'); - this._setTabFromUrl(null); - location.hash = '#/blank-screen'; - return; + if ( + currentTab === configurationTabID && + !this.isConfigurationUIEditable() + ) { + // Change the inaccessible configuration to another accessible + tab = this.tabsConfiguration.find( + ({ id }) => id !== configurationTabID, + )!.id; + this._setTabFromUrl(tab); } + this.setState({ + tab, + tabs: + getWzCurrentAppID() === appSettings.id + ? // WORKAROUND: This avoids the configuration tab is displayed + this.tabsConfiguration.filter(({ id }) => + !this.isConfigurationUIEditable() + ? id !== configurationTabID + : true, + ) + : null, + }); + } - await this.getHosts(); + /** + * This switch to a selected tab + * @param {Object} tab + */ + switchTab(tab) { + this.setState({ tab }); + this._setTabFromUrl(tab); + } + + // Get current API index + getCurrentAPIIndex() { + if (this.state.apiEntries.length) { + const idx = this.state.apiEntries + .map(entry => entry.id) + .indexOf(this.currentDefault); + this.setState({ currentApiEntryIndex: idx }); + } + } - const currentApi = AppState.getCurrentAPI(); + /** + * Returns the index of the API in the entries array + * @param {Object} api + */ + getApiIndex(api) { + return this.state.apiEntries.map(entry => entry.id).indexOf(api.id); + } - if (currentApi) { - const { id } = JSON.parse(currentApi); - this.currentDefault = id; + // Get settings function + async getSettings() { + try { + try { + this.setState({ + indexPatterns: await SavedObject.getListOfWazuhValidIndexPatterns(), + }); + } catch (error) { + this.wzMisc.setBlankScr( + 'Sorry but no valid index patterns were found', + ); + this._setTabFromUrl(null); + location.hash = '#/blank-screen'; + return; + } + + await this.getHosts(); + + const currentApi = AppState.getCurrentAPI(); + + if (currentApi) { + const { id } = JSON.parse(currentApi); + this.currentDefault = id; + } + this.getCurrentAPIIndex(); + + // TODO: what is the purpose of this? + if ( + !this.state.currentApiEntryIndex && + this.state.currentApiEntryIndex !== 0 + ) { + return; + } + } catch (error) { + const options = { + context: `${Settings.name}.getSettings`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error getting API entries`, + }, + }; + getErrorOrchestrator().handleError(options); } - this.getCurrentAPIIndex(); + return; + } - if ( - !this.state.currentApiEntryIndex && - this.state.currentApiEntryIndex !== 0 - ) { - return; + // Check manager connectivity + async checkManager(item, isIndex?, silent = false) { + try { + // Get the index of the API in the entries + const index = isIndex ? item : this.getApiIndex(item); + + // Get the Api information + const api = this.state.apiEntries[index]; + const { username, url, port, id } = api; + const tmpData = { + username: username, + url: url, + port: port, + cluster_info: {}, + insecure: 'true', + id: id, + }; + + // Test the connection + const data = await ApiCheck.checkApi(tmpData, true); + tmpData.cluster_info = data?.data; + const { cluster_info } = tmpData; + // Updates the cluster-information in the registry + this.state.apiEntries[index].cluster_info = cluster_info; + this.state.apiEntries[index].status = 'online'; + this.state.apiEntries[index].allow_run_as = data.data.allow_run_as; + this.wzMisc.setApiIsDown(false); + !silent && ErrorHandler.info('Connection success', 'Settings'); + } catch (error) { + this.setState({ load: false }); + if (!silent) { + const options = { + context: `${Settings.name}.checkManager`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + return Promise.reject(error); } - } catch (error) { - const options = { - context: `${Settings.name}.getSettings`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error getting API entries`, - }, - }; - getErrorOrchestrator().handleError(options); } - return; - } - - // Check manager connectivity - async checkManager(item, isIndex?, silent = false) { - try { - // Get the index of the API in the entries - const index = isIndex ? item : this.getApiIndex(item); - - // Get the Api information - const api = this.state.apiEntries[index]; - const { username, url, port, id } = api; - const tmpData = { - username: username, - url: url, - port: port, - cluster_info: {}, - insecure: 'true', - id: id, - }; - // Test the connection - const data = await ApiCheck.checkApi(tmpData, true); - tmpData.cluster_info = data?.data; - const { cluster_info } = tmpData; - // Updates the cluster-information in the registry - this.state.apiEntries[index].cluster_info = cluster_info; - this.state.apiEntries[index].status = 'online'; - this.state.apiEntries[index].allow_run_as = data.data.allow_run_as; - this.wzMisc.setApiIsDown(false); - !silent && ErrorHandler.info('Connection success', 'Settings'); - } catch (error) { - this.setState({ load: false }); - if (!silent) { + /** + * Returns Wazuh app info + */ + async getAppInfo() { + try { + const data = await GenericRequest.request('GET', '/api/setup'); + const response = data.data.data; + this.appInfo = { + 'app-version': response['app-version'], + revision: response['revision'], + }; + + this.setState({ load: false }); + // TODO: this seems not to be used to display or not the index pattern selector + AppState.setPatternSelector(this.props.configurationIPSelector); + const pattern = AppState.getCurrentPattern(); + + this.getCurrentAPIIndex(); + if ( + (this.state.currentApiEntryIndex || + this.state.currentApiEntryIndex === 0) && + this.state.currentApiEntryIndex >= 0 + ) { + await this.checkManager(this.state.currentApiEntryIndex, true, true); + } + } catch (error) { + AppState.removeNavigation(); const options = { - context: `${Settings.name}.checkManager`, + context: `${Settings.name}.getAppInfo`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -391,154 +379,64 @@ export class Settings extends React.Component { }; getErrorOrchestrator().handleError(options); } - return Promise.reject(error); } - } - - /** - * This set the error, and checks if is updating - * @param {*} error - * @param {*} updating - */ - printError(error, updating) { - const text = ErrorHandler.handle(error, 'Settings'); - if (!updating) this.messageError = text; - else this.messageErrorUpdate = text; - } - - /** - * Returns Wazuh app info - */ - async getAppInfo() { - try { - const data = await this.genericReq.request('GET', '/api/setup'); - const response = data.data.data; - this.appInfo = { - 'app-version': response['app-version'], - revision: response['revision'], - }; - - this.setState({ load: false }); - const config = this.wazuhConfig.getConfig(); - AppState.setPatternSelector(config['ip.selector']); - const pattern = AppState.getCurrentPattern(); - this.getCurrentAPIIndex(); - if ( - (this.state.currentApiEntryIndex || - this.state.currentApiEntryIndex === 0) && - this.state.currentApiEntryIndex >= 0 - ) { - await this.checkManager(this.state.currentApiEntryIndex, true, true); + /** + * Get the API hosts + */ + async getHosts() { + try { + const result = await GenericRequest.request('GET', '/hosts/apis', {}); + const hosts = result.data || []; + this.setState({ + apiEntries: hosts, + }); + return hosts; + } catch (error) { + return Promise.reject(error); } - } catch (error) { - AppState.removeNavigation(); - const options = { - context: `${Settings.name}.getAppInfo`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); } - } - - /** - * Get the API hosts - */ - async getHosts() { - try { - const result = await this.genericReq.request('GET', '/hosts/apis', {}); - const hosts = result.data || []; - this.setState({ - apiEntries: hosts, - }); - return hosts; - } catch (error) { - return Promise.reject(error); + + renderView(tab) { + // WORKAROUND: This avoids the configuration view is displayed + if (tab === configurationTabID && !this.isConfigurationUIEditable()) { + return null; + } + const View = Views[tab]; + return View ? : null; } - } - - /** - * Copy to the clickboard the string passed - * @param {String} msg - */ - copyToClipBoard(msg) { - const el = document.createElement('textarea'); - el.value = msg; - document.body.appendChild(el); - el.select(); - document.execCommand('copy'); - document.body.removeChild(el); - ErrorHandler.info('Error copied to the clipboard'); - } - - render() { - return ( -
- {this.state.load ? ( -
- -
- ) : null} - {/* It must get renderized only in configuration app to show Miscellaneous tab in configuration App */} - {!this.state.load && - !this.apiIsDown && - this.state.settingsTabsProps?.tabs ? ( -
- -
- ) : null} - {/* end head */} - - {/* api */} - {this.state.tab === 'api' && !this.state.load ? ( -
- {/* API table section */} -
- + + render() { + return ( +
+ {this.state.load ? ( +
+
-
- ) : null} - {/* End API configuration card section */} - {/* end api */} - - {/* configuration */} - {this.state.tab === 'configuration' && !this.state.load ? ( -
- -
- ) : null} - {/* end configuration */} - {/* miscellaneous */} - {this.state.tab === 'miscellaneous' && !this.state.load ? ( -
- -
- ) : null} - {/* end miscellaneous */} - {/* about */} - {this.state.tab === 'about' && !this.state.load ? ( -
- -
- ) : null} - {/* end about */} - {/* sample data */} - {this.state.tab === 'sample_data' && !this.state.load ? ( -
- -
- ) : null} - {/* end sample data */} -
- ); - } -} + ) : null} + {/* It must get renderized only in configuration app to show Miscellaneous tab in configuration App */} + {!this.state.load && ( + <> + {!this.apiIsDown && this.state.tabs && ( +
+ + {this.state.tabs.map(tab => ( + this.switchTab(tab.id)} + > + {tab.name} + + ))} + +
+ )} + {this.renderView(this.state.tab)} + + )} +
+ ); + } + }, +); diff --git a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js index 9cb91c89c2..da238bb4fc 100644 --- a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js +++ b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js @@ -34,7 +34,6 @@ import { } from '../../../../../components/common/hocs'; import { PromptStatisticsDisabled } from './prompt-statistics-disabled'; import { PromptStatisticsNoIndices } from './prompt-statistics-no-indices'; -import { WazuhConfig } from '../../../../../react-services/wazuh-config'; import { WzRequest } from '../../../../../react-services/wz-request'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; @@ -43,8 +42,7 @@ import { getCore } from '../../../../../kibana-services'; import { appSettings, statistics } from '../../../../../utils/applications'; import { RedirectAppLinks } from '../../../../../../../../src/plugins/opensearch_dashboards_react/public'; import { DashboardTabsPanels } from '../../../../../components/overview/server-management-statistics/dashboards/dashboardTabsPanels'; - -const wzConfig = new WazuhConfig(); +import { connect } from 'react-redux'; export class WzStatisticsOverview extends Component { _isMounted = false; @@ -174,19 +172,21 @@ export class WzStatisticsOverview extends Component { - - - - Settings - - - + {this.props.configurationUIEditable && ( + + + + Settings + + + + )} @@ -215,14 +215,21 @@ export class WzStatisticsOverview extends Component { } } +const mapStateToProps = state => ({ + statisticsEnabled: state.appConfig.data?.['cron.statistics.status'], + configurationUIEditable: + state.appConfig.data?.['configuration.ui_api_editable'], +}); + export default compose( withGlobalBreadcrumb([{ text: statistics.breadcrumbLabel }]), withUserAuthorizationPrompt([ { action: 'cluster:status', resource: '*:*:*' }, { action: 'cluster:read', resource: 'node:id:*' }, ]), + connect(mapStateToProps), withGuard(props => { - return !(wzConfig.getConfig() || {})['cron.statistics.status']; // if 'cron.statistics.status' is false, then it renders PromptStatisticsDisabled component + return !props.statisticsEnabled; }, PromptStatisticsDisabled), )(props => { const [loading, setLoading] = useState(false); diff --git a/plugins/main/public/plugin.ts b/plugins/main/public/plugin.ts index f8dee3b0e8..2214c6d21c 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -149,9 +149,9 @@ export class WazuhPlugin ? undefined : 'Interval is not valid.'; }); + setWzCurrentAppID(id); // Set the dynamic redirection setWzMainParams(redirectTo()); - setWzCurrentAppID(id); initializeInterceptor(core); if (!this.initializeInnerAngular) { throw Error( diff --git a/plugins/main/server/controllers/decorators.test.ts b/plugins/main/server/controllers/decorators.test.ts new file mode 100644 index 0000000000..57da5b1900 --- /dev/null +++ b/plugins/main/server/controllers/decorators.test.ts @@ -0,0 +1,175 @@ +import { + routeDecoratorProtectedAdministrator, + routeDecoratorConfigurationAPIEditable, + compose, +} from './decorators'; + +const mockContext = ({ + isAdmin = false, + isConfigurationAPIEditable = false, +}: { + isAdmin?: boolean; + isConfigurationAPIEditable: boolean; +}) => ({ + wazuh: { + security: { + getCurrentUser: () => {}, + }, + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + get: jest.fn(() => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + })), + }, + }, + wazuh_core: { + configuration: { + get: jest.fn(async () => isConfigurationAPIEditable), + }, + dashboardSecurity: { + isAdministratorUser: () => ({ + administrator: isAdmin, + administrator_requirements: !isAdmin + ? 'User is not administrator' + : null, + }), + }, + }, +}); + +const mockRequest = () => { + return {}; +}; + +const mockResponse = () => { + const mockRes = jest.fn(data => data); + return { + ok: mockRes, + forbidden: mockRes, + custom: mockRes, + }; +}; + +describe('route decorator: routeDecoratorProtectedAdministrator', () => { + it.each` + title | isAdmin | isHandlerRun | responseBodyMessage + ${'Run handler'} | ${true} | ${true} | ${null} + ${'Avoid handler is run'} | ${false} | ${false} | ${'403 - User is not administrator'} + `( + `$title`, + async ({ + isAdmin, + isHandlerRun, + responseBodyMessage, + }: { + isAdmin: boolean; + isHandlerRun: boolean; + responseBodyMessage: string | null; + }) => { + const mockHandler = jest.fn(); + const wrappedHandler = + routeDecoratorProtectedAdministrator(3021)(mockHandler); + const response = await wrappedHandler( + mockContext({ isAdmin }), + mockRequest(), + mockResponse(), + ); + + if (isHandlerRun) { + expect(mockHandler).toHaveBeenCalled(); + } else { + expect(mockHandler).not.toHaveBeenCalled(); + } + + responseBodyMessage && + expect(response.body.message).toBe(responseBodyMessage); + }, + ); +}); + +describe('route decorator: routeDecoratorConfigurationAPIEditable', () => { + it.each` + title | isConfigurationAPIEditable | isHandlerRun | responseBodyMessage + ${'Run handler'} | ${true} | ${true} | ${null} + ${'Avoid handler is run'} | ${false} | ${false} | ${'The ability to edit the configuration from API is disabled. This can be enabled using configuration.ui_api_editable setting from the configuration file. Contact with an administrator.'} + `( + `$title`, + async ({ + isConfigurationAPIEditable, + isHandlerRun, + responseBodyMessage, + }: { + isConfigurationAPIEditable: boolean; + isHandlerRun: boolean; + responseBodyMessage: string | null; + }) => { + const mockHandler = jest.fn(); + const wrappedHandler = + routeDecoratorConfigurationAPIEditable(3021)(mockHandler); + const response = await wrappedHandler( + mockContext({ isConfigurationAPIEditable }), + mockRequest(), + mockResponse(), + ); + + if (isHandlerRun) { + expect(mockHandler).toHaveBeenCalled(); + } else { + expect(mockHandler).not.toHaveBeenCalled(); + } + + responseBodyMessage && + expect(response.body.message).toBe(responseBodyMessage); + }, + ); +}); + +describe('route decorators composition', () => { + it.each` + title | config | isHandlerRun | responseBodyMessage + ${'Run handler'} | ${{ isConfigurationAPIEditable: true, isAdmin: true }} | ${true} | ${null} + ${'Avoid handler is run'} | ${{ isConfigurationAPIEditable: false, isAdmin: true }} | ${false} | ${'The ability to edit the configuration from API is disabled. This can be enabled using configuration.ui_api_editable setting from the configuration file. Contact with an administrator.'} + ${'Avoid handler is run'} | ${{ isConfigurationAPIEditable: true, isAdmin: false }} | ${false} | ${'403 - User is not administrator'} + ${'Avoid handler is run'} | ${{ isConfigurationAPIEditable: false, isAdmin: false }} | ${false} | ${'The ability to edit the configuration from API is disabled. This can be enabled using configuration.ui_api_editable setting from the configuration file. Contact with an administrator.'} + `( + `$title`, + async ({ + config, + isHandlerRun, + responseBodyMessage, + }: { + config: { + isAdmin: boolean; + isConfigurationAPIEditable: boolean; + }; + isHandlerRun: boolean; + responseBodyMessage: string | null; + }) => { + const mockHandler = jest.fn(); + const wrappedHandler = compose( + routeDecoratorConfigurationAPIEditable(3021), + routeDecoratorProtectedAdministrator(3021), + )(mockHandler); + const response = await wrappedHandler( + mockContext(config), + mockRequest(), + mockResponse(), + ); + + if (isHandlerRun) { + expect(mockHandler).toHaveBeenCalled(); + } else { + expect(mockHandler).not.toHaveBeenCalled(); + } + + responseBodyMessage && + expect(response.body.message).toBe(responseBodyMessage); + }, + ); +}); diff --git a/plugins/main/server/controllers/decorators.ts b/plugins/main/server/controllers/decorators.ts index d96765f8bd..e3f2c90ddf 100644 --- a/plugins/main/server/controllers/decorators.ts +++ b/plugins/main/server/controllers/decorators.ts @@ -1,22 +1,56 @@ import { ErrorResponse } from '../lib/error-response'; -export function routeDecoratorProtectedAdministrator( - routeHandler, - errorCode: number, -) { - return async (context, request, response) => { - try { - const { administrator, administrator_requirements } = - await context.wazuh_core.dashboardSecurity.isAdministratorUser( - context, - request, +export function routeDecoratorProtectedAdministrator(errorCode: number) { + return handler => { + return async (context, request, response) => { + try { + const { administrator, administrator_requirements } = + await context.wazuh_core.dashboardSecurity.isAdministratorUser( + context, + request, + ); + if (!administrator) { + return ErrorResponse(administrator_requirements, 403, 403, response); + } + return await handler(context, request, response); + } catch (error) { + return ErrorResponse(error.message || error, errorCode, 500, response); + } + }; + }; +} + +export function routeDecoratorConfigurationAPIEditable(errorCode) { + return handler => { + return async (context, request, response) => { + try { + const canEditConfiguration = await context.wazuh_core.configuration.get( + 'configuration.ui_api_editable', ); - if (!administrator) { - return ErrorResponse(administrator_requirements, 403, 403, response); + + if (!canEditConfiguration) { + return response.forbidden({ + body: { + message: + 'The ability to edit the configuration from API is disabled. This can be enabled using configuration.ui_api_editable setting from the configuration file. Contact with an administrator.', + }, + }); + } + return await handler(context, request, response); + } catch (error) { + return ErrorResponse(error.message || error, errorCode, 500, response); } - return await routeHandler(context, request, response); - } catch (error) { - return ErrorResponse(error.message || error, errorCode, 500, response); - } + }; }; } + +export function compose(...functions: Function[]) { + if (functions.length === 1) { + return functions[0]; + } + return functions.reduce( + (acc, fn) => + (...args: any) => + acc(fn(...args)), + ); +} diff --git a/plugins/main/server/controllers/wazuh-elastic.ts b/plugins/main/server/controllers/wazuh-elastic.ts index 5b462058f8..87af3f2743 100644 --- a/plugins/main/server/controllers/wazuh-elastic.ts +++ b/plugins/main/server/controllers/wazuh-elastic.ts @@ -641,7 +641,7 @@ export class WazuhElasticCtrl { * @param {*} response * {index: string, alerts: [...], count: number} or ErrorResponse */ - createSampleAlerts = routeDecoratorProtectedAdministrator( + createSampleAlerts = routeDecoratorProtectedAdministrator(1000)( async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest<{ category: string }>, @@ -724,7 +724,6 @@ export class WazuhElasticCtrl { return ErrorResponse(errorMessage || error, 1000, statusCode, response); } }, - 1000, ); /** * This deletes sample alerts @@ -733,7 +732,7 @@ export class WazuhElasticCtrl { * @param {*} response * {result: "deleted", index: string} or ErrorResponse */ - deleteSampleAlerts = routeDecoratorProtectedAdministrator( + deleteSampleAlerts = routeDecoratorProtectedAdministrator(1000)( async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest<{ category: string }>, @@ -779,7 +778,6 @@ export class WazuhElasticCtrl { return ErrorResponse(errorMessage || error, 1000, statusCode, response); } }, - 1000, ); async alerts( diff --git a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts index 7bffe972e3..1a65103498 100644 --- a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts @@ -22,7 +22,11 @@ import path from 'path'; import { createDirectoryIfNotExists } from '../../lib/filesystem'; import glob from 'glob'; import { getFileExtensionFromBuffer } from '../../../common/services/file-extension'; -import { routeDecoratorProtectedAdministrator } from '../decorators'; +import { + compose, + routeDecoratorConfigurationAPIEditable, + routeDecoratorProtectedAdministrator, +} from '../decorators'; // TODO: these controllers have no logs. We should include them. export class WazuhUtilsCtrl { @@ -71,7 +75,10 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Object} */ - updateConfiguration = routeDecoratorProtectedAdministrator( + updateConfiguration = compose( + routeDecoratorConfigurationAPIEditable(3021), + routeDecoratorProtectedAdministrator(3021), + )( async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, @@ -100,7 +107,6 @@ export class WazuhUtilsCtrl { }, }); }, - 3021, ); /** @@ -110,7 +116,10 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Object} Configuration File or ErrorResponse */ - uploadFile = routeDecoratorProtectedAdministrator( + uploadFile = compose( + routeDecoratorConfigurationAPIEditable(3022), + routeDecoratorProtectedAdministrator(3022), + )( async ( context: RequestHandlerContext, request: KibanaRequest, @@ -175,7 +184,6 @@ export class WazuhUtilsCtrl { }, }); }, - 3022, ); /** @@ -185,7 +193,10 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Object} Configuration File or ErrorResponse */ - deleteFile = routeDecoratorProtectedAdministrator( + deleteFile = compose( + routeDecoratorConfigurationAPIEditable(3023), + routeDecoratorProtectedAdministrator(3023), + )( async ( context: RequestHandlerContext, request: KibanaRequest, @@ -222,6 +233,5 @@ export class WazuhUtilsCtrl { }, }); }, - 3023, ); } diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts index 608c59abdb..9fa9841b8c 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts @@ -6,7 +6,6 @@ import { loggingSystemMock } from '../../../../../src/core/server/logging/loggin import { ByteSizeValue } from '@osd/config-schema'; import supertest from 'supertest'; import { WazuhUtilsRoutes } from './wazuh-utils'; -import { WazuhUtilsCtrl } from '../../controllers/wazuh-utils/wazuh-utils'; import fs from 'fs'; import path from 'path'; import glob from 'glob'; @@ -34,20 +33,22 @@ const context = { get: jest.fn(), set: jest.fn(), }, + dashboardSecurity: { + isAdministratorUser: jest.fn(), + }, }, }; +// Register settings +context.wazuh_core.configuration._settings.set('pattern', { + validate: () => undefined, + isConfigurableFromSettings: true, +}); + const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); let server, innerServer; -jest.mock('../../controllers/decorators', () => ({ - routeDecoratorProtectedAdministrator: - handler => - async (...args) => - handler(...args), -})); - beforeAll(async () => { // Create server const config = { @@ -89,6 +90,48 @@ afterAll(async () => { jest.clearAllMocks(); }); +describe('[endpoint] PUT /utils/configuration - protected route', () => { + it.each` + title | isConfigurationAPIEditable | isAdmin | responseStatusCode | responseBodyMessage + ${'test'} | ${true} | ${false} | ${403} | ${'403 - Mock: User has no permissions'} + ${'test'} | ${false} | ${true} | ${403} | ${'The ability to edit the configuration from API is disabled. This can be enabled using configuration.ui_api_editable setting from the configuration file. Contact with an administrator.'} + `( + '$title', + async ({ + isConfigurationAPIEditable, + isAdmin, + responseStatusCode, + responseBodyMessage, + }: { + isConfigurationAPIEditable: boolean; + isAdmin: boolean; + responseStatusCode: number; + responseBodyMessage: string | null; + }) => { + context.wazuh_core.configuration.get.mockReturnValueOnce( + isConfigurationAPIEditable, + ); + context.wazuh_core.dashboardSecurity.isAdministratorUser.mockReturnValueOnce( + { + administrator: isAdmin, + administrator_requirements: !isAdmin + ? 'Mock: User has no permissions' + : null, + }, + ); + const settings = { pattern: 'test-alerts-groupA-*' }; + const response = await supertest(innerServer.listener) + .put('/utils/configuration') + .send(settings) + .expect(responseStatusCode); + + if (responseBodyMessage) { + expect(response.body.message).toBe(responseBodyMessage); + } + }, + ); +}); + describe.skip('[endpoint] GET /utils/configuration', () => { it(`Get plugin configuration and ensure the hosts is not returned GET /utils/configuration - 200`, async () => { const initialConfig = { @@ -240,8 +283,6 @@ describe.skip('[endpoint] PUT /utils/configuration', () => { .send(settings) .expect(responseStatusCode); - console.log(response.body); - responseStatusCode === 200 && expect(response.body.data.updatedConfiguration).toEqual(settings); responseStatusCode === 200 && @@ -292,6 +333,8 @@ describe.skip('[endpoint] PUT /utils/configuration', () => { ${'checks.template'} | ${0} | ${400} | ${'[request body.checks.template]: expected value of type [boolean] but got [number]'} ${'checks.timeFilter'} | ${true} | ${200} | ${null} ${'checks.timeFilter'} | ${0} | ${400} | ${'[request body.checks.timeFilter]: expected value of type [boolean] but got [number]'} + ${'configuration.ui_api_editable'} | ${true} | ${200} | ${null} + ${'configuration.ui_api_editable'} | ${true} | ${400} | ${'[request body.configuration.ui_api_editable]: expected value of type [boolean] but got [number]'} ${'cron.prefix'} | ${'test'} | ${200} | ${null} ${'cron.prefix'} | ${'test space'} | ${400} | ${'[request body.cron.prefix]: No whitespaces allowed.'} ${'cron.prefix'} | ${''} | ${400} | ${'[request body.cron.prefix]: Value can not be empty.'} diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 59da67e7bd..83944ad2e9 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -866,6 +866,38 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, validate: SettingsValidator.isBoolean, }, + 'configuration.ui_api_editable': { + title: 'Configuration UI editable', + description: + 'Enable or disable the ability to edit the configuration from UI or API endpoints. When disabled, this can only be edited from the configuration file, the related API endpoints are disabled, and the UI is inaccessible.', + store: { + file: { + configurableManaged: false, + }, + }, + category: SettingCategory.GENERAL, + type: EpluginSettingType.switch, + defaultValue: true, + isConfigurableFromSettings: false, + requiresRestartingPluginPlatform: true, + options: { + switch: { + values: { + disabled: { label: 'false', value: false }, + enabled: { label: 'true', value: true }, + }, + }, + }, + uiFormTransformChangedInputValue: function ( + value: boolean | string, + ): boolean { + return Boolean(value); + }, + validateUIForm: function (value) { + return this.validate(value); + }, + validate: SettingsValidator.isBoolean, + }, 'cron.prefix': { title: 'Cron prefix', description: 'Define the index prefix of predefined jobs.', diff --git a/plugins/wazuh-core/common/plugin-settings.test.ts b/plugins/wazuh-core/common/plugin-settings.test.ts index 9345d688ce..9444fdd31d 100644 --- a/plugins/wazuh-core/common/plugin-settings.test.ts +++ b/plugins/wazuh-core/common/plugin-settings.test.ts @@ -37,6 +37,8 @@ describe('[settings] Input validation', () => { ${'checks.setup'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} ${'checks.template'} | ${true} | ${undefined} ${'checks.template'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} + ${'configuration.ui_api_editable'} | ${true} | ${undefined} + ${'configuration.ui_api_editable'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} ${'checks.timeFilter'} | ${true} | ${undefined} ${'checks.timeFilter'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} ${'cron.prefix'} | ${'test'} | ${undefined}