Skip to content

[Security Assistant] Conversation sharing#230614

Merged
stephmilovic merged 153 commits intoelastic:mainfrom
stephmilovic:assistant_sharing
Aug 28, 2025
Merged

[Security Assistant] Conversation sharing#230614
stephmilovic merged 153 commits intoelastic:mainfrom
stephmilovic:assistant_sharing

Conversation

@stephmilovic
Copy link
Copy Markdown
Contributor

@stephmilovic stephmilovic commented Aug 5, 2025

Summary

Adds conversation sharing functionality to the Security Assistant, allowing users to share conversations with team members. The implementation includes ownership tracking, sharing controls, and appropriate access restrictions for shared conversations.

  • Adds user profile system for sharing conversations with specific team members
    • Includes new suggest internal endpoint to search users as required by user profile service
  • Implements conversation ownership tracking and access controls
  • Provides UI components for managing conversation visibility and permissions

API changes

New fields on conversation

  • createdBy is now a required field on conversations.
  • messages[0].user is now included when the message has role === 'user'.
  • The users array can now be updated when modifying a conversation:
    • An empty users array indicates a shared conversation.
    • A users array with one entry indicates a private conversation.
    • A users array with multiple entries indicates a restricted conversation.

Note on legacy conversations:
Older conversations do not include the new createdBy field. Instead, they follow the previous convention where the users array contains a single entry for the conversation owner.

There is no migration step. Instead, when a legacy conversation is updated:

  • The createdBy field is backfilled using the value from users[0].
  • Each role=user message is assigned a user value during the update.
    This logic is applied in the painless update script.

Because of this, permission checks are required on both the client and server:

  • Two helper methods, getCurrentConversationOwner and getIsConversationOwner, handle owner resolution by checking createdBy first, then falling back to users[0].
  • On the server, getIsConversationOwner prevents users from updating conversations they do not own.
  • On the client, getCurrentConversationOwner is used to determine the user avatar when a role=user message does not already specify a user.

Find conversation route updates

The find conversation route now supports a new query parameter, is_owner (default: false).

  • When is_owner: false
    Returns private conversations the user owns, restricted conversations shared with them, and globally shared conversations:
    users:{ name: "elastic" OR id: "123" } OR (NOT users: { name: * } and NOT users: { id: * })
  • When is_owner: true
    Returns only conversations owned by the user::
    (created_by.id : "123" OR created_by.name : "elastic") OR (NOT created_by:* AND users:{ name: "elastic" OR id: "123" })

Documents data writer

The getFilterByUser query was updated to restrict updates to conversations unless owned by the current user, consistent with the owned-conversation KQL.

Telemetry

The following server telemetry event types have been added:

  • conversation_shared_success
  • conversation_shared_error
  • shared_conversation_accessed - when a non-owner accesses a conversation
  • conversation_duplicated

Audit logging

