Skip to content

[Security Solution] Render user profile avatars on notes#280902

Open
orangecola wants to merge 3 commits into
elastic:mainfrom
orangecola:claude/notes-profile-images-b430ff
Open

[Security Solution] Render user profile avatars on notes#280902
orangecola wants to merge 3 commits into
elastic:mainfrom
orangecola:claude/notes-profile-images-b430ff

Conversation

@orangecola

@orangecola orangecola commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Addresses #280901.

Note author avatars in the Security Solution were rendered with a bare <EuiAvatar name={displayName} />, so they showed auto-generated initials in an auto-assigned color and ignored the avatar the user had configured in their Kibana user profile. The same person appeared with their real profile image in Cases and the user menu, but with generated initials on their notes.

This PR adds a shared NoteAvatar component that resolves the note's stored display name back to a user profile and renders UserAvatar from @kbn/user-profile-components (image if set, otherwise the profile's own initials and color), falling back to the previous generated avatar when no profile matches — deleted users, imported notes, unauthenticated writes.

Call sites switched over:

Surface File
Notes list (flyout + timeline Notes tab) public/notes/components/notes_list.tsx
Note management page, "Created by" column public/notes/pages/note_management_page.tsx
Timeline participants public/timelines/components/notes/participants.tsx
Note previews (open timeline) public/timelines/components/open_timeline/note_previews/index.tsx
Timeline description comment public/timelines/components/timeline/tabs/notes/index.tsx

Why matching is done on the display name

Notes do not persist a user profile identifier. The server stores only a display string:

// server/lib/timeline/saved_object/notes/saved_object.ts
savedNote.createdBy = userInfo ? getUserDisplayName(userInfo) : UNAUTHENTICATED_USER;
savedNote.updatedBy = userInfo ? getUserDisplayName(userInfo) : UNAUTHENTICATED_USER;

With no uid on the note, the client has nothing to look up by, so NoteAvatar matches getUserDisplayName(profile.user) === note.updatedBy against the profiles returned by the existing useSuggestUsers hook.

Second commit: raising the suggestion cap

While verifying the first commit against a real stack I found it only worked for very small deployments. useSuggestUsers({ searchTerm: '' }) calls GET /internal/detection_engine/users/_find, and that route called security.userProfiles.suggest() without a size — which defaults to DEFAULT_SUGGESTIONS_COUNT = 10. Measured on a 9.4.4 stack with two users holding profile images and three notes between them:

Activated user profiles Profiles returned by suggestUsers('') Notes resolving to a profile avatar
2 2 3 / 3
16 10 1 / 3

At 16 users the author of two of the three notes fell outside the 10 returned profiles and kept the buggy fallback. The second commit passes an explicit size: 100 (MAX_SUGGESTIONS_COUNT, the ceiling the security plugin enforces — above it suggest throws).

Reviewers please note this is a mitigation, not a cure, and it has two consequences worth weighing:

  • The cliff moves from 10 users to 100; deployments above that still show fallback avatars for some authors, and which authors is effectively arbitrary since the suggest ordering is neither stable nor authorship-aware.
  • /internal/detection_engine/users/_find is shared with alert assignee suggestions. Because the route passes requiredPrivileges, the security plugin fetches max(size * 2, …) profiles and then privilege-filters them, so this raises that request from 20 to 200 profiles per call. The result is cached client-side with staleTime: Infinity, but it is a real increase per fresh fetch.

Two further limits are inherent to name matching and not fixed here: users sharing a display name are indistinguishable, and because the note stores whatever getUserDisplayName returned at write time, later changing a user's full_name silently breaks the match for their existing notes.

The durable fix is to persist the author's profile uid on the note saved object and resolve it with bulkGetUserProfiles, keeping name matching only as a fallback for notes written before that change. That needs a saved-object model version and server-side work, so it is left for a follow-up — happy to take direction on whether this should wait for it.

Third commit: fixing two assertions that could never pass

The NoteAvatar and NotesList tests added in the first commit both asserted the profile image like this:

expect(avatar.querySelector('img')).toHaveAttribute('src', 'my-image-url');

EuiAvatar renders imageUrl as a CSS background-image and never emits an <img> element, so querySelector('img') is always null and both tests fail. They now assert on the background image, matching how kbn-user-profile-components tests the same component. The component code was correct — only the assertions were wrong.

Testing

  • Added unit tests for NoteAvatar: profile image, profile initials + color, fallback when no profile matches, and ? when the display name is missing.
  • Added a route test asserting the explicit size is passed to userProfiles.suggest.

Uploading image.png…

Run locally against this branch:

Suite Result
public/notes (incl. note_avatar, notes_list) 13 suites, 113 tests passed
public/timelines (participants, note previews, notes tab) 8 suites, 43 tests passed
suggest_user_profiles_route passed
type_check --project .../security_solution tsc exited 0

End-to-end verification: built Kibana 9.6.0 from this branch against a version-matched ES 9.6.0-SNAPSHOT on a trial license, with two users holding uploaded profile images and three notes between them. The notes management page "Created by" column, the timeline Notes tab, and the "Created by" / "Participants" panels all render the real profile images; on 9.4.4 the same scenario renders generated letter avatars (E, SC). Screenshots below.

Checklist

  • Unit tests added for the new component and the route change
  • Unit tests and type check run locally
  • Verified end-to-end in a running Kibana built from this branch
  • Verified in CI

…stions

`userProfiles.suggest` defaults to 10 results when no size is given, which is
too few for consumers that match a stored display name against the full set of
profiles rather than searching interactively - note avatars being the case that
surfaced this. Request 100, the maximum the security plugin accepts.
`EuiAvatar` renders `imageUrl` as a CSS background-image rather than an `<img>`
element, so both profile-image assertions were querying for an element that is
never rendered and failing. Assert on the background image instead, matching
how kbn-user-profile-components tests the same component.
@orangecola
orangecola marked this pull request as ready for review July 25, 2026 14:19
@orangecola
orangecola requested review from a team as code owners July 25, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants