From b6b18377446ea770d3a2079626855741a9a6755d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 Apr 2024 13:53:32 +0200 Subject: [PATCH 01/11] feat(configuration): add ability to disable the edition of configuration from API endpoints - Add ability to disable the edition of configuration from API endpoints - Add plugin setting to manage this ability - Add route controlle decorator - Protect the related API route controllers to updating the configuration - Changed the sign of routeDecoratorProtectedAdministrator - Adapted its usage on the API endpoints - Create compose utility to compose functions - Add test related to API controllers decorators - Add test about PUT /utils/configuration related to API endpoint protection --- .../server/controllers/decorators.test.ts | 175 ++++++++++++++++++ plugins/main/server/controllers/decorators.ts | 66 +++++-- .../main/server/controllers/wazuh-elastic.ts | 6 +- .../controllers/wazuh-utils/wazuh-utils.ts | 24 ++- .../routes/wazuh-utils/wazuh-utils.test.ts | 63 ++++++- plugins/wazuh-core/common/constants.ts | 32 ++++ .../wazuh-core/common/plugin-settings.test.ts | 2 + 7 files changed, 331 insertions(+), 37 deletions(-) create mode 100644 plugins/main/server/controllers/decorators.test.ts 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 20a981bbeb..9acafa644a 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -857,6 +857,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 configuration file, the related API endpoints are disabled, and the UI is inaccesible.', + 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} From 054169f32e5bfb39f6f482f5b267b2c3a496e9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 Apr 2024 14:26:06 +0200 Subject: [PATCH 02/11] fix(typo): plugin settin --- plugins/wazuh-core/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 9acafa644a..d7f0987c98 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -860,7 +860,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '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 configuration file, the related API endpoints are disabled, and the UI is inaccesible.', + '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 inaccesible.', store: { file: { configurableManaged: false, From b1f475ed2800f418f85affb8ca0cdd33170e253f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 Apr 2024 14:26:44 +0200 Subject: [PATCH 03/11] fix: remove repeated message on updating configuration from UI --- .../public/components/settings/configuration/configuration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', }, }; From 6f253c6759efaff620d6b9608c75eaaa13f39069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 19 Apr 2024 14:28:04 +0200 Subject: [PATCH 04/11] changelog: add entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454a832f74..ce0aab3c20 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) ### Changed From a053a4b50990b52b8b9948dc113aac26a095b9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Apr 2024 09:17:08 +0200 Subject: [PATCH 05/11] remove: unused code on settings component --- .../public/components/settings/settings.tsx | 135 +----------------- 1 file changed, 3 insertions(+), 132 deletions(-) diff --git a/plugins/main/public/components/settings/settings.tsx b/plugins/main/public/components/settings/settings.tsx index c6121ccb13..ffd6d49291 100644 --- a/plugins/main/public/components/settings/settings.tsx +++ b/plugins/main/public/components/settings/settings.tsx @@ -57,8 +57,6 @@ export class Settings extends React.Component { tabNames; tabsConfiguration; apiIsDown; - messageError; - messageErrorUpdate; googleGroupsSVG; currentDefault; appInfo; @@ -70,7 +68,6 @@ export class Settings extends React.Component { this.pluginPlatformVersion = (pluginPlatform || {}).version || false; this.pluginAppName = PLUGIN_APP_NAME; - this.genericReq = GenericRequest; this.wzMisc = new WzMisc(); this.wazuhConfig = new WazuhConfig(); @@ -115,13 +112,7 @@ export class Settings extends React.Component { ); } - componentDidMount(): void { - this.onInit(); - } - /** - * On load - */ - async onInit() { + async componentDidMount(): void { try { const urlTab = this._getTabFromUrl(); @@ -199,15 +190,6 @@ export class Settings extends React.Component { } } - /** - * 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 @@ -216,92 +198,6 @@ export class Settings extends React.Component { 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(); - } - } - } - 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); - } - } - // Get settings function async getSettings() { try { @@ -395,23 +291,12 @@ export class Settings extends React.Component { } } - /** - * 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 data = await GenericRequest.request('GET', '/api/setup'); const response = data.data.data; this.appInfo = { 'app-version': response['app-version'], @@ -452,7 +337,7 @@ export class Settings extends React.Component { */ async getHosts() { try { - const result = await this.genericReq.request('GET', '/hosts/apis', {}); + const result = await GenericRequest.request('GET', '/hosts/apis', {}); const hosts = result.data || []; this.setState({ apiEntries: hosts, @@ -463,20 +348,6 @@ export class Settings extends React.Component { } } - /** - * 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 (
From 83e5316989b35d6e23dc03f9a0bae1d21c61ed75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Apr 2024 09:47:18 +0200 Subject: [PATCH 06/11] fix(settings): refactor some unused code --- .../components/settings/about/index.tsx | 6 +- .../public/components/settings/settings.tsx | 60 +++++++------------ 2 files changed, 25 insertions(+), 41 deletions(-) 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/settings.tsx b/plugins/main/public/components/settings/settings.tsx index ffd6d49291..b28e2aee45 100644 --- a/plugins/main/public/components/settings/settings.tsx +++ b/plugins/main/public/components/settings/settings.tsx @@ -10,10 +10,7 @@ * 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'; @@ -42,20 +39,15 @@ import { export class Settings extends React.Component { state: { tab: string; + tabs: { id: string; name: string }[] | null; load: boolean; - loadingLogs: boolean; - settingsTabsProps?; currentApiEntryIndex; indexPatterns; apiEntries; }; - pluginAppName: string; - pluginPlatformVersion: string | boolean; - genericReq; - wzMisc; + wzMisc: WzMisc; wazuhConfig; - tabNames; - tabsConfiguration; + tabsConfiguration: { id: string; name: string }[]; apiIsDown; googleGroupsSVG; currentDefault; @@ -65,9 +57,6 @@ export class Settings extends React.Component { constructor(props) { super(props); - this.pluginPlatformVersion = (pluginPlatform || {}).version || false; - this.pluginAppName = PLUGIN_APP_NAME; - this.wzMisc = new WzMisc(); this.wazuhConfig = new WazuhConfig(); @@ -76,13 +65,12 @@ export class Settings extends React.Component { this.wzMisc.setWizard(false); } this.urlTabRegex = new RegExp('tab=' + '[^&]*'); - this.tabNames = TabNames; this.apiIsDown = this.wzMisc.getApiIsDown(); this.state = { currentApiEntryIndex: false, tab: 'api', + tabs: null, load: true, - loadingLogs: true, indexPatterns: [], apiEntries: [], }; @@ -112,7 +100,7 @@ export class Settings extends React.Component { ); } - async componentDidMount(): void { + async componentDidMount(): Promise { try { const urlTab = this._getTabFromUrl(); @@ -154,20 +142,10 @@ export class Settings extends React.Component { * 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, + tabs: + getWzCurrentAppID() === appSettings.id ? this.tabsConfiguration : null, }); } @@ -222,6 +200,7 @@ export class Settings extends React.Component { } this.getCurrentAPIIndex(); + // TODO: what is the purpose of this? if ( !this.state.currentApiEntryIndex && this.state.currentApiEntryIndex !== 0 @@ -357,11 +336,19 @@ export class Settings extends React.Component {
) : 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 ? ( + {!this.state.load && !this.apiIsDown && this.state.tabs ? (
- + + {this.state.tabs.map(tab => ( + this.switchTab(tab.id)} + > + {tab.name} + + ))} +
) : null} {/* end head */} @@ -395,10 +382,7 @@ export class Settings extends React.Component { {/* about */} {this.state.tab === 'about' && !this.state.load ? (
- +
) : null} {/* end about */} From bdf11dec9c088494d3166f2213b7cc0dc0fda479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Apr 2024 10:44:42 +0200 Subject: [PATCH 07/11] feat(configuration): configuration tab accessibility depends on plugin setting --- .../public/components/settings/settings.tsx | 701 ++++++++++-------- plugins/main/public/plugin.ts | 2 +- 2 files changed, 373 insertions(+), 330 deletions(-) diff --git a/plugins/main/public/components/settings/settings.tsx b/plugins/main/public/components/settings/settings.tsx index b28e2aee45..dc9ec142d4 100644 --- a/plugins/main/public/components/settings/settings.tsx +++ b/plugins/main/public/components/settings/settings.tsx @@ -12,15 +12,13 @@ import React from 'react'; 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'; @@ -35,365 +33,410 @@ 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; - tabs: { id: string; name: string }[] | null; - load: boolean; - currentApiEntryIndex; - indexPatterns; - apiEntries; - }; - wzMisc: WzMisc; - wazuhConfig; - tabsConfiguration: { id: string; name: string }[]; - apiIsDown; - googleGroupsSVG; - currentDefault; - appInfo; - urlTabRegex; - - constructor(props) { - super(props); - - 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.apiIsDown = this.wzMisc.getApiIsDown(); - this.state = { - currentApiEntryIndex: false, - tab: 'api', - tabs: null, - load: 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; }; + wzMisc: WzMisc; + tabsConfiguration: { id: string; name: string }[]; + apiIsDown; + googleGroupsSVG; + currentDefault; + appInfo; + urlTabRegex; - 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}` : '', - ); - } - - async componentDidMount(): Promise { - 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)); - } + constructor(props) { + super(props); + + this.wzMisc = new WzMisc(); - // 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`, - }, + if (this.wzMisc.getWizard()) { + window.sessionStorage.removeItem('healthCheck'); + this.wzMisc.setWizard(false); + } + 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') { - this.setState({ - tab: currentTab, - tabs: - getWzCurrentAppID() === appSettings.id ? this.tabsConfiguration : null, - }); - } - - /** - * 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] ?? ''; } - } - - /** - * 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); - } - - // Get settings function - async getSettings() { - try { + + _setTabFromUrl(tab?) { + window.location.href = window.location.href.replace( + this.urlTabRegex, + tab ? `tab=${tab}` : '', + ); + } + + async componentDidMount(): Promise { 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; - } + 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); + } - await this.getHosts(); + // Set component props + this.setComponentProps(urlTab); - const currentApi = AppState.getCurrentAPI(); + // Loading data + await this.getSettings(); - if (currentApi) { - const { id } = JSON.parse(currentApi); - this.currentDefault = id; + 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); } - this.getCurrentAPIIndex(); + } + + isConfigurationUIEditable() { + return this.props.configurationUIEditable; + } + /** + * Sets the component props + */ + setComponentProps(currentTab: keyof typeof Views = 'api') { + let tab = currentTab; - // TODO: what is the purpose of this? if ( - !this.state.currentApiEntryIndex && - this.state.currentApiEntryIndex !== 0 + currentTab === configurationTabID && + !this.isConfigurationUIEditable() ) { - return; + // Change the inaccessible configuration to another accessible + tab = this.tabsConfiguration.find( + ({ id }) => id !== configurationTabID, + )!.id; + this._setTabFromUrl(tab); } - } 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.setState({ + tab, + tabs: + getWzCurrentAppID() === appSettings.id + ? // WORKAROUND: This avoids the configuration tab is displayed + this.tabsConfiguration.filter(({ id }) => + !this.isConfigurationUIEditable() + ? id !== configurationTabID + : true, + ) + : null, + }); } - 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) { + /** + * 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 }); + } + } + + /** + * 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); + } + + // 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}.checkManager`, + 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, + title: `${error.name}: Error getting API entries`, }, }; getErrorOrchestrator().handleError(options); } - return Promise.reject(error); + return; } - } - - /** - * 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 }); - const config = this.wazuhConfig.getConfig(); - AppState.setPatternSelector(config['ip.selector']); - const pattern = AppState.getCurrentPattern(); + // 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); - 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 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) { - 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 GenericRequest.request('GET', '/hosts/apis', {}); - const hosts = result.data || []; - this.setState({ - apiEntries: hosts, - }); - return hosts; - } catch (error) { - return Promise.reject(error); + + /** + * 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}.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); + } } - } - 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.tabs ? ( -
- - {this.state.tabs.map(tab => ( - this.switchTab(tab.id)} - > - {tab.name} - - ))} - -
- ) : null} - {/* end head */} - - {/* api */} - {this.state.tab === 'api' && !this.state.load ? ( -
- {/* API table section */} -
- + /** + * 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); + } + } + + renderView(tab) { + // WORKAROUND: This avoids the configuration view is displayed + if (tab === configurationTabID && !this.isConfigurationUIEditable()) { + return null; + } + const View = Views[tab]; + return View ? : null; + } + + 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/plugin.ts b/plugins/main/public/plugin.ts index 5f4f8b1844..2d0e5974b5 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -150,9 +150,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( From 2484db77177ad7c79892e7f5d8382a3b75cf5052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Apr 2024 11:02:53 +0200 Subject: [PATCH 08/11] fix(configuration: typo --- plugins/wazuh-core/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index d7f0987c98..d3f3ac3d7e 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -860,7 +860,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '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 inaccesible.', + '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, From 688a89a1ba832d109988619b98326c152c365099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Apr 2024 12:09:54 +0200 Subject: [PATCH 09/11] fix: replace branding reference --- plugins/main/public/components/settings/api/api-table.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/components/settings/api/api-table.js b/plugins/main/public/components/settings/api/api-table.js index 9bb6252bef..cf8057ca4e 100644 --- a/plugins/main/public/components/settings/api/api-table.js +++ b/plugins/main/public/components/settings/api/api-table.js @@ -225,7 +225,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) { @@ -292,7 +292,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; From 14fb38ce26fd147dd2cb9e2dce3521d2c4dcbf73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 26 Apr 2024 08:28:20 +0200 Subject: [PATCH 10/11] feat(configuration): hide the button to save the enrollment.dns plugin setting from the Deploy new agent guide --- .../server-address/server-address.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) 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)} + /> + + + )} ); }; From c97bd92a9f28d4116d4e6b325c8f2b4beca85536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 26 Apr 2024 13:47:57 +0200 Subject: [PATCH 11/11] feat(configuration): hide Settings button on Statistics application depending on if the configuration can editable through UI --- .../statistics/statistics-overview.js | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) 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);