diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index a5c3772c8f..f16177223e 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -76,16 +76,9 @@ export class WazuhApiCtrl { } } } - let token; - if (context.wazuh_core.manageHosts.isEnabledAuthWithRunAs(idHost)) { - token = await context.wazuh.api.client.asCurrentUser.authenticate( - idHost, - ); - } else { - token = await context.wazuh.api.client.asInternalUser.authenticate( - idHost, - ); - } + const token = await context.wazuh.api.client.asCurrentUser.authenticate( + idHost, + ); let textSecure = ''; if (context.wazuh.server.info.protocol === 'https') { diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index fcb642e2bb..3355bb96a4 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'; import { formatLabelValuePair } from './settings'; import { formatBytes } from './file-size'; -export interface ILogger { +export interface Logger { debug(message: string): void; info(message: string): void; warn(message: string): void; @@ -180,7 +180,7 @@ export class Configuration implements IConfiguration { store: IConfigurationStore | null = null; _settings: Map; _categories: Map; - constructor(private logger: ILogger, store: IConfigurationStore) { + constructor(private logger: Logger, store: IConfigurationStore) { this._settings = new Map(); this._categories = new Map(); this.setStore(store); diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index ef08e41595..a006dff3a8 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -11,6 +11,7 @@ import { } from '../common/constants'; import { DashboardSecurity } from './utils/dashboard-security'; import * as hooks from './hooks'; +import { CoreHTTPClient } from './services/http/http-client'; export class WazuhCorePlugin implements Plugin @@ -19,12 +20,21 @@ export class WazuhCorePlugin services: { [key: string]: any } = {}; public async setup(core: CoreSetup): Promise { const noop = () => {}; - const logger = { + // Debug logger + const consoleLogger = { + info: console.log, + error: console.error, + debug: console.debug, + warn: console.warn, + }; + // No operation logger + const noopLogger = { info: noop, error: noop, debug: noop, warn: noop, }; + const logger = noopLogger; this._internal.configurationStore = new ConfigurationStore( logger, core.http, @@ -44,9 +54,22 @@ export class WazuhCorePlugin this.services.configuration.registerCategory({ ...value, id: key }); }); + // Create dashboardSecurity this.services.dashboardSecurity = new DashboardSecurity(logger, core.http); + // Create http + this.services.http = new CoreHTTPClient(logger, { + getTimeout: async () => + (await this.services.configuration.get('timeout')) as number, + getURL: (path: string) => core.http.basePath.prepend(path), + getServerAPI: () => 'api-host-id', // TODO: implement + getIndexPatternTitle: async () => 'wazuh-alerts-*', // TODO: implement + http: core.http, + }); + + // Setup services await this.services.dashboardSecurity.setup(); + await this.services.http.setup(); return { ...this.services, @@ -60,6 +83,7 @@ export class WazuhCorePlugin setCore(core); setUiSettings(core.uiSettings); + // Start services await this.services.configuration.start({ http: core.http }); return { diff --git a/plugins/wazuh-core/public/services/http/README.md b/plugins/wazuh-core/public/services/http/README.md new file mode 100644 index 0000000000..96b14e9807 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/README.md @@ -0,0 +1,105 @@ +# HTTPClient + +The `HTTPClient` provides a custom mechanim to do an API request to the backend side. + +This defines a request interceptor that disables the requests when `core.http` returns a response with status code 401, avoiding a problem in the login flow (with SAML). + +The request interceptor is used in the clients: + +- generic +- server + +## Generic + +This client provides a method to run the request that injects some properties related to an index pattern and selected server API host in the headers of the API request that could be used for some backend endpoints + +### Usage + +#### Request + +```ts +plugins.wazuhCore.http.request('GET', '/api/check-api', {}); +``` + +## Server + +This client provides: + +- some methods to communicate with the Wazuh server API +- manage authentication with Wazuh server API +- store the login data + +### Usage + +#### Authentication + +```ts +plugins.wazuhCore.http.auth(); +``` + +#### Unauthentication + +```ts +plugins.wazuhCore.http.unauth(); +``` + +#### Request + +```ts +plugins.wazuhCore.http.request('GET', '/agents', {}); +``` + +#### CSV + +```ts +plugins.wazuhCore.http.csv('GET', '/agents', {}); +``` + +#### Check API id + +```ts +plugins.wazuhCore.http.checkApiById('api-host-id'); +``` + +#### Check API + +```ts +plugins.wazuhCore.http.checkApi(apiHostData); +``` + +#### Get user data + +```ts +plugins.wazuhCore.http.getUserData(); +``` + +The changes in the user data can be retrieved thourgh the `userData$` observable. + +```ts +plugins.wazuhCore.http.userData$.subscribe(userData => { + // do something with the data +}); +``` + +### Register interceptor + +In each application when this is mounted through the `mount` method, the request interceptor must be registered and when the application is unmounted must be unregistered. + +> We should research about the possibility to register/unregister the interceptor once in the `wazuh-core` plugin instead of registering/unregisting in each mount of application. + +```ts +// setup lifecycle plugin method + +// Register an application +core.application.register({ + // rest of registration properties + mount: () => { + // Register the interceptor + plugins.wazuhCore.http.register(); + return () => { + // Unregister the interceptor + plugins.wazuhCore.http.unregister(); + }; + }, +}); +``` diff --git a/plugins/wazuh-core/public/services/http/constants.ts b/plugins/wazuh-core/public/services/http/constants.ts new file mode 100644 index 0000000000..8ea7ec6d05 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/constants.ts @@ -0,0 +1,5 @@ +export const PLUGIN_PLATFORM_REQUEST_HEADERS = { + 'osd-xsrf': 'kibana', +}; + +export const HTTP_CLIENT_DEFAULT_TIMEOUT = 20000; diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts new file mode 100644 index 0000000000..7d193b3599 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -0,0 +1,127 @@ +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import { Logger } from '../../../common/services/configuration'; +import { + HTTPClientGeneric, + HTTPClientRequestInterceptor, + HTTPVerb, +} from './types'; + +interface GenericRequestServices { + request: HTTPClientRequestInterceptor['request']; + getURL(path: string): string; + getTimeout(): Promise; + getIndexPatternTitle(): Promise; + getServerAPI(): string; + checkAPIById(apiId: string): Promise; +} + +export class GenericRequest implements HTTPClientGeneric { + onErrorInterceptor?: (error: any) => Promise; + constructor( + private logger: Logger, + private services: GenericRequestServices, + ) {} + async request( + method: HTTPVerb, + path: string, + payload = null, + returnError = false, + ) { + try { + if (!method || !path) { + throw new Error('Missing parameters'); + } + const timeout = await this.services.getTimeout(); + const requestHeaders = { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }; + const url = this.services.getURL(path); + + try { + requestHeaders.pattern = await this.services.getIndexPatternTitle(); + } catch (error) {} + + try { + requestHeaders.id = this.services.getServerAPI(); + } catch (error) { + // Intended + } + var options = {}; + + if (method === 'GET') { + options = { + method: method, + headers: requestHeaders, + url: url, + timeout: timeout, + }; + } + if (method === 'PUT') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + if (method === 'POST') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + if (method === 'DELETE') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + + const data = await this.services.request(options); + if (!data) { + throw new Error(`Error doing a request to ${url}, method: ${method}.`); + } + + return data; + } catch (error) { + //if the requests fails, we need to check if the API is down + const currentApi = this.services.getServerAPI(); //JSON.parse(AppState.getCurrentAPI() || '{}'); + if (currentApi) { + try { + await this.services.checkAPIById(currentApi); + } catch (err) { + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + // if ( + // ['/settings', '/health-check', '/blank-screen'].every( + // pathname => + // !NavigationService.getInstance() + // .getPathname() + // .startsWith(pathname), + // ) + // ) { + // NavigationService.getInstance().navigate('/health-check'); + // } + } + } + // if(this.onErrorInterceptor){ + // await this.onErrorInterceptor(error) + // } + if (returnError) return Promise.reject(error); + return (((error || {}).response || {}).data || {}).message || false + ? Promise.reject(new Error(error.response.data.message)) + : Promise.reject(error || new Error('Server did not respond')); + } + } + setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise) { + this.onErrorInterceptor = onErrorInterceptor; + } +} diff --git a/plugins/wazuh-core/public/services/http/http-client.ts b/plugins/wazuh-core/public/services/http/http-client.ts new file mode 100644 index 0000000000..041574f958 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/http-client.ts @@ -0,0 +1,66 @@ +import { Logger } from '../../../common/services/configuration'; +import { HTTP_CLIENT_DEFAULT_TIMEOUT } from './constants'; +import { GenericRequest } from './generic-client'; +import { RequestInterceptorClient } from './request-interceptor'; +import { WzRequest } from './server-client'; +import { HTTPClient, HTTPClientRequestInterceptor } from './types'; + +interface HTTPClientServices { + http: any; + getTimeout(): Promise; + getURL(path: string): string; + getServerAPI(): string; + getIndexPatternTitle(): Promise; +} + +export class CoreHTTPClient implements HTTPClient { + private requestInterceptor: HTTPClientRequestInterceptor; + public generic; + public server; + private _timeout: number = HTTP_CLIENT_DEFAULT_TIMEOUT; + constructor(private logger: Logger, private services: HTTPClientServices) { + this.logger.debug('Creating client'); + // Create request interceptor + this.requestInterceptor = new RequestInterceptorClient( + logger, + this.services.http, + ); + + const getTimeout = async () => + (await this.services.getTimeout()) || this._timeout; + + const internalServices = { + getTimeout, + getServerAPI: this.services.getServerAPI, + getURL: this.services.getURL, + }; + + // Create clients + this.server = new WzRequest(logger, { + request: options => this.requestInterceptor.request(options), + ...internalServices, + }); + this.generic = new GenericRequest(logger, { + request: options => this.requestInterceptor.request(options), + getIndexPatternTitle: this.services.getIndexPatternTitle, + ...internalServices, + checkAPIById: apiId => this.server.checkAPIById(apiId), + }); + this.logger.debug('Created client'); + } + async setup() { + this.logger.debug('Setup'); + } + async start() {} + async stop() {} + async register() { + this.logger.debug('Starting client'); + this.requestInterceptor.init(); + this.logger.debug('Started client'); + } + async unregister() { + this.logger.debug('Stopping client'); + this.requestInterceptor.destroy(); + this.logger.debug('Stopped client'); + } +} diff --git a/plugins/wazuh-core/public/services/http/index.ts b/plugins/wazuh-core/public/services/http/index.ts new file mode 100644 index 0000000000..f6c9b1c770 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export { CoreHTTPClient } from './http-client'; diff --git a/plugins/wazuh-core/public/services/http/request-interceptor.ts b/plugins/wazuh-core/public/services/http/request-interceptor.ts new file mode 100644 index 0000000000..21b2600da3 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/request-interceptor.ts @@ -0,0 +1,84 @@ +import axios, { AxiosRequestConfig } from 'axios'; +import { HTTP_STATUS_CODES } from '../../../common/constants'; +import { Logger } from '../../../common/services/configuration'; +import { HTTPClientRequestInterceptor } from './types'; + +export class RequestInterceptorClient implements HTTPClientRequestInterceptor { + // define if the request is allowed to run + private _allow: boolean = true; + // store the cancel token to abort the requests + private _source: any; + // unregister the interceptor + private unregisterInterceptor: () => void = () => {}; + constructor(private logger: Logger, private http: any) { + this.logger.debug('Creating'); + this._source = axios.CancelToken.source(); + this.logger.debug('Created'); + } + private registerInterceptor() { + this.logger.debug('Registering interceptor in core http'); + this.unregisterInterceptor = this.http.intercept({ + responseError: (httpErrorResponse, controller) => { + if ( + httpErrorResponse.response?.status === HTTP_STATUS_CODES.UNAUTHORIZED + ) { + this.cancel(); + } + }, + request: (current, controller) => { + if (!this._allow) { + throw new Error('Disable request'); + } + }, + }); + this.logger.debug('Registered interceptor in core http'); + } + init() { + this.logger.debug('Initiating'); + this.registerInterceptor(); + this.logger.debug('Initiated'); + } + destroy() { + this.logger.debug('Destroying'); + this.logger.debug('Unregistering interceptor in core http'); + this.unregisterInterceptor(); + this.unregisterInterceptor = () => {}; + this.logger.debug('Unregistered interceptor in core http'); + this.logger.debug('Destroyed'); + } + cancel() { + this.logger.debug('Disabling requests'); + this._allow = false; + this._source.cancel('Requests cancelled'); + this.logger.debug('Disabled requests'); + } + async request(options: AxiosRequestConfig = {}) { + if (!this._allow) { + return Promise.reject('Requests are disabled'); + } + if (!options.method || !options.url) { + return Promise.reject('Missing parameters'); + } + const optionsWithCancelToken = { + ...options, + cancelToken: this._source?.token, + }; + + if (this._allow) { + try { + const requestData = await axios(optionsWithCancelToken); + return Promise.resolve(requestData); + } catch (error) { + if ( + error.response?.data?.message === 'Unauthorized' || + error.response?.data?.message === 'Authentication required' + ) { + this.cancel(); + // To reduce the dependencies, we use window object instead of the NavigationService + window.location.reload(); + } + throw error; + } + } + } +} diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts new file mode 100644 index 0000000000..a958962b55 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -0,0 +1,110 @@ +import { WzRequest } from './server-client'; + +const noop = () => {}; +const logger = { + debug: noop, + info: noop, + warn: noop, + error: noop, +}; + +const USER_TOKEN = + 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3YXp1aCIsImF1ZCI6IldhenVoIEFQSSBSRVNUIiwibmJmIjoxNzI2NzM3MDY3LCJleHAiOjE3MjY3Mzc5NjcsInN1YiI6IndhenVoLXd1aSIsInJ1bl9hcyI6ZmFsc2UsInJiYWNfcm9sZXMiOlsxXSwicmJhY19tb2RlIjoid2hpdGUifQ.AOL4dDe3c4WCYXMjqbkBqfKFAChtjvD_uZ0FXfLOMnfU0n6zPo61OZ43Kt0bYhW25BQIXR9Belb49gG3_qAIZpcaAQhQv4HPcL41ESRSvZc2wsa9_HYgV8Z7gieSuT15gdnSNogLKFS7yK5gQQivLo1e4QfVsDThrG_TVdJPbCG3GPq9'; + +function createClient() { + const mockRequest = jest.fn(options => { + console.log({ options }); + if (options.url === '/api/login') { + return { + data: { + token: USER_TOKEN, + }, + }; + } else if (options.url === '/api/request') { + if (options.data.path === '/security/users/me/policies') { + return { + data: { + rbac_mode: 'white', + }, + }; + } else if ( + options.data.method === 'DELETE' && + options.data.path === '/security/user/authenticate' + ) { + return { + data: { + message: 'User wazuh-wui was successfully logged out', + error: 0, + }, + }; + } + } + // if(path === '/security/users/me/policies'){ + + // } + }); + const client = new WzRequest(logger, { + getServerAPI: () => 'test', + getTimeout: () => Promise.resolve(1000), + getURL: path => path, + request: mockRequest, + }); + return { client, mockRequest }; +} + +describe('Create client', () => { + it('Ensure the initial userData value', done => { + const { client } = createClient(); + + client.userData$.subscribe(userData => { + expect(userData).toEqual({ + logged: false, + token: null, + account: null, + policies: null, + }); + done(); + }); + }); + + it('Authentication', done => { + const { client, mockRequest } = createClient(); + + client.auth().then(data => { + expect(data).toEqual({ + token: USER_TOKEN, + policies: {}, + account: null, + logged: true, + }); + + client.userData$.subscribe(userData => { + expect(userData).toEqual({ + token: USER_TOKEN, + policies: {}, + account: null, + logged: true, + }); + done(); + }); + }); + }); + + it.only('Unauthentication', done => { + const { client } = createClient(); + + client.unauth().then(data => { + expect(data).toEqual({}); + done(); + }); + }); + + it('Request', async () => { + const { client, mockRequest } = createClient(); + + const data = await client.request('GET', '/security/users/me/policies', {}); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(data).toEqual({ data: { rbac_mode: 'white' } }); + }); +}); diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts new file mode 100644 index 0000000000..86ea5c44c3 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -0,0 +1,532 @@ +/* + * Wazuh app - API request service + * Copyright (C) 2015-2024 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 { + HTTPClientRequestInterceptor, + HTTPClientServer, + HTTPVerb, + HTTPClientServerUserData, +} from './types'; +import { Logger } from '../../../common/services/configuration'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import jwtDecode from 'jwt-decode'; +import { BehaviorSubject } from 'rxjs'; + +interface WzRequestServices { + request: HTTPClientRequestInterceptor['request']; + getURL(path: string): string; + getTimeout(): Promise; + getServerAPI(): string; +} + +interface ServerAPIResponseItems { + affected_items: Array; + failed_items: Array; + total_affected_items: number; + total_failed_items: number; +} + +interface ServerAPIResponseItemsData { + data: ServerAPIResponseItems; + message: string; + error: number; +} + +export interface ServerAPIResponseItemsDataHTTPClient { + data: ServerAPIResponseItemsData; +} + +export class WzRequest implements HTTPClientServer { + onErrorInterceptor?: ( + error: any, + options: { + checkCurrentApiIsUp: boolean; + shouldRetry: boolean; + overwriteHeaders?: any; + }, + ) => Promise; + private userData: HTTPClientServerUserData; + userData$: BehaviorSubject; + constructor(private logger: Logger, private services: WzRequestServices) { + this.userData = { + logged: false, + token: null, + account: null, + policies: null, + }; + this.userData$ = new BehaviorSubject(this.userData); + } + + /** + * Permorn a generic request + * @param {String} method + * @param {String} path + * @param {Object} payload + */ + private async _request( + method: HTTPVerb, + path: string, + payload: any = null, + extraOptions: { + shouldRetry?: boolean; + checkCurrentApiIsUp?: boolean; + overwriteHeaders?: any; + } = { + shouldRetry: true, + checkCurrentApiIsUp: true, + overwriteHeaders: {}, + }, + ): Promise { + const shouldRetry = + typeof extraOptions.shouldRetry === 'boolean' + ? extraOptions.shouldRetry + : true; + const checkCurrentApiIsUp = + typeof extraOptions.checkCurrentApiIsUp === 'boolean' + ? extraOptions.checkCurrentApiIsUp + : true; + const overwriteHeaders = + typeof extraOptions.overwriteHeaders === 'object' + ? extraOptions.overwriteHeaders + : {}; + try { + if (!method || !path) { + throw new Error('Missing parameters'); + } + + const timeout = await this.services.getTimeout(); + + const url = this.services.getURL(path); + const options = { + method: method, + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + ...overwriteHeaders, + }, + url: url, + data: payload, + timeout: timeout, + }; + + const data = await this.services.request(options); + + if (data['error']) { + throw new Error(data['error']); + } + + return Promise.resolve(data); + } catch (error) { + //if the requests fails, we need to check if the API is down + if (checkCurrentApiIsUp) { + const currentApi = this.services.getServerAPI(); + if (currentApi) { + try { + await this.checkAPIById(currentApi); + } catch (error) { + // TODO :implement + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + // if ( + // !NavigationService.getInstance() + // .getPathname() + // .startsWith('/settings') + // ) { + // NavigationService.getInstance().navigate('/health-check'); + // } + throw error; + } + } + } + // if(this.onErrorInterceptor){ + // await this.onErrorInterceptor(error, {checkCurrentApiIsUp, shouldRetry, overwriteHeaders}) + // } + const errorMessage = + (error && + error.response && + error.response.data && + error.response.data.message) || + (error || {}).message; + if ( + typeof errorMessage === 'string' && + errorMessage.includes('status code 401') && + shouldRetry + ) { + try { + await this.auth(true); //await WzAuthentication.refresh(true); + return this._request(method, path, payload, { shouldRetry: false }); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject( + this.returnErrorInstance(error, error.data.message), + ) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + return errorMessage + ? Promise.reject(this.returnErrorInstance(error, errorMessage)) + : Promise.reject( + this.returnErrorInstance(error, 'Server did not respond'), + ); + } + } + + /** + * Perform a request to the Wazuh API + * @param {String} method Eg. GET, PUT, POST, DELETE + * @param {String} path API route + * @param {Object} body Request body + */ + async request( + method: HTTPVerb, + path: string, + body: any, + options: { + checkCurrentApiIsUp?: boolean; + returnOriginalResponse?: boolean; + } = { checkCurrentApiIsUp: true, returnOriginalResponse: false }, + ): Promise> { + try { + if (!method || !path || !body) { + throw new Error('Missing parameters'); + } + + const { returnOriginalResponse, ...optionsToGenericReq } = options; + + const id = this.services.getServerAPI(); + const requestData = { method, path, body, id }; + const response = await this._request( + 'POST', + '/api/request', + requestData, + optionsToGenericReq, + ); + + if (returnOriginalResponse) { + return response; + } + + const hasFailed = + (((response || {}).data || {}).data || {}).total_failed_items || 0; + + if (hasFailed) { + const error = + ((((response.data || {}).data || {}).failed_items || [])[0] || {}) + .error || {}; + const failed_ids = + ((((response.data || {}).data || {}).failed_items || [])[0] || {}) + .id || {}; + const message = (response.data || {}).message || 'Unexpected error'; + const errorMessage = `${message} (${error.code}) - ${error.message} ${ + failed_ids && failed_ids.length > 1 + ? ` Affected ids: ${failed_ids} ` + : '' + }`; + return Promise.reject(this.returnErrorInstance(null, errorMessage)); + } + return Promise.resolve(response); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + + /** + * Perform a request to generate a CSV + * @param {String} path + * @param {Object} filters + */ + async csv(path: string, filters: any) { + try { + if (!path || !filters) { + throw new Error('Missing parameters'); + } + const id = this.services.getServerAPI(); + const requestData = { path, id, filters }; + const data = await this._request('POST', '/api/csv', requestData); + return Promise.resolve(data); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + + /** + * Customize message and return an error object + * @param error + * @param message + * @returns error + */ + private returnErrorInstance(error: any, message: string | undefined) { + if (!error || typeof error === 'string') { + return new Error(message || error); + } + error.message = message; + return error; + } + + setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise) { + this.onErrorInterceptor = onErrorInterceptor; + } + + /** + * Requests and returns an user token to the API. + * + * @param {boolean} force + * @returns {string} token as string or Promise.reject error + */ + private async login(force = false) { + try { + let idHost = this.services.getServerAPI(); + while (!idHost) { + await new Promise(r => setTimeout(r, 500)); + idHost = this.services.getServerAPI(); + } + + const response = await this._request('POST', '/api/login', { + idHost, + force, + }); + + const token = ((response || {}).data || {}).token; + return token as string; + } catch (error) { + throw error; + } + } + + /** + * Refresh the user's token + * + * @param {boolean} force + * @returns {void} nothing or Promise.reject error + */ + async auth(force = false) { + try { + // Get user token + const token: string = await this.login(force); + if (!token) { + // Remove old existent token + // await this.unauth(); + return; + } + + // Decode token and get expiration time + const jwtPayload = jwtDecode(token); + + // Get user Policies + const userPolicies = await this.getUserPolicies(); + + // Dispatch actions to set permissions and administrator consideration + // TODO: implement + // store.dispatch(updateUserPermissions(userPolicies)); + + // store.dispatch( + // updateUserAccount( + // getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + // jwtPayload, + // ), + // ), + // ); + // store.dispatch(updateWithUserLogged(true)); + const data = { + token, + policies: userPolicies, + account: null, // TODO: implement + logged: true, + }; + + this.updateUserData(data); + return data; + } catch (error) { + // TODO: implement + // const options: UIErrorLog = { + // context: `${WzAuthentication.name}.refresh`, + // level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + // severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + // error: { + // error: error, + // message: error.message || error, + // title: `${error.name}: Error getting the authorization token`, + // }, + // }; + // getErrorOrchestrator().handleError(options); + // store.dispatch( + // updateUserAccount( + // getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + // {}, // This value should cause the user is not considered as an administrator + // ), + // ), + // ); + // store.dispatch(updateWithUserLogged(true)); + this.updateUserData({ + token: null, + policies: null, + account: null, // TODO: implement + logged: true, + }); + throw error; + } + } + + /** + * Get current user's policies + * + * @returns {Object} user's policies or Promise.reject error + */ + private async getUserPolicies() { + try { + let idHost = this.services.getServerAPI(); + while (!idHost) { + await new Promise(r => setTimeout(r, 500)); + idHost = this.services.getServerAPI(); + } + const response = await this.request( + 'GET', + '/security/users/me/policies', + { idHost }, + ); + return response?.data?.data || {}; + } catch (error) { + throw error; + } + } + + getUserData() { + return this.userData; + } + + /** + * Sends a request to the Wazuh's API to delete the user's token. + * + * @returns {Object} + */ + async unauth() { + try { + const response = await this.request( + 'DELETE', + '/security/user/authenticate', + { delay: 5000 }, + ); + + return response?.data?.data || {}; + } catch (error) { + throw error; + } + } + + /** + * Update the internal user data and emit the value to the subscribers of userData$ + * @param data + */ + private updateUserData(data: HTTPClientServerUserData) { + this.userData = data; + this.userData$.next(this.getUserData()); + } + + async checkAPIById(serverHostId: string, idChanged = false) { + try { + const timeout = await this.services.getTimeout(); + const payload = { id: serverHostId }; + if (idChanged) { + payload.idChanged = serverHostId; + } + + const url = this.services.getURL('/api/check-stored-api'); + const options = { + method: 'POST', + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }, + url: url, + data: payload, + timeout: timeout, + }; + + // TODO: implement + // if (Object.keys(configuration).length) { + // AppState.setPatternSelector(configuration['ip.selector']); + // } + + const response = await this.services.request(options); + + if (response.error) { + return Promise.reject(this.returnErrorInstance(response)); + } + + return response; + } catch (error) { + if (error.response) { + // TODO: implement + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + const response = (error.response.data || {}).message || error.message; + return Promise.reject(this.returnErrorInstance(response)); + } else { + return (error || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.message)) + : Promise.reject( + this.returnErrorInstance( + error, + error || 'Server did not respond', + ), + ); + } + } + } + + /** + * Check the status of an API entry + * @param {String} apiObject + */ + async checkAPI(apiEntry: any, forceRefresh = false) { + try { + const timeout = await this.services.getTimeout(); + const url = this.services.getURL('/api/check-api'); + + const options = { + method: 'POST', + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }, + url: url, + data: { ...apiEntry, forceRefresh }, + timeout: timeout, + }; + + const response = await this.services.request(options); + + if (response.error) { + return Promise.reject(this.returnErrorInstance(response)); + } + + return response; + } catch (error) { + if (error.response) { + const response = (error.response.data || {}).message || error.message; + return Promise.reject(this.returnErrorInstance(response)); + } else { + return (error || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.message)) + : Promise.reject( + this.returnErrorInstance( + error, + error || 'Server did not respond', + ), + ); + } + } + } +} diff --git a/plugins/wazuh-core/public/services/http/types.ts b/plugins/wazuh-core/public/services/http/types.ts new file mode 100644 index 0000000000..574670eb32 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/types.ts @@ -0,0 +1,49 @@ +import { AxiosRequestConfig } from 'axios'; +import { BehaviorSubject } from 'rxjs'; + +export interface HTTPClientRequestInterceptor { + init(): void; + destroy(): void; + cancel(): void; + request(options: AxiosRequestConfig): Promise; +} + +export type HTTPVerb = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; + +export interface HTTPClientGeneric { + request( + method: HTTPVerb, + path: string, + payload?: any, + returnError?: boolean, + ): Promise; +} + +export type HTTPClientServerUserData = { + token: string | null; + policies: any | null; + account: any | null; + logged: boolean; +}; + +export interface HTTPClientServer { + request( + method: HTTPVerb, + path: string, + body: any, + options: { + checkCurrentApiIsUp?: boolean; + returnOriginalResponse?: boolean; + }, + ): Promise; + csv(path: string, filters: any): Promise; + auth(force: boolean): Promise; + unauth(force: boolean): Promise; + userData$: BehaviorSubject; + getUserData(): HTTPClientServerUserData; +} + +export interface HTTPClient { + generic: HTTPClientGeneric; + server: HTTPClientServer; +} diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index a3acfa7c4d..942c5d6fe4 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -1,5 +1,6 @@ import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; +import { HTTPClient } from './services/http/types'; import { DashboardSecurity } from './utils/dashboard-security'; export interface WazuhCorePluginSetup { @@ -7,6 +8,7 @@ export interface WazuhCorePluginSetup { API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurity; + http: HTTPClient; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginStart { @@ -15,6 +17,7 @@ export interface WazuhCorePluginStart { API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurity; + http: HTTPClient; } export interface AppPluginStartDependencies {} diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts index 305291c21d..ab91cf002c 100644 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ b/plugins/wazuh-core/public/utils/configuration-store.ts @@ -1,6 +1,6 @@ import { IConfigurationStore, - ILogger, + Logger, IConfiguration, } from '../../common/services/configuration'; @@ -8,7 +8,7 @@ export class ConfigurationStore implements IConfigurationStore { private _stored: any; file: string = ''; configuration: IConfiguration | null = null; - constructor(private logger: ILogger, private http: any) { + constructor(private logger: Logger, private http: any) { this._stored = {}; } setConfiguration(configuration: IConfiguration) { diff --git a/plugins/wazuh-core/public/utils/dashboard-security.ts b/plugins/wazuh-core/public/utils/dashboard-security.ts index 669ef6fbae..dd0889c192 100644 --- a/plugins/wazuh-core/public/utils/dashboard-security.ts +++ b/plugins/wazuh-core/public/utils/dashboard-security.ts @@ -1,9 +1,9 @@ import { WAZUH_ROLE_ADMINISTRATOR_ID } from '../../common/constants'; -import { ILogger } from '../../common/services/configuration'; +import { Logger } from '../../common/services/configuration'; export class DashboardSecurity { private securityPlatform: string = ''; - constructor(private logger: ILogger, private http) {} + constructor(private logger: Logger, private http) {} private async fetchCurrentPlatform() { try { this.logger.debug('Fetching the security platform'); diff --git a/plugins/wazuh-core/server/services/server-api-client.ts b/plugins/wazuh-core/server/services/server-api-client.ts index be9622b642..cd87333fb1 100644 --- a/plugins/wazuh-core/server/services/server-api-client.ts +++ b/plugins/wazuh-core/server/services/server-api-client.ts @@ -64,6 +64,11 @@ export interface ServerAPIScopedUserClient { ) => Promise>; } +export interface ServerAPIAuthenticateOptions { + useRunAs: boolean; + authContext?: any; +} + /** * This service communicates with the Wazuh server APIs */ @@ -86,7 +91,8 @@ export class ServerAPIClient { // Create internal user client this.asInternalUser = { - authenticate: async apiHostID => await this._authenticate(apiHostID), + authenticate: async apiHostID => + await this._authenticateInternalUser(apiHostID), request: async ( method: RequestHTTPMethod, path: RequestPath, @@ -158,7 +164,7 @@ export class ServerAPIClient { */ private async _authenticate( apiHostID: string, - authContext?: any, + options: ServerAPIAuthenticateOptions, ): Promise { const api: APIHost = await this.manageHosts.get(apiHostID); const optionsRequest = { @@ -171,16 +177,24 @@ export class ServerAPIClient { password: api.password, }, url: `${api.url}:${api.port}/security/user/authenticate${ - !!authContext ? '/run_as' : '' + options.useRunAs ? '/run_as' : '' }`, - ...(!!authContext ? { data: authContext } : {}), + ...(options?.authContext ? { data: options?.authContext } : {}), }; const response: AxiosResponse = await this._axios(optionsRequest); const token: string = (((response || {}).data || {}).data || {}).token; - if (!authContext) { - this._CacheInternalUserAPIHostToken.set(apiHostID, token); - } + return token; + } + + /** + * Get the authentication token for the internal user and cache it + * @param apiHostID Server API ID + * @returns + */ + private async _authenticateInternalUser(apiHostID: string): Promise { + const token = await this._authenticate(apiHostID, { useRunAs: false }); + this._CacheInternalUserAPIHostToken.set(apiHostID, token); return token; } @@ -192,13 +206,21 @@ export class ServerAPIClient { */ asScoped(context: any, request: any): ServerAPIScopedUserClient { return { - authenticate: async (apiHostID: string) => - await this._authenticate( - apiHostID, - ( - await this.dashboardSecurity.getCurrentUser(request, context) - ).authContext, - ), + authenticate: async (apiHostID: string) => { + const useRunAs = this.manageHosts.isEnabledAuthWithRunAs(apiHostID); + + const token = useRunAs + ? await this._authenticate(apiHostID, { + useRunAs: true, + authContext: ( + await this.dashboardSecurity.getCurrentUser(request, context) + ).authContext, + }) + : await this._authenticate(apiHostID, { + useRunAs: false, + }); + return token; + }, request: async ( method: RequestHTTPMethod, path: string, @@ -232,11 +254,13 @@ export class ServerAPIClient { this._CacheInternalUserAPIHostToken.has(options.apiHostID) && !options.forceRefresh ? this._CacheInternalUserAPIHostToken.get(options.apiHostID) - : await this._authenticate(options.apiHostID); + : await this._authenticateInternalUser(options.apiHostID); return await this._request(method, path, data, { ...options, token }); } catch (error) { if (error.response && error.response.status === 401) { - const token: string = await this._authenticate(options.apiHostID); + const token: string = await this._authenticateInternalUser( + options.apiHostID, + ); return await this._request(method, path, data, { ...options, token }); } throw error;