From a4489a4b0c85f089e116d27d8d9a917a5fc398f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Nov 2022 12:12:06 -0300 Subject: [PATCH] [Backport 4.4-2.3-wzd] Add centralized request service (#4854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add centralized request service (#4831) * create request handler * Add interceptor initialization to app mount * Implement request handling service * Add request disabling to server * Modify changelog * Use core plugin as parameter * Move services to public * Remove service from backend * Modify imports of the service * Add validation for unauthorized requests * Improve code quality * Remove duplicated logic * Add reload on unauthorized requests * Change when check-wazuh is executed * Remove unnecessary verifications * Improve code quality * fix: condition on request service and replace http status code by constant * fix: remove unused import * fix: default value of parameter in request service Co-authored-by: Federico Rodriguez Co-authored-by: Antonio David Gutiérrez (cherry picked from commit 99fbab16d7c6145cd3423c26901dd4df8a692639) * Added unregister and request interceptor Co-authored-by: Nico Guevara <42900763+Tostti@users.noreply.github.com> Co-authored-by: Álex Co-authored-by: Federico Rodriguez --- CHANGELOG.md | 1 + public/plugin.ts | 150 ++++++++++++----------- public/react-services/generic-request.js | 6 +- public/react-services/wz-api-check.js | 6 +- public/react-services/wz-request.ts | 7 +- public/services/request-handler.js | 57 +++++++++ public/utils/index.ts | 2 - public/utils/odfe-utils.ts | 32 ----- 8 files changed, 143 insertions(+), 118 deletions(-) create mode 100644 public/services/request-handler.js delete mode 100644 public/utils/odfe-utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b6269b68b0..9a5065a44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added a centralized service to handle the requests [#4831](https://github.com/wazuh/wazuh-kibana-app/pull/4831) - Added data-test-subj create policy [#4873](https://github.com/wazuh/wazuh-kibana-app/pull/4873) + ### Changed - Changed the HTTP verb from `GET` to `POST` in the requests to login to the Wazuh API [#4103](https://github.com/wazuh/wazuh-kibana-app/pull/4103) diff --git a/public/plugin.ts b/public/plugin.ts index ea8104d632..c414c07aa9 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -28,15 +28,15 @@ import { AppState } from './react-services/app-state'; import { setErrorOrchestrator } from './react-services/common-services'; import { ErrorOrchestratorService } from './react-services/error-orchestrator/error-orchestrator.service'; import { getThemeAssetURL, getAssetURL } from './utils/assets'; -import { WzRequest } from './react-services/wz-request'; import store from './redux/store'; import { updateAppConfig } from './redux/actions/appConfigActions'; +import { initializeInterceptor, unregisterInterceptor } from './services/request-handler'; const SIDEBAR_LOGO = 'customization.logo.sidebar'; const innerAngularName = 'app/wazuh'; export class WazuhPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} + constructor(private readonly initializerContext: PluginInitializerContext) { } public initializeInnerAngular?: () => void; private innerAngularInitialized: boolean = false; private stateUpdater = new BehaviorSubject(() => ({})); @@ -45,91 +45,95 @@ export class WazuhPlugin implements Plugin { - try { - if (!this.initializeInnerAngular) { - throw Error('Wazuh plugin method initializeInnerAngular is undefined'); - } + //Check if user has wazuh disabled and avoid registering the application if not + let response = { isWazuhDisabled: 1 }; + try { + response = await core.http.get('/api/check-wazuh'); + } + catch (error) { + console.error('plugin.ts: Error checking if Wazuh is enabled', error); + } - // Update redux app state logos with the custom logos - if (logosInitialState?.logos) { - store.dispatch(updateAppConfig(logosInitialState.logos)); - } - // hide the telemetry banner. - // Set the flag in the telemetry saved object as the notice was seen and dismissed - this.hideTelemetryBanner && await this.hideTelemetryBanner(); - setScopedHistory(params.history); - // Load application bundle - const { renderApp } = await import('./application'); - // Get start services as specified in kibana.json - const [coreStart, depsStart] = await core.getStartServices(); - setErrorOrchestrator(ErrorOrchestratorService); - setHttp(core.http); - setCookies(new Cookies()); - if(!AppState.checkCookies() || params.history.parentHistory.action === 'PUSH') { - window.location.reload(); - } - await this.initializeInnerAngular(); - //Check is user has Wazuh disabled - const response = await WzRequest.genericReq( - 'GET', - `/api/check-wazuh`, - ) + if (!response.isWazuhDisabled) { + core.application.register({ + id: `wazuh`, + title: 'Wazuh', + icon: core.http.basePath.prepend( + logosInitialState?.logos?.[SIDEBAR_LOGO] ? + getAssetURL(logosInitialState?.logos?.[SIDEBAR_LOGO]) : + getThemeAssetURL('icon.svg', UI_THEME)), + mount: async (params: AppMountParameters) => { + try { + initializeInterceptor(core); + if (!this.initializeInnerAngular) { + throw Error('Wazuh plugin method initializeInnerAngular is undefined'); + } - params.element.classList.add('dscAppWrapper', 'wz-app'); - const unmount = await renderApp(innerAngularName, params.element); - //Update if user has Wazuh disabled - this.stateUpdater.next(() => { - if (response.data.isWazuhDisabled) { - unmount(); + // Update redux app state logos with the custom logos + if (logosInitialState?.logos) { + store.dispatch(updateAppConfig(logosInitialState.logos)); + } + // hide the telemetry banner. + // Set the flag in the telemetry saved object as the notice was seen and dismissed + this.hideTelemetryBanner && await this.hideTelemetryBanner(); + setScopedHistory(params.history); + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + setErrorOrchestrator(ErrorOrchestratorService); + setHttp(core.http); + setCookies(new Cookies()); + if (!AppState.checkCookies() || params.history.parentHistory.action === 'PUSH') { + window.location.reload(); } - return { - status: response.data.isWazuhDisabled, - category: { - id: 'wazuh', - label: 'Wazuh', - order: 0, - euiIconType: core.http.basePath.prepend(logosInitialState?.logos?.[SIDEBAR_LOGO] ? getAssetURL(logosInitialState?.logos?.[SIDEBAR_LOGO]) : getThemeAssetURL('icon.svg', UI_THEME)), - }} - }) - return () => { - unmount(); - }; - }catch(error){ - console.debug(error); - } - }, - category: { - id: 'wazuh', - label: 'Wazuh', - order: 0, - euiIconType: core.http.basePath.prepend(logosInitialState?.logos?.[SIDEBAR_LOGO] ? getAssetURL(logosInitialState?.logos?.[SIDEBAR_LOGO]) : getThemeAssetURL('icon.svg', UI_THEME)), - }, - updater$: this.stateUpdater - }); + await this.initializeInnerAngular(); + params.element.classList.add('dscAppWrapper', 'wz-app'); + const unmount = await renderApp(innerAngularName, params.element); + this.stateUpdater.next(() => { + return { + status: response.isWazuhDisabled, + category: { + id: 'wazuh', + label: 'Wazuh', + order: 0, + euiIconType: core.http.basePath.prepend(logosInitialState?.logos?.[SIDEBAR_LOGO] ? getAssetURL(logosInitialState?.logos?.[SIDEBAR_LOGO]) : getThemeAssetURL('icon.svg', UI_THEME)), + } + } + }) + return () => { + unmount(); + unregisterInterceptor(); + }; + } catch (error) { + console.debug(error); + } + }, + category: { + id: 'wazuh', + label: 'Wazuh', + order: 0, + euiIconType: core.http.basePath.prepend(logosInitialState?.logos?.[SIDEBAR_LOGO] ? getAssetURL(logosInitialState?.logos?.[SIDEBAR_LOGO]) : getThemeAssetURL('icon.svg', UI_THEME)), + }, + updater$: this.stateUpdater + }); + } return {}; } public start(core: CoreStart, plugins: AppPluginStartDependencies): WazuhStart { // hide security alert - if(plugins.securityOss) { + if (plugins.securityOss) { plugins.securityOss.insecureCluster.hideAlert(true); }; - if(plugins?.telemetry?.telemetryNotifications?.setOptedInNoticeSeen) { + if (plugins?.telemetry?.telemetryNotifications?.setOptedInNoticeSeen) { // assign to a method to hide the telemetry banner used when the app is mounted this.hideTelemetryBanner = () => plugins.telemetry.telemetryNotifications.setOptedInNoticeSeen(); }; diff --git a/public/react-services/generic-request.js b/public/react-services/generic-request.js index 5c3a2d98ac..368ffcb567 100644 --- a/public/react-services/generic-request.js +++ b/public/react-services/generic-request.js @@ -10,14 +10,13 @@ * Find more information about this on the LICENSE file. */ -import axios from 'axios'; import { AppState } from './app-state'; import { WazuhConfig } from './wazuh-config'; import { ApiCheck } from './wz-api-check'; import { WzMisc } from '../factories/misc'; -import { OdfeUtils } from '../utils'; import { getHttp, getDataPlugin } from '../kibana-services'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; +import { request } from '../services/request-handler'; export class GenericRequest { static async request(method, path, payload = null, returnError = false) { @@ -81,7 +80,7 @@ export class GenericRequest { }; } - Object.assign(data, await axios(options)); + Object.assign(data, await request(options)); if (!data) { throw new Error( `Error doing a request to ${tmpUrl}, method: ${method}.` @@ -90,7 +89,6 @@ export class GenericRequest { return data; } catch (err) { - OdfeUtils.checkOdfeSessionExpired(err); //if the requests fails, we need to check if the API is down const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); if (currentApi && currentApi.id) { diff --git a/public/react-services/wz-api-check.js b/public/react-services/wz-api-check.js index cbf4bf1316..38cc0f957f 100644 --- a/public/react-services/wz-api-check.js +++ b/public/react-services/wz-api-check.js @@ -10,11 +10,11 @@ * Find more information about this on the LICENSE file. */ import { WazuhConfig } from './wazuh-config'; -import axios from 'axios'; import { AppState } from './app-state'; import { WzMisc } from '../factories/misc'; import { getHttp } from '../kibana-services'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; +import { request } from '../services/request-handler'; export class ApiCheck { static async checkStored(data, idChanged = false) { @@ -40,7 +40,7 @@ export class ApiCheck { AppState.setPatternSelector(configuration['ip.selector']); } - const response = await axios(options); + const response = await request(options); if (response.error) { return Promise.reject(response); @@ -79,7 +79,7 @@ export class ApiCheck { timeout: timeout || 20000 }; - const response = await axios(options); + const response = await request(options); if (response.error) { return Promise.reject(response); diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 3ece60c307..e9087a273f 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -9,16 +9,16 @@ * * Find more information about this on the LICENSE file. */ -import axios from 'axios'; import { AppState } from './app-state'; import { ApiCheck } from './wz-api-check'; import { WzAuthentication } from './wz-authentication'; import { WzMisc } from '../factories/misc'; import { WazuhConfig } from './wazuh-config'; -import { OdfeUtils } from '../utils'; import IApiResponse from './interfaces/api-response.interface'; import { getHttp } from '../kibana-services'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; +import { request } from '../services/request-handler'; + export class WzRequest { static wazuhConfig: any; @@ -58,7 +58,7 @@ export class WzRequest { timeout: timeout, }; - const data = await axios(options); + const data = await request(options); if (data['error']) { throw new Error(data['error']); @@ -66,7 +66,6 @@ export class WzRequest { return Promise.resolve(data); } catch (error) { - OdfeUtils.checkOdfeSessionExpired(error); //if the requests fails, we need to check if the API is down if(checkCurrentApiIsUp){ const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); diff --git a/public/services/request-handler.js b/public/services/request-handler.js new file mode 100644 index 0000000000..119668839c --- /dev/null +++ b/public/services/request-handler.js @@ -0,0 +1,57 @@ +import axios from 'axios'; +import { HTTP_STATUS_CODES } from '../../common/constants'; + +let allow = true; +export let unregisterInterceptor = () => { }; +const source = axios.CancelToken.source(); + +const disableRequests = () => { + allow = false; + source.cancel('Requests cancelled'); + return; +} + +export const initializeInterceptor = (core) => { + unregisterInterceptor = core.http.intercept({ + responseError: (httpErrorResponse, controller) => { + if ( + httpErrorResponse.response?.status === HTTP_STATUS_CODES.UNAUTHORIZED + ) { + disableRequests(); + } + }, + request: (current, controller) => { + if ( + !allow + ) { + throw new Error("Disable request"); + }; + }, + }); +} + +export const request = async (options = {}) => { + if (!allow) { + return Promise.reject('Requests are disabled'); + }; + if (!options.method || !options.url) { + return Promise.reject("Missing parameters"); + }; + options = { + ...options, cancelToken: source.token + }; + + if (allow) { + try { + const requestData = await axios(options); + return Promise.resolve(requestData); + } + catch (e) { + if (e.response?.data?.message === 'Unauthorized' || e.response?.data?.message === 'Authentication required') { + disableRequests(); + window.location.reload(); + } + return Promise.reject(e); + } + } +} diff --git a/public/utils/index.ts b/public/utils/index.ts index 3691c70d1c..007a96971b 100644 --- a/public/utils/index.ts +++ b/public/utils/index.ts @@ -1,5 +1,3 @@ -export * as OdfeUtils from './odfe-utils'; - export { checkPluginVersion } from './check-plugin-version'; export { addHelpMenuToAppChrome } from './add_help_menu_to_app'; \ No newline at end of file diff --git a/public/utils/odfe-utils.ts b/public/utils/odfe-utils.ts deleted file mode 100644 index d030a60b43..0000000000 --- a/public/utils/odfe-utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Wazuh app - Module with utilities for Opendistro - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import { AxiosError } from 'axios' - -type TAuthenticationRequiredError = { - statusCode: 401 - error: 'Unauthorized' - message: 'Authentication required' -} - -const isAuthenticationRequired = (e: any): e is TAuthenticationRequiredError => { - const statusCode = e.statusCode && e.statusCode === 401; - const error = e.error && e.error === 'Unauthorized'; - const message = e.message && e.message === 'Authentication required'; - return statusCode && error && message; -} - -export const checkOdfeSessionExpired = (error: AxiosError) => { - const { data } = (error || {}).response || {}; - if (isAuthenticationRequired(data || {})) { - location.reload(); - } -}