From 14602d15ad1e36b27e663d26f02c7b9087fe8de5 Mon Sep 17 00:00:00 2001 From: Cesare de Cal Date: Fri, 20 Mar 2026 17:39:00 +0100 Subject: [PATCH] [Osquery] Fix `profile_uid` dropped in `getUserInfo` authc fallback (#258866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm working on adding Scout API tests (https://github.com/elastic/kibana/pull/258534) and noticed that `created_by_profile_uid` and `updated_by_profile_uid` fields are absent from all Osquery API responses on ECH (Elastic Cloud Hosted), even though the authenticated user clearly has a `profile_uid` available. ## Test it yourself on ECH (dev console) Confirm the user has a `profile_uid`: ```bash GET kbn:/internal/security/me ``` This returns `{ "profile_uid": "u_..." }`. Now create a saved query and check the response keys: ```bash POST kbn:/api/osquery/saved_queries {"id":"profile-uid-test","query":"select 1;","interval":"3600"} ``` The `created_by_profile_uid` and `updated_by_profile_uid` fields are missing from the response on ECH. On local stateful they appear just fine. ## Hypothesis (LLM-assisted) `getUserInfo()` has two code paths for resolving user identity: 1. **Primary**: `userProfiles.getCurrent()` — returns `profile_uid` from the user profile service 2. **Fallback**: `authc.getCurrentUser()` — used when the primary fails or returns `null` The fallback hardcodes `profile_uid: null` instead of reading `user.profile_uid` from the `AuthenticatedUser` object (available since 2022, PR #141092). On ECH (Elastic Cloud Hosted), `userProfiles.getCurrent()` returns `null`, so the fallback is always used. The hardcoded `null` then cascades through route handlers: - Converted to `undefined` via `?? undefined` - Stripped by `JSON.stringify` (packs) or `pickBy` (saved queries) ## Why didn't we spot this sooner and why Scout comes to the rescue The existing FTR API tests [[1](https://github.com/elastic/kibana/blob/main/x-pack/platform/test/api_integration/apis/osquery/saved_queries.ts#L90-L126)] [[2](https://github.com/elastic/kibana/blob/main/x-pack/platform/test/api_integration/apis/osquery/packs.ts#L191-L251)] covering this ground aren't run on ECH. Scout is designed to be [deployment-agnostic](https://www.elastic.co/docs/extend/kibana/scout/best-practices#design-tests-with-a-cloud-first-mindset), so we're easily able to run the same set of tests on different testing surfaces :-) (cherry picked from commit fe7e2477ddd0d89e13e5a8ff594a22eb4b8e2d5c) # Conflicts: # x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.test.ts --- .../osquery/server/lib/get_user_info.test.ts | 97 +++++++++++++++++++ .../osquery/server/lib/get_user_info.ts | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.test.ts diff --git a/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.test.ts b/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.test.ts new file mode 100644 index 0000000000000..1f1a8ee483dea --- /dev/null +++ b/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; +import { getUserInfo } from './get_user_info'; + +describe('getUserInfo', () => { + const logger = loggingSystemMock.createLogger(); + const request = httpServerMock.createKibanaRequest(); + + it('returns undefined when security is unavailable', async () => { + const result = await getUserInfo({ request, logger }); + + expect(result).toBeUndefined(); + }); + + it('returns user profile info when available', async () => { + const security = { + userProfiles: { + getCurrent: jest.fn().mockResolvedValue({ + uid: 'profile-1', + user: { + username: 'user-name', + full_name: 'Full Name', + email: 'user@example.com', + }, + }), + }, + authc: { + getCurrentUser: jest.fn(), + }, + } as unknown as SecurityPluginStart; + + const result = await getUserInfo({ request, security, logger }); + + expect(result).toEqual({ + username: 'Full Name', + full_name: 'Full Name', + email: 'user@example.com', + profile_uid: 'profile-1', + }); + }); + + it('falls back to authc when user profile lookup fails', async () => { + const security = { + userProfiles: { + getCurrent: jest.fn().mockRejectedValue(new Error('failed')), + }, + authc: { + getCurrentUser: jest.fn().mockReturnValue({ + username: 'fallback-user', + full_name: null, + email: 'fallback@example.com', + }), + }, + } as unknown as SecurityPluginStart; + + const result = await getUserInfo({ request, security, logger }); + + expect(result).toEqual({ + username: 'fallback@example.com', + full_name: null, + email: 'fallback@example.com', + profile_uid: null, + }); + }); + + it('uses profile_uid from authc fallback when available', async () => { + const security = { + userProfiles: { + getCurrent: jest.fn().mockResolvedValue(null), + }, + authc: { + getCurrentUser: jest.fn().mockReturnValue({ + username: 'cloud-user', + full_name: null, + email: 'cloud-user@example.com', + profile_uid: 'u_cloud_profile_123', + }), + }, + } as unknown as SecurityPluginStart; + + const result = await getUserInfo({ request, security, logger }); + + expect(result).toEqual({ + username: 'cloud-user@example.com', + full_name: null, + email: 'cloud-user@example.com', + profile_uid: 'u_cloud_profile_123', + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.ts b/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.ts index 890094057a168..75b432705b214 100644 --- a/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.ts +++ b/x-pack/platform/plugins/shared/osquery/server/lib/get_user_info.ts @@ -58,7 +58,7 @@ export const getUserInfo = async ({ username: displayName, full_name: user.full_name ?? null, email: user.email ?? null, - profile_uid: null, + profile_uid: user.profile_uid ?? null, }; } } catch (error) {