The following audit logging been added:

  • security_assistant_conversation_shared
   {
  "event": {
    "action": "security_assistant_conversation_shared",
    "category": [
      "database"
    ],
    "type": [
      "change"
    ],
    "outcome": "success"
  },
  "user": {
    "id": "u_xSVO6jcSCvoEcle7e3XVVfBU4Swm1R8-x7pi5bxrSvU_0",
    "name": "test_daija_glover",
    "roles": [
      "superuser"
    ]
  },
  "kibana": {
    "space_id": "default",
    "session_id": "1AZ8kfSYHzVO5ZMZ97DrNi1wjN6BFKHTw75KH8WiF7w="
  },
  "trace": {
    "id": "7e080b32-41b4-453b-80fe-b9c1e12a1c57"
  },
  "client": {
    "ip": "127.0.0.1"
  },
  "http": {
    "request": {
      "headers": {
        "x-forwarded-for": "127.0.0.1"
      }
    }
  },
  "service": {
    "node": {
      "roles": [
        "background_tasks",
        "ui"
      ]
    }
  },
  "ecs": {
    "version": "9.0.0"
  },
  "@timestamp": "2025-08-26T13:16:10.422-06:00",
  "message": "User has shared conversation [id=b873b917-2fd0-4452-98e8-8c359f6acbfa, title=\"Getting Started with Elastic Security\"] to all users in the space",
  "log": {
    "level": "INFO",
    "logger": "plugins.security.audit.ecs"
  },
  "process": {
    "pid": 61536,
    "uptime": 65.705743792
  },
  "span": {
    "id": "8364fa9bf07311d6"
  }
}
  • security_assistant_conversation_private
{
  "event": {
    "action": "security_assistant_conversation_private",
    "category": [
      "database"
    ],
    "type": [
      "change"
    ],
    "outcome": "success"
  },
  "user": {
    "id": "u_xSVO6jcSCvoEcle7e3XVVfBU4Swm1R8-x7pi5bxrSvU_0",
    "name": "test_daija_glover",
    "roles": [
      "superuser"
    ]
  },
  "kibana": {
    "space_id": "default",
    "session_id": "1AZ8kfSYHzVO5ZMZ97DrNi1wjN6BFKHTw75KH8WiF7w="
  },
  "trace": {
    "id": "ae998403-8453-44ae-a9b8-ac8002c3bf28"
  },
  "client": {
    "ip": "127.0.0.1"
  },
  "http": {
    "request": {
      "headers": {
        "x-forwarded-for": "127.0.0.1"
      }
    }
  },
  "service": {
    "node": {
      "roles": [
        "background_tasks",
        "ui"
      ]
    }
  },
  "ecs": {
    "version": "9.0.0"
  },
  "@timestamp": "2025-08-26T13:15:46.300-06:00",
  "message": "User has made private conversation [id=b873b917-2fd0-4452-98e8-8c359f6acbfa, title=\"Getting Started with Elastic Security\"]",
  "log": {
    "level": "INFO",
    "logger": "plugins.security.audit.ecs"
  },
  "process": {
    "pid": 61536,
    "uptime": 41.582780958
  },
  "span": {
    "id": "68a0d5f52faa17d4"
  }
}
  • security_assistant_conversation_restricted
{
  "event": {
    "action": "security_assistant_conversation_restricted",
    "category": [
      "database"
    ],
    "type": [
      "change"
    ],
    "outcome": "success"
  },
  "user": {
    "id": "u_xSVO6jcSCvoEcle7e3XVVfBU4Swm1R8-x7pi5bxrSvU_0",
    "name": "test_daija_glover",
    "roles": [
      "superuser"
    ]
  },
  "kibana": {
    "space_id": "default",
    "session_id": "1AZ8kfSYHzVO5ZMZ97DrNi1wjN6BFKHTw75KH8WiF7w="
  },
  "trace": {
    "id": "b59f9790-87ff-45f0-b28e-1d9ffa6cfb09"
  },
  "client": {
    "ip": "127.0.0.1"
  },
  "http": {
    "request": {
      "headers": {
        "x-forwarded-for": "127.0.0.1"
      }
    }
  },
  "service": {
    "node": {
      "roles": [
        "background_tasks",
        "ui"
      ]
    }
  },
  "ecs": {
    "version": "9.0.0"
  },
  "@timestamp": "2025-08-26T14:40:59.897-06:00",
  "message": "User has restricted conversation [id=b873b917-2fd0-4452-98e8-8c359f6acbfa, title=\"Getting Started with Elastic Security\"] to user ([id=u_LdnmWaOWbWS1ObwqRW2MLWMkWtxCSyiElishzEpew0g_0, name=test_dina_bahringer])",
  "log": {
    "level": "INFO",
    "logger": "plugins.security.audit.ecs"
  },
  "process": {
    "pid": 77921,
    "uptime": 29.727069625
  },
  "span": {
    "id": "80e57252aceea924"
  }
}

UI Changes

Share badge

Screenshot 2025-08-26 at 3 10 11 PM

When the user interacts with the new Share badge, there will be 3 selections: Private, Shared, and Restricted. When the conversation owner changes to Private or Shared, a success toast should appear. If a conversation owner changes to Restricted, a Share Modal opens and allows the user to select specific team members to share the conversation with.

Share modal

Screenshot 2025-08-20 at 3 11 21 PM

Share icons

When Shared is selected, a users icon should appear on the share badge and in the Conversation list item. When Restricted is selected, the Share Icon will be readOnly. The private conversation icon is a lock. If the conversation was owner shared, the Share Icon in the Conversation list item will be pink (changing before release). If the conversation was shared with the user, the Share Icon will be text color.
Screenshot 2025-08-28 at 2 33 36 PM
Screenshot 2025-08-28 at 2 30 47 PM
Screenshot 2025-08-28 at 2 31 21 PM

Owner shared callout

When a conversation has been shared and the current user is the owner, they will see the Owner shared callout. The callout informs the user that the conversation is shared and any further edits/messages will also be shared. It can be dismissed per conversation, the dismissal status is stored in local storage. The Owner shared callout will also have the appropriate Share Icon.
Screenshot 2025-08-26 at 3 14 01 PM

Conversation list items

We already covered that the Conversation list items now have a Share icon indicating the conversation's shared status. The Conversation list items were also updated to include a context menu, with options to Copy URL, Duplicate, and if conversation owner, Delete. We'll cover the new actions in a bit.
Screenshot 2025-08-28 at 2 33 03 PM
Screenshot 2025-08-28 at 2 32 53 PM

Conversation settings menu

The assistant's context settings menu has split into two menus, a Conversation settings menu and an Assistant settings menu. As their titles indicate, the category of actions depends on which menu. The assistant setting actions will be the same regardless of ownership. The conversation settings actions will depend on conversation ownership, much like the conversation list items.
Screenshot 2025-08-20 at 3 18 18 PM
Screenshot 2025-08-20 at 3 18 23 PM
Screenshot 2025-08-20 at 3 20 52 PM

Responsive assistant header

Due to the addition of the Share Badge and the Conversation Settings Menu, I needed to make adjustments for the header to be responsive.

Screen.Recording.2025-08-26.at.3.19.01.PM.mov

Shared Conversation Callout

When a conversation has been shared with you, you will be unable to make edits to the conversation or send any new messages in the conversation. The original owner could continue the conversation and you will see their new messages appear. The Shared Conversation Callout informs the user of the functionality, and suggests they duplicate the conversation (see below) if they want to continue. Like the Owner shared callout, this callout can also be dismissed and the dismissal state is tracked per conversation id in local storage.
Screenshot 2025-08-28 at 2 36 50 PM

Copy URL action

As mentioned earlier, we now support a Copy URL action. This is currently the only way to inform a user of a message shared with them. The message owner must send them a URL to open it directly, or they may discover it themselves in their conversation list. The url copied always links to the getting started page to ensure there are not access issue. However, any url within security that has the query parameter ?assistant=12345 will open the assistant with the conversation selected. If you do not have access to the conversation, the assistant will open to a new conversation just as if you had tried to open a deleted conversation from local storage. We can iterate later on some conversation not shared with you messaging. There are Copy URL buttons in the Share Modal, Conversation List Items, and Conversation Settings Menu.

Duplicate conversation action

Any conversation can now be duplicated from the Shared Conversation Callout, Conversation List Items, and Conversation Settings Menu. It will be titled [Duplicate] Original title and the user will become the owner of this new conversation. The usefulness of this feature comes when someone has shared a conversation with you and you can duplicate the conversation to continue the conversation.

Screen.Recording.2025-08-26.at.3.20.12.PM.mov

Conversation settings

The final area with UI changes is the Conversation settings page. This page only shows user owned conversation. A new column has been added to the table with the share badge. Additionally, the edit flyout has a Share select where the conversation share state can be updated.
Screenshot 2025-08-28 at 2 45 07 PM
Screenshot 2025-08-28 at 2 45 02 PM

Testing Instructions

The sharing feature is behind a feature flag. Enable it by adding:

feature_flags.overrides.elasticAssistant.assistantSharingEnabled: true

In kibana.dev.yml, enable audit logging and telemetry for testing:

xpack.security.audit.enabled: true
telemetry.optIn: true

Setup

  1. Start from main with a fresh instance of Elasticsearch and Kibana.
  2. Start Elasticsearch with the audit logging tag: -E xpack.security.audit.enabled
  3. Create test users by running the create_and_login_users.js script:
    node create_and_login_users.js 10
    • This script is checked into the branch for testing. After use, stash it.
    • Starting on main ensures the created_at mapping does not exist yet.
  4. Log in with two of these users (User A and User B) and create several conversations in the Assistant UI.
  5. Check out this branch.

Verification Steps

User isolation

  1. Log in as User A. Confirm that only User A’s conversations appear.
    Repeat with User B.

Sharing from User A

  1. While logged in as User A:

    • Share one conversation with User B
    • Share one conversation globally
    • Create 3 new conversations
      • Share one with User B
      • Share one globally
  2. Verify with User A:

    • The Owner Shared Conversation Callout appears on shared conversations.
    • Dismiss one callout, navigate away, and return. Ensure the callout remains dismissed.

Viewing as User B

  1. Log in as User B. Confirm that all 4 conversations shared by User A are visible.
  2. Open one shared conversation:
  • The Shared Conversation Callout is shown with a Duplicate button.
  • Continuing the conversation is not possible.
  • Messages are attributed to User A.
  • Editing, deleting, or resetting is not possible.
  1. Duplicate the conversation:

    • The duplicate is editable and continuable.
    • Original messages remain attributed to User A.
      New messages are attributed to User B.
  2. Dismiss the Shared Conversation Callout on a shared conversation, switch to another, then return. Confirm the dismissal persists.

Conversation list

  1. Open the conversation list in the Assistant sidebar:

    • Shared conversations display the correct icons.
    • Context menu for a shared conversation includes Copy URL and Duplicate (not Delete).
    • Context menu for an owned conversation includes Delete.
  2. Share User B’s conversations globally and with specific users. Confirm icons display correctly.

Copy URL

  1. Use any Copy URL button. Paste into a new browser window. Ensure the correct conversation opens.

Conversation menus

  1. Start a new conversation. In the Create system prompt menu, confirm that only User B’s own conversations appear (shared ones should not).
  2. Open AI Assistant settings:
    • Only User B’s conversations appear (no shared ones).
    • Shared icon appears next to titles where appropriate.
    • Editing a conversation’s shared state updates only on save (not on cancel).

Role restrictions

  1. Create a role that restricts Assistant access. Assign a user (Bad User) to it.
    From User A or User B, try assigning a conversation to Bad User.
    • Bad User should not appear in search results.

Telemetry

  1. After several hours, the telemetry should appear on the server. Search in discover for the new telemetry events: https://telemetry-v2-staging.elastic.dev/s/securitysolution/app/r/s/iUKaC

Audit logging

  1. If you started with the audit log enabled, you should have access right away. Search the logs/ directory in root for the audit log actions (security_assistant_conversation_shared,security_assistant_conversation_restricted, and security_assistant_conversation_private)

@KDKHD
Copy link
Copy Markdown
Member

KDKHD commented Aug 27, 2025

Still need to test Telemetry and Audit logging.

/**
* The full name of the user.
*/
full_name: z.string().optional(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not particularly related to your changes, but I see we are mixing camel and snake case styles in assistant APIs. Wondering whether we should try to stick to one of them.

createdBy: { id: 'userX', name: 'Alice' },
users: [user, otherUser],
};
expect(getIsConversationOwner(conversation, user)).toBe(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double check, is the name same as a username?

import type { ConversationResponse, User } from '../schemas';

export const getCurrentConversationOwner = (
conversation?: Pick<ConversationResponse, 'createdBy' | 'users'>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to make conversation optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its optional in the assistant, so it may be undefined if passed to this function

*/
export const getIsConversationOwner = (
// keep type loose for use in both assistant package and security plugins
conversation: Pick<ConversationResponse, 'createdBy' | 'users' | 'id'> | undefined,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, is there a reason to allow undefined conversation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its optional in the assistant, so it may be undefined if passed to this function

id: user?.uid ?? '',
name: user?.user?.username ?? '',
})),
// readd current user
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// readd current user
// read current user

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-add, not read

Copy link
Copy Markdown
Contributor

@e40pud e40pud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the new feature @stephmilovic!! Code changes LGTM ❤️

Thanks @KDKHD for the local testing! 🚀

Just a note, there are changes coming from my side relevant to the "conversation summary" which might be in conflict with this PR. I'm still waiting for the docs team to add dependencies for the AI Assistant before I can merge those.

@stephmilovic
Copy link
Copy Markdown
Contributor Author

stephmilovic commented Aug 27, 2025

When a user who has shared conversations is deleted, the conversations are still visible to other users. Is this expected?

could not reproduce

Monty West is a user I deleted; however, they still appear in the share menu

This is happening in other user selects too (cases, alerts). I think it is an issue with the SecurityPluginStart['userProfiles'].suggest api

During message duplication, message metadata such as citations are lost:

Fixed here: 1674cf9

Even when test_edwina_keebler does not have access to the assistant, I can share conversations with them

This one is interesting, happening only some of the times to me. I'll investigate further

@KDKHD
Copy link
Copy Markdown
Member

KDKHD commented Aug 28, 2025

image

In this search, the closest match appears at the bottom, can it be at the top?

@elasticmachine
Copy link
Copy Markdown
Contributor

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
alerting 315 318 +3
apm 1969 1972 +3
automaticImport 804 834 +30
cases 1152 1155 +3
datasetQuality 830 833 +3
discover 1396 1399 +3
elasticAssistant 460 492 +32
embeddableAlertsTable 515 518 +3
infra 1531 1534 +3
ml 2501 2504 +3
monitoring 725 728 +3
observability 1397 1400 +3
observabilityAIAssistantApp 437 440 +3
observabilityShared 336 339 +3
securitySolution 7884 7904 +20
slo 1239 1242 +3
stackAlerts 278 281 +3
synthetics 1347 1350 +3
timelines 248 251 +3
transform 784 787 +3
triggersActionsUi 966 969 +3
uptime 868 871 +3
total +139

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/elastic-assistant 176 175 -1
@kbn/elastic-assistant-common 677 688 +11
total +10

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
securitySolution 10.2MB 10.2MB +18.9KB

Public APIs missing exports

Total count of every type that is part of your API that should be exported but is not. This will cause broken links in the API documentation system. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats exports for more detailed information.

id before after diff
@kbn/elastic-assistant 14 13 -1

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
elasticAssistant 273.8KB 301.4KB +27.6KB
securitySolution 96.0KB 96.0KB +2.0B
total +27.6KB
Unknown metric groups

API count

id before after diff
@kbn/elastic-assistant 210 209 -1
@kbn/elastic-assistant-common 790 804 +14
total +13

ESLint disabled line counts

id before after diff
securitySolution 680 681 +1

Total ESLint disabled count

id before after diff
securitySolution 780 781 +1

History

@stephmilovic stephmilovic merged commit f00f71e into elastic:main Aug 28, 2025
12 checks passed
ymao1 pushed a commit to ymao1/kibana that referenced this pull request Aug 29, 2025
@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Sep 1, 2025
@kibanamachine
Copy link
Copy Markdown
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create automatically backports add a backport:* label or prevent reminders by adding the backport:skip label.
You can also create backports manually by running node scripts/backport --pr 230614 locally
cc: @stephmilovic

@stephmilovic stephmilovic added backport:skip This PR does not require backporting and removed backport missing Added to PRs automatically when the are determined to be missing a backport. backport:version Backport to applied version labels labels Sep 2, 2025
benironside added a commit to elastic/docs-content that referenced this pull request Sep 16, 2025
Fixes #2815 by documenting chat sharing for AI Assistant. Related Kibana
PR: elastic/kibana#230614

---------

Co-authored-by: Mike Birnstiehl <114418652+mdbirnstiehl@users.noreply.github.com>
Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
Co-authored-by: Dhrumil Patel <96066689+dhru42@users.noreply.github.com>
Co-authored-by: natasha-moore-elastic <137783811+natasha-moore-elastic@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting release_note:enhancement Team:Security Generative AI Security Generative AI Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v9.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.