From 96b7c97e52f3475de29d111605df7b14c515dfbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 17 Sep 2024 09:09:55 +0200 Subject: [PATCH 1/3] fix: a problem upgrading the API host registry data through /api/check-stored-api endpoint The allow_run_as missing data in the API host registry could cause the authentication used the internal user instead of the context of logger user when run_as was enabled. --- plugins/main/server/controllers/wazuh-api.ts | 105 ++++-------------- .../routes/wazuh-api-http-status.test.ts | 1 + .../common/api-user-status-run-as.ts | 21 +--- .../server/services/manage-hosts.ts | 72 +++++++++--- 4 files changed, 81 insertions(+), 118 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index af9eafc8aa..32bcf15f68 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -162,94 +162,31 @@ export class WazuhApiCtrl { } // If we have a valid response from the Wazuh API - if ( - responseManagerInfo.status === HTTP_STATUS_CODES.OK && - responseManagerInfo.data - ) { - // Clear and update cluster information before being sent back to frontend - delete api.cluster_info; - const responseAgents = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - `/agents`, - { params: { agents_list: '000' } }, - { apiHostID: id }, + try { + const { status, manager, node, cluster } = + await context.wazuh_core.manageHosts.getRegistryDataByHost( + apiHostData, + { + throwError: true, + }, ); - if (responseAgents.status === HTTP_STATUS_CODES.OK) { - const managerName = - responseAgents.data.data.affected_items[0].manager; - - const responseClusterStatus = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - `/cluster/status`, - {}, - { apiHostID: id }, - ); - if (responseClusterStatus.status === HTTP_STATUS_CODES.OK) { - if (responseClusterStatus.data.data.enabled === 'yes') { - const responseClusterLocalInfo = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - `/cluster/local/info`, - {}, - { apiHostID: id }, - ); - if (responseClusterLocalInfo.status === HTTP_STATUS_CODES.OK) { - const clusterEnabled = - responseClusterStatus.data.data.enabled === 'yes'; - api.cluster_info = { - status: clusterEnabled ? 'enabled' : 'disabled', - manager: managerName, - node: responseClusterLocalInfo.data.data.affected_items[0] - .node, - cluster: clusterEnabled - ? responseClusterLocalInfo.data.data.affected_items[0] - .cluster - : 'Disabled', - }; - } - } else { - // Cluster mode is not active - api.cluster_info = { - status: 'disabled', - manager: managerName, - cluster: 'Disabled', - }; - } - } else { - // Cluster mode is not active - api.cluster_info = { - status: 'disabled', - manager: managerName, - cluster: 'Disabled', - }; - } - - if (api.cluster_info) { - // Update cluster information in the wazuh-registry.json - await context.wazuh_core.manageHosts.updateRegistryByHost( - id, - api.cluster_info, - ); + api.cluster_info = { status, manager, node, cluster }; - return response.ok({ - body: { - statusCode: HTTP_STATUS_CODES.OK, - data: api, - idChanged: request.body.idChanged || null, - }, - }); - } - } + return response.ok({ + body: { + statusCode: HTTP_STATUS_CODES.OK, + data: api, + idChanged: request.body.idChanged || null, + }, + }); + } catch (error) { + // If we have an invalid response from the Wazuh API + throw new Error( + responseManagerInfo.data.detail || + `${api.url}:${api.port} is unreachable`, + ); } - - // If we have an invalid response from the Wazuh API - throw new Error( - responseManagerInfo.data.detail || - `${api.url}:${api.port} is unreachable`, - ); } catch (error) { if (error.code === 'EPROTO') { return response.ok({ diff --git a/plugins/main/server/routes/wazuh-api-http-status.test.ts b/plugins/main/server/routes/wazuh-api-http-status.test.ts index 4503f41615..977cf2c43a 100644 --- a/plugins/main/server/routes/wazuh-api-http-status.test.ts +++ b/plugins/main/server/routes/wazuh-api-http-status.test.ts @@ -37,6 +37,7 @@ const context = { cacheAPIUserAllowRunAs: { set: jest.fn(), API_USER_STATUS_RUN_AS: { + UNABLE_TO_CHECK: -1, ALL_DISABLED: 0, USER_NOT_ALLOWED: 1, HOST_DISABLED: 2, diff --git a/plugins/wazuh-core/common/api-user-status-run-as.ts b/plugins/wazuh-core/common/api-user-status-run-as.ts index b6da7080df..6d1de83a6c 100644 --- a/plugins/wazuh-core/common/api-user-status-run-as.ts +++ b/plugins/wazuh-core/common/api-user-status-run-as.ts @@ -1,23 +1,8 @@ -/** - * @example - * HOST = set in configuration - * USER = set in user interface - * - * ALL_DISABLED - * binary 00 = decimal 0 ---> USER 0 y HOST 0 - * - * USER_NOT_ALLOWED - * binary 01 = decimal 1 ---> USER 0 y HOST 1 - * - * HOST_DISABLED - * binary 10 = decimal 2 ---> USER 1 y HOST 0 - * - * ENABLED - * binary 11 = decimal 3 ---> USER 1 y HOST 1 - */ export enum API_USER_STATUS_RUN_AS { + UNABLE_TO_CHECK = -1 /* Initial value or could not check the user can + use the run_as */, ALL_DISABLED = 0, // Wazuh HOST and USER API user configured with run_as=false or undefined USER_NOT_ALLOWED = 1, // Wazuh HOST API user configured with run_as=true in configuration but it has not run_as in Wazuh API - HOST_DISABLED = 2, // Wazuh HOST API user configured with run_as=false in configuration but it has not run_as in Wazuh API + HOST_DISABLED = 2, // Wazuh HOST API user configured with run_as=false in configuration but it has run_as in Wazuh API ENABLED = 3, // Wazuh API user configured with run_as=true and allow run_as } diff --git a/plugins/wazuh-core/server/services/manage-hosts.ts b/plugins/wazuh-core/server/services/manage-hosts.ts index a1db94593c..593c4076f1 100644 --- a/plugins/wazuh-core/server/services/manage-hosts.ts +++ b/plugins/wazuh-core/server/services/manage-hosts.ts @@ -31,6 +31,13 @@ interface IAPIHostRegistry { allow_run_as: API_USER_STATUS_RUN_AS; } +interface GetRegistryDataByHostOptions { + /* this option lets to throw the error when trying to fetch the required data + of the API host that is used by the checkStoredAPI of /api/check-stored-api + endpoint */ + throwError: boolean; +} + /** * This service manages the API connections. * Get API hosts configuration @@ -141,6 +148,7 @@ export class ManageHosts { */ private async getRegistryDataByHost( host: IAPIHost, + options: GetRegistryDataByHostOptions, ): Promise { const apiHostID = host.id; this.logger.debug(`Getting registry data from host [${apiHostID}]`); @@ -150,7 +158,7 @@ export class ManageHosts { node = null, status = 'disabled', cluster = 'Disabled', - allow_run_as = API_USER_STATUS_RUN_AS.ALL_DISABLED; + allow_run_as = API_USER_STATUS_RUN_AS.UNABLE_TO_CHECK; try { const responseAgents = await this.serverAPIClient.asInternalUser.request( @@ -165,21 +173,24 @@ export class ManageHosts { } // Get allow_run_as - if (!host.run_as) { - allow_run_as = API_USER_STATUS_RUN_AS.HOST_DISABLED; - } else { - const responseAllowRunAs = - await this.serverAPIClient.asInternalUser.request( - 'GET', - '/security/users/me', - {}, - { apiHostID }, - ); - if (this.isServerAPIClientResponseOk(responseAllowRunAs)) { - allow_run_as = responseAllowRunAs.data.data.affected_items[0] - .allow_run_as + const responseAllowRunAs = + await this.serverAPIClient.asInternalUser.request( + 'GET', + '/security/users/me', + {}, + { apiHostID }, + ); + if (this.isServerAPIClientResponseOk(responseAllowRunAs)) { + const allow_run_as_response = + responseAllowRunAs.data.data.affected_items[0].allow_run_as; + if (host.run_as) { + allow_run_as = allow_run_as_response ? API_USER_STATUS_RUN_AS.ENABLED : API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED; + } else { + allow_run_as = allow_run_as_response + ? API_USER_STATUS_RUN_AS.HOST_DISABLED + : API_USER_STATUS_RUN_AS.ALL_DISABLED; } } @@ -191,7 +202,10 @@ export class ManageHosts { { apiHostID }, ); - if (this.isServerAPIClientResponseOk(responseClusterStatus) && responseClusterStatus.data?.data?.enabled === 'yes') { + if ( + this.isServerAPIClientResponseOk(responseClusterStatus) && + responseClusterStatus.data?.data?.enabled === 'yes' + ) { status = 'enabled'; const responseClusterLocal = @@ -207,7 +221,11 @@ export class ManageHosts { cluster = responseClusterLocal.data.data.affected_items[0].cluster; } } - } catch (error) {} + } catch (error) { + if (options?.throwError) { + throw error; + } + } const data = { manager, @@ -283,11 +301,33 @@ export class ManageHosts { `API host with ID [${apiId}] was not found in the registry. This could be caused by a problem getting and storing the registry data or the API host was removed.`, ); } + if (registryHost.allow_run_as === API_USER_STATUS_RUN_AS.UNABLE_TO_CHECK) { + throw new Error( + `API host with host ID [${apiId}] could not check the ability to use the run as. Ensure the API host is accesible and the internal user has the minimal permissions to check this capability.`, + ); + } if (registryHost.allow_run_as === API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED) { throw new Error( `API host with host ID [${apiId}] misconfigured. The configurated API user is not allowed to use [run_as]. Allow it in the API user configuration or set [run_as] host setting with [false] value.`, ); } + + /* The allowed values to compare should be: + API_USER_STATUS_RUN_AS.ENABLED: use run_as + API_USER_STATUS_RUN_AS.HOST_DISABLED: not use run_as + API_USER_STATUS_RUN_AS.ALL_DISABLED: not use run_as + */ + if ( + ![ + API_USER_STATUS_RUN_AS.ENABLED, + API_USER_STATUS_RUN_AS.HOST_DISABLED, + API_USER_STATUS_RUN_AS.ALL_DISABLED, + ].includes(registryHost.allow_run_as) + ) { + throw new Error( + `API host with host ID [${apiId}] has an unexpected value [${registryHost.allow_run_as}] stored in the registry. This could be caused by a problem getting and storing the registry data.`, + ); + } return registryHost.allow_run_as === API_USER_STATUS_RUN_AS.ENABLED; } } From 2c38650154852b97a8f13d0dd4af6ee24ada08a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 17 Sep 2024 13:27:23 +0200 Subject: [PATCH 2/3] chore(changelog): add entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 502279b34b..e459d1985b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added -- Add feature to filter by field in the events table rows [#6977](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6991) - Support for Wazuh 4.9.1 +- Add feature to filter by field in the events table rows [#6977](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6991) ### Fixed @@ -19,6 +19,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed missing link to Vulnerabilities detection and Office 365 in the agent menu of `Endpoints Summary` [#6983](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6983) - Fixed missing options depending on agent operating system in the agent configuration report [#6983](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6983) - Fixed an style that affected the Discover plugin [#6989](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6989) +- Fixed a problem updating the API host registry in the GET /api/check-stored-api [#6995](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6995) ### Changed From 0f275142ae55580da4a6a328c11a3831e1aface9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 17 Sep 2024 14:58:16 +0200 Subject: [PATCH 3/3] fix: ensure the user authentication uses the related endpoint according to the configuration of run_as - Ensure the user authentication uses the related endpoint according to the configuration of run_as Move the logic to decide the authentication (user or not run_as) to asCurrentUser.authenticate - Fix when the `run_as: false` for a server API host, any login of an user caused the internal user token was replaced by the obtained for the logged user. --- plugins/main/server/controllers/wazuh-api.ts | 13 +--- .../server/services/manage-hosts.ts | 1 + .../server/services/server-api-client.ts | 62 +++++++++++++------ 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 32bcf15f68..545e989e85 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -74,16 +74,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/server/services/manage-hosts.ts b/plugins/wazuh-core/server/services/manage-hosts.ts index 593c4076f1..84165d95e2 100644 --- a/plugins/wazuh-core/server/services/manage-hosts.ts +++ b/plugins/wazuh-core/server/services/manage-hosts.ts @@ -17,6 +17,7 @@ import { HTTP_STATUS_CODES } from '../../common/constants'; interface IAPIHost { id: string; + url: string; username: string; password: string; port: number; diff --git a/plugins/wazuh-core/server/services/server-api-client.ts b/plugins/wazuh-core/server/services/server-api-client.ts index be9622b642..0c08568405 100644 --- a/plugins/wazuh-core/server/services/server-api-client.ts +++ b/plugins/wazuh-core/server/services/server-api-client.ts @@ -18,10 +18,12 @@ import { ManageHosts } from './manage-hosts'; import { ISecurityFactory } from './security-factory'; interface APIHost { + id: string; url: string; - port: string; username: string; password: string; + port: number; + run_as: boolean; } type RequestHTTPMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; @@ -64,6 +66,11 @@ export interface ServerAPIScopedUserClient { ) => Promise>; } +export interface ServerAPIAuthenticateOptions { + useRunAs: boolean; + authContext?: any; +} + /** * This service communicates with the Wazuh server APIs */ @@ -86,7 +93,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,9 +166,9 @@ export class ServerAPIClient { */ private async _authenticate( apiHostID: string, - authContext?: any, + options: ServerAPIAuthenticateOptions, ): Promise { - const api: APIHost = await this.manageHosts.get(apiHostID); + const api = (await this.manageHosts.get(apiHostID)) as APIHost; const optionsRequest = { method: 'POST', headers: { @@ -171,16 +179,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 +208,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 +256,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;