[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials#265956
Conversation
a936793 to
2b49af3
Compare
|
Pinging @elastic/security-entity-analytics (Team:Entity Analytics) |
2df1b98 to
d8afcf2
Compare
ymao1
left a comment
There was a problem hiding this comment.
LGTM. Works as described. Can we create a followup issue to maybe hide or disable the leads section if the user doesn't have access to the .entity_analytics-entity-leads index? Maybe disable the Generate button if they don't have write access (but still show the leads generated since they can read?
|
Thanks for testing this @ymao1! You're right to flag the discrepancy let me clarify the two scenarios this PR addresses: Scenario A — Direct read/write calls (what the HTTP 403 fix covers) When a user directly calls:
These are the routes where the HTTP 403 improvement applies. Scenario B — Generate flow (fire-and-forget)
The improvement here is different:
So what you observed , a 200 from status with I'll update the reproduction steps in the PR description to make this distinction clearer. Apologies for the confusion! |
💛 Build succeeded, but was flaky
Failed CI StepsMetrics [docs]
History
|
|
Starting backport for target branches: 9.4 https://github.com/elastic/kibana/actions/runs/25117707078 |
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
…S index-level permission denials (#265956) (#266434) # Backport This will backport the following commits from `main` to `9.4`: - [[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials (#265956)](#265956) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Abhishek Bhatia","email":"117628830+abhishekbhatia1710@users.noreply.github.com"},"sourceCommit":{"committedDate":"2026-04-29T15:22:48Z","message":"[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials (#265956)\n\n## Summary\n\nThis PR improves the error handling in the Lead Generation feature when\nElasticsearch index-level permissions are missing. Previously,\n`security_exception` errors from ES were silently swallowed, resulting\nin either empty data responses (misleading 200s) or unhelpful 500\nerrors. After this change, those errors propagate as proper HTTP 403\nresponses with an actionable message.\n\n---\n\n## Background\n\nThe Lead Generation feature stores leads in\n`.entity-analytics.entity-leads-*` indices. Two separate permission\nlayers protect these APIs:\n\n1. **Kibana feature privileges** (`securitySolution` +\n`${APP_ID}-entity-analytics`) — enforced by the Kibana framework before\nthe handler runs. Missing this results in a Kibana-level 403.\n2. **Elasticsearch index-level privileges** — enforced by ES when\n`esClient.asCurrentUser` executes a query. Missing `read`/`write` access\non the leads indices causes ES to return a `security_exception` with\nstatus 403.\n\nLayer 1 was already correctly configured on all routes. Layer 2 was\nsilently absorbed.\n\n---\n\n## What was broken\n\nIn `lead_data_client.ts`, the `findLeads`, `getStatus`, and\n`createLeads` methods all had catch blocks that swallowed every error:\n\n```typescript\n// findLeads (before)\n} catch (e) {\n // checked only for index_not_found_exception; security_exception was also silently absorbed\n logger.error(`Unable to find leads due to error: ${e}`);\n return { leads: [], total: 0, page, perPage }; // misleading 200 OK\n}\n```\n\nA user without `.entity-analytics.entity-leads-*` read access would get\nback an empty list with a 200 OK instead of a 403 \"access denied\"\nresponse. For `createLeads`, the write would silently fail with only a\nlog warning.\n\n### API flow before fix\n\n```\nGET /internal/entity_analytics/leads\n\nreturns { leads: [], total: 0 } 200 OK with empty data\n```\n\n---\n\n\n### API flow after fix\n\n```\nGET /internal/entity_analytics/leads\ntransformError -> HTTP 403\n- > frontend useQuery onError → toast: \"security_exception: indices:data/read/search (...) \n is unauthorized for user [...]\"\n```\n\n`index_not_found_exception` continues to be swallowed and returns an\nempty result, which is the intended behaviour before the indices are\nfirst created.\n\n---\n\n## Testing steps (before fix)\n\n1. Create a Kibana role with `securitySolution` feature access but\n**no** access to `.entity-analytics.entity-leads-*` ES indices.\n2. Log in as a user with that role.\n3. `GET /internal/entity_analytics/leads` — returns `200 { leads: [],\ntotal: 0 }` instead of `403`.\n4. `GET /internal/entity_analytics/leads/status` — returns `200 {\nisEnabled: false, indexExists: false, ... }` instead of `403`.\n\n## Testing steps (after fix)\n\nSame setup:\n3. `GET /internal/entity_analytics/leads` -> `403 security_exception:\nindices:data/read/search is unauthorized`\n4. `GET /internal/entity_analytics/leads/status` -> `403\nsecurity_exception: ...`\n5. UI: toast notification displays the permission error rather than\nshowing empty leads.\n\n---------\n\nCo-authored-by: Ying <ying.mao@elastic.co>","sha":"812db45997ceb87ce94a3478c3d317148edc83b6","branchLabelMapping":{"^v9.5.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Entity Analytics","backport:version","v9.4.0","v9.5.0"],"title":"[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials","number":265956,"url":"https://github.com/elastic/kibana/pull/265956","mergeCommit":{"message":"[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials (#265956)\n\n## Summary\n\nThis PR improves the error handling in the Lead Generation feature when\nElasticsearch index-level permissions are missing. Previously,\n`security_exception` errors from ES were silently swallowed, resulting\nin either empty data responses (misleading 200s) or unhelpful 500\nerrors. After this change, those errors propagate as proper HTTP 403\nresponses with an actionable message.\n\n---\n\n## Background\n\nThe Lead Generation feature stores leads in\n`.entity-analytics.entity-leads-*` indices. Two separate permission\nlayers protect these APIs:\n\n1. **Kibana feature privileges** (`securitySolution` +\n`${APP_ID}-entity-analytics`) — enforced by the Kibana framework before\nthe handler runs. Missing this results in a Kibana-level 403.\n2. **Elasticsearch index-level privileges** — enforced by ES when\n`esClient.asCurrentUser` executes a query. Missing `read`/`write` access\non the leads indices causes ES to return a `security_exception` with\nstatus 403.\n\nLayer 1 was already correctly configured on all routes. Layer 2 was\nsilently absorbed.\n\n---\n\n## What was broken\n\nIn `lead_data_client.ts`, the `findLeads`, `getStatus`, and\n`createLeads` methods all had catch blocks that swallowed every error:\n\n```typescript\n// findLeads (before)\n} catch (e) {\n // checked only for index_not_found_exception; security_exception was also silently absorbed\n logger.error(`Unable to find leads due to error: ${e}`);\n return { leads: [], total: 0, page, perPage }; // misleading 200 OK\n}\n```\n\nA user without `.entity-analytics.entity-leads-*` read access would get\nback an empty list with a 200 OK instead of a 403 \"access denied\"\nresponse. For `createLeads`, the write would silently fail with only a\nlog warning.\n\n### API flow before fix\n\n```\nGET /internal/entity_analytics/leads\n\nreturns { leads: [], total: 0 } 200 OK with empty data\n```\n\n---\n\n\n### API flow after fix\n\n```\nGET /internal/entity_analytics/leads\ntransformError -> HTTP 403\n- > frontend useQuery onError → toast: \"security_exception: indices:data/read/search (...) \n is unauthorized for user [...]\"\n```\n\n`index_not_found_exception` continues to be swallowed and returns an\nempty result, which is the intended behaviour before the indices are\nfirst created.\n\n---\n\n## Testing steps (before fix)\n\n1. Create a Kibana role with `securitySolution` feature access but\n**no** access to `.entity-analytics.entity-leads-*` ES indices.\n2. Log in as a user with that role.\n3. `GET /internal/entity_analytics/leads` — returns `200 { leads: [],\ntotal: 0 }` instead of `403`.\n4. `GET /internal/entity_analytics/leads/status` — returns `200 {\nisEnabled: false, indexExists: false, ... }` instead of `403`.\n\n## Testing steps (after fix)\n\nSame setup:\n3. `GET /internal/entity_analytics/leads` -> `403 security_exception:\nindices:data/read/search is unauthorized`\n4. `GET /internal/entity_analytics/leads/status` -> `403\nsecurity_exception: ...`\n5. UI: toast notification displays the permission error rather than\nshowing empty leads.\n\n---------\n\nCo-authored-by: Ying <ying.mao@elastic.co>","sha":"812db45997ceb87ce94a3478c3d317148edc83b6"}},"sourceBranch":"main","suggestedTargetBranches":["9.4"],"targetPullRequestStates":[{"branch":"9.4","label":"v9.4.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.5.0","branchLabelMappingKey":"^v9.5.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/265956","number":265956,"mergeCommit":{"message":"[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials (#265956)\n\n## Summary\n\nThis PR improves the error handling in the Lead Generation feature when\nElasticsearch index-level permissions are missing. Previously,\n`security_exception` errors from ES were silently swallowed, resulting\nin either empty data responses (misleading 200s) or unhelpful 500\nerrors. After this change, those errors propagate as proper HTTP 403\nresponses with an actionable message.\n\n---\n\n## Background\n\nThe Lead Generation feature stores leads in\n`.entity-analytics.entity-leads-*` indices. Two separate permission\nlayers protect these APIs:\n\n1. **Kibana feature privileges** (`securitySolution` +\n`${APP_ID}-entity-analytics`) — enforced by the Kibana framework before\nthe handler runs. Missing this results in a Kibana-level 403.\n2. **Elasticsearch index-level privileges** — enforced by ES when\n`esClient.asCurrentUser` executes a query. Missing `read`/`write` access\non the leads indices causes ES to return a `security_exception` with\nstatus 403.\n\nLayer 1 was already correctly configured on all routes. Layer 2 was\nsilently absorbed.\n\n---\n\n## What was broken\n\nIn `lead_data_client.ts`, the `findLeads`, `getStatus`, and\n`createLeads` methods all had catch blocks that swallowed every error:\n\n```typescript\n// findLeads (before)\n} catch (e) {\n // checked only for index_not_found_exception; security_exception was also silently absorbed\n logger.error(`Unable to find leads due to error: ${e}`);\n return { leads: [], total: 0, page, perPage }; // misleading 200 OK\n}\n```\n\nA user without `.entity-analytics.entity-leads-*` read access would get\nback an empty list with a 200 OK instead of a 403 \"access denied\"\nresponse. For `createLeads`, the write would silently fail with only a\nlog warning.\n\n### API flow before fix\n\n```\nGET /internal/entity_analytics/leads\n\nreturns { leads: [], total: 0 } 200 OK with empty data\n```\n\n---\n\n\n### API flow after fix\n\n```\nGET /internal/entity_analytics/leads\ntransformError -> HTTP 403\n- > frontend useQuery onError → toast: \"security_exception: indices:data/read/search (...) \n is unauthorized for user [...]\"\n```\n\n`index_not_found_exception` continues to be swallowed and returns an\nempty result, which is the intended behaviour before the indices are\nfirst created.\n\n---\n\n## Testing steps (before fix)\n\n1. Create a Kibana role with `securitySolution` feature access but\n**no** access to `.entity-analytics.entity-leads-*` ES indices.\n2. Log in as a user with that role.\n3. `GET /internal/entity_analytics/leads` — returns `200 { leads: [],\ntotal: 0 }` instead of `403`.\n4. `GET /internal/entity_analytics/leads/status` — returns `200 {\nisEnabled: false, indexExists: false, ... }` instead of `403`.\n\n## Testing steps (after fix)\n\nSame setup:\n3. `GET /internal/entity_analytics/leads` -> `403 security_exception:\nindices:data/read/search is unauthorized`\n4. `GET /internal/entity_analytics/leads/status` -> `403\nsecurity_exception: ...`\n5. UI: toast notification displays the permission error rather than\nshowing empty leads.\n\n---------\n\nCo-authored-by: Ying <ying.mao@elastic.co>","sha":"812db45997ceb87ce94a3478c3d317148edc83b6"}}]}] BACKPORT--> Co-authored-by: Abhishek Bhatia <117628830+abhishekbhatia1710@users.noreply.github.com> Co-authored-by: Ying <ying.mao@elastic.co>
…nerate button based on ES index permissions (#266586) ## Summary Follow-up to #265956, addressing the review request by @ymao1 in [this comment](#265956 (review)). Closes elastic/security-team#17123 Adds permission-aware UI behaviour to the leads section based on the user's Elasticsearch index-level access to `.entity_analytics.entity-leads-*`: - **No read access**: the entire leads section is hidden - **Read but no write access**: leads are shown, but the Generate and Refresh buttons are disabled with a tooltip explaining the permission requirement - **Full access**: no change, UI behaves as before ### How it works A new internal API route `GET /internal/entity_analytics/leads/privileges` checks `read` and `write` privileges on the leads index pattern for the current user (using `checkPrivilegesDynamicallyWithRequest`). The result is fetched once on page load inside `useHuntingLeads` and drives the UI state. ## Screenshots Generate button disabled (no write access) <img width="1722" height="783" alt="Screenshot 2026-04-30 at 1 54 51 PM" src="https://github.com/user-attachments/assets/eade852a-f204-49f3-a96d-08e64913049c" /> Leads section hidden (no read access) <img width="1919" height="903" alt="Screenshot 2026-04-30 at 1 54 02 PM" src="https://github.com/user-attachments/assets/99649213-f63e-4304-b76a-4cfc922bb987" /> ## Testing ### Test users to create Use the Kibana Dev Console (`Stack Management > Dev Tools`) to create the following users. **1. Full access user** (read + write on leads index) ``` POST /_security/role/leads_full_access { "indices": [ { "names": [".entity_analytics.entity-leads-*"], "privileges": ["read", "write", "create_index"] } ] } POST /_security/user/leads_full_user { "roles": ["kibana_admin", "leads_full_access"] } ``` **2. Read-only user** (read on leads index, no write) ``` POST /_security/role/leads_read_only { "indices": [ { "names": [".entity_analytics.entity-leads-*"], "privileges": ["read"] } ] } POST /_security/user/leads_read_user { "roles": ["kibana_admin", "leads_read_only"] } ``` **3. No access user** (no leads index permissions) ``` POST /_security/user/leads_no_access_user { "roles": ["kibana_admin"] } ``` ### Test steps 1. Log in as `leads_full_user` and navigate to the Entity Analytics page. The leads section should be fully visible with Generate and Refresh buttons enabled. 2. Log in as `leads_read_user` and navigate to the Entity Analytics page. The leads section should be visible but the Generate and Refresh buttons should be disabled with a tooltip. 3. Log in as `leads_no_access_user` and navigate to the Entity Analytics page. The leads section should not be rendered. > Note: For steps 2 and 3 to be meaningful, lead generation should have been enabled first (via a full-access user) so the index exists. The privilege check is role-based and works even before the index is created.
…ble Generate button based on ES index permissions (#266586) (#267383) # Backport This will backport the following commits from `main` to `9.4`: - [[Entity Analytics][Lead generation] Hide leads section and disable Generate button based on ES index permissions (#266586)](#266586) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Abhishek Bhatia","email":"117628830+abhishekbhatia1710@users.noreply.github.com"},"sourceCommit":{"committedDate":"2026-05-04T07:02:05Z","message":"[Entity Analytics][Lead generation] Hide leads section and disable Generate button based on ES index permissions (#266586)\n\n## Summary\n\nFollow-up to #265956, addressing the review request by @ymao1 in [this\ncomment](https://github.com/elastic/kibana/pull/265956#pullrequestreview-4197650546).\n\nCloses elastic/security-team#17123\n\nAdds permission-aware UI behaviour to the leads section based on the\nuser's Elasticsearch index-level access to\n`.entity_analytics.entity-leads-*`:\n\n- **No read access**: the entire leads section is hidden\n- **Read but no write access**: leads are shown, but the Generate and\nRefresh buttons are disabled with a tooltip explaining the permission\nrequirement\n- **Full access**: no change, UI behaves as before\n\n### How it works\n\nA new internal API route `GET\n/internal/entity_analytics/leads/privileges` checks `read` and `write`\nprivileges on the leads index pattern for the current user (using\n`checkPrivilegesDynamicallyWithRequest`). The result is fetched once on\npage load inside `useHuntingLeads` and drives the UI state.\n\n## Screenshots\n\nGenerate button disabled (no write access)\n\n<img width=\"1722\" height=\"783\" alt=\"Screenshot 2026-04-30 at 1 54 51 PM\"\nsrc=\"https://github.com/user-attachments/assets/eade852a-f204-49f3-a96d-08e64913049c\"\n/>\n\n\nLeads section hidden (no read access)\n<img width=\"1919\" height=\"903\" alt=\"Screenshot 2026-04-30 at 1 54 02 PM\"\nsrc=\"https://github.com/user-attachments/assets/99649213-f63e-4304-b76a-4cfc922bb987\"\n/>\n\n\n\n## Testing\n\n### Test users to create\n\nUse the Kibana Dev Console (`Stack Management > Dev Tools`) to create\nthe following users.\n\n**1. Full access user** (read + write on leads index)\n\n```\nPOST /_security/role/leads_full_access\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\", \"write\", \"create_index\"]\n }\n ]\n}\n\nPOST /_security/user/leads_full_user\n{\n \"roles\": [\"kibana_admin\", \"leads_full_access\"]\n}\n```\n\n**2. Read-only user** (read on leads index, no write)\n\n```\nPOST /_security/role/leads_read_only\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\"]\n }\n ]\n}\n\nPOST /_security/user/leads_read_user\n{\n \"roles\": [\"kibana_admin\", \"leads_read_only\"]\n}\n```\n\n**3. No access user** (no leads index permissions)\n\n```\nPOST /_security/user/leads_no_access_user\n{\n \"roles\": [\"kibana_admin\"]\n}\n```\n\n### Test steps\n\n1. Log in as `leads_full_user` and navigate to the Entity Analytics\npage. The leads section should be fully visible with Generate and\nRefresh buttons enabled.\n2. Log in as `leads_read_user` and navigate to the Entity Analytics\npage. The leads section should be visible but the Generate and Refresh\nbuttons should be disabled with a tooltip.\n3. Log in as `leads_no_access_user` and navigate to the Entity Analytics\npage. The leads section should not be rendered.\n\n> Note: For steps 2 and 3 to be meaningful, lead generation should have\nbeen enabled first (via a full-access user) so the index exists. The\nprivilege check is role-based and works even before the index is\ncreated.","sha":"4aec4fe4cc1aeaba2a4dff558f338bc3491efd29","branchLabelMapping":{"^v9.5.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Entity Analytics","backport:version","v9.4.0","v9.5.0"],"title":"[Entity Analytics][Lead generation] Hide leads section and disable Generate button based on ES index permissions","number":266586,"url":"https://github.com/elastic/kibana/pull/266586","mergeCommit":{"message":"[Entity Analytics][Lead generation] Hide leads section and disable Generate button based on ES index permissions (#266586)\n\n## Summary\n\nFollow-up to #265956, addressing the review request by @ymao1 in [this\ncomment](https://github.com/elastic/kibana/pull/265956#pullrequestreview-4197650546).\n\nCloses elastic/security-team#17123\n\nAdds permission-aware UI behaviour to the leads section based on the\nuser's Elasticsearch index-level access to\n`.entity_analytics.entity-leads-*`:\n\n- **No read access**: the entire leads section is hidden\n- **Read but no write access**: leads are shown, but the Generate and\nRefresh buttons are disabled with a tooltip explaining the permission\nrequirement\n- **Full access**: no change, UI behaves as before\n\n### How it works\n\nA new internal API route `GET\n/internal/entity_analytics/leads/privileges` checks `read` and `write`\nprivileges on the leads index pattern for the current user (using\n`checkPrivilegesDynamicallyWithRequest`). The result is fetched once on\npage load inside `useHuntingLeads` and drives the UI state.\n\n## Screenshots\n\nGenerate button disabled (no write access)\n\n<img width=\"1722\" height=\"783\" alt=\"Screenshot 2026-04-30 at 1 54 51 PM\"\nsrc=\"https://github.com/user-attachments/assets/eade852a-f204-49f3-a96d-08e64913049c\"\n/>\n\n\nLeads section hidden (no read access)\n<img width=\"1919\" height=\"903\" alt=\"Screenshot 2026-04-30 at 1 54 02 PM\"\nsrc=\"https://github.com/user-attachments/assets/99649213-f63e-4304-b76a-4cfc922bb987\"\n/>\n\n\n\n## Testing\n\n### Test users to create\n\nUse the Kibana Dev Console (`Stack Management > Dev Tools`) to create\nthe following users.\n\n**1. Full access user** (read + write on leads index)\n\n```\nPOST /_security/role/leads_full_access\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\", \"write\", \"create_index\"]\n }\n ]\n}\n\nPOST /_security/user/leads_full_user\n{\n \"roles\": [\"kibana_admin\", \"leads_full_access\"]\n}\n```\n\n**2. Read-only user** (read on leads index, no write)\n\n```\nPOST /_security/role/leads_read_only\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\"]\n }\n ]\n}\n\nPOST /_security/user/leads_read_user\n{\n \"roles\": [\"kibana_admin\", \"leads_read_only\"]\n}\n```\n\n**3. No access user** (no leads index permissions)\n\n```\nPOST /_security/user/leads_no_access_user\n{\n \"roles\": [\"kibana_admin\"]\n}\n```\n\n### Test steps\n\n1. Log in as `leads_full_user` and navigate to the Entity Analytics\npage. The leads section should be fully visible with Generate and\nRefresh buttons enabled.\n2. Log in as `leads_read_user` and navigate to the Entity Analytics\npage. The leads section should be visible but the Generate and Refresh\nbuttons should be disabled with a tooltip.\n3. Log in as `leads_no_access_user` and navigate to the Entity Analytics\npage. The leads section should not be rendered.\n\n> Note: For steps 2 and 3 to be meaningful, lead generation should have\nbeen enabled first (via a full-access user) so the index exists. The\nprivilege check is role-based and works even before the index is\ncreated.","sha":"4aec4fe4cc1aeaba2a4dff558f338bc3491efd29"}},"sourceBranch":"main","suggestedTargetBranches":["9.4"],"targetPullRequestStates":[{"branch":"9.4","label":"v9.4.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.5.0","branchLabelMappingKey":"^v9.5.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/266586","number":266586,"mergeCommit":{"message":"[Entity Analytics][Lead generation] Hide leads section and disable Generate button based on ES index permissions (#266586)\n\n## Summary\n\nFollow-up to #265956, addressing the review request by @ymao1 in [this\ncomment](https://github.com/elastic/kibana/pull/265956#pullrequestreview-4197650546).\n\nCloses elastic/security-team#17123\n\nAdds permission-aware UI behaviour to the leads section based on the\nuser's Elasticsearch index-level access to\n`.entity_analytics.entity-leads-*`:\n\n- **No read access**: the entire leads section is hidden\n- **Read but no write access**: leads are shown, but the Generate and\nRefresh buttons are disabled with a tooltip explaining the permission\nrequirement\n- **Full access**: no change, UI behaves as before\n\n### How it works\n\nA new internal API route `GET\n/internal/entity_analytics/leads/privileges` checks `read` and `write`\nprivileges on the leads index pattern for the current user (using\n`checkPrivilegesDynamicallyWithRequest`). The result is fetched once on\npage load inside `useHuntingLeads` and drives the UI state.\n\n## Screenshots\n\nGenerate button disabled (no write access)\n\n<img width=\"1722\" height=\"783\" alt=\"Screenshot 2026-04-30 at 1 54 51 PM\"\nsrc=\"https://github.com/user-attachments/assets/eade852a-f204-49f3-a96d-08e64913049c\"\n/>\n\n\nLeads section hidden (no read access)\n<img width=\"1919\" height=\"903\" alt=\"Screenshot 2026-04-30 at 1 54 02 PM\"\nsrc=\"https://github.com/user-attachments/assets/99649213-f63e-4304-b76a-4cfc922bb987\"\n/>\n\n\n\n## Testing\n\n### Test users to create\n\nUse the Kibana Dev Console (`Stack Management > Dev Tools`) to create\nthe following users.\n\n**1. Full access user** (read + write on leads index)\n\n```\nPOST /_security/role/leads_full_access\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\", \"write\", \"create_index\"]\n }\n ]\n}\n\nPOST /_security/user/leads_full_user\n{\n \"roles\": [\"kibana_admin\", \"leads_full_access\"]\n}\n```\n\n**2. Read-only user** (read on leads index, no write)\n\n```\nPOST /_security/role/leads_read_only\n{\n \"indices\": [\n {\n \"names\": [\".entity_analytics.entity-leads-*\"],\n \"privileges\": [\"read\"]\n }\n ]\n}\n\nPOST /_security/user/leads_read_user\n{\n \"roles\": [\"kibana_admin\", \"leads_read_only\"]\n}\n```\n\n**3. No access user** (no leads index permissions)\n\n```\nPOST /_security/user/leads_no_access_user\n{\n \"roles\": [\"kibana_admin\"]\n}\n```\n\n### Test steps\n\n1. Log in as `leads_full_user` and navigate to the Entity Analytics\npage. The leads section should be fully visible with Generate and\nRefresh buttons enabled.\n2. Log in as `leads_read_user` and navigate to the Entity Analytics\npage. The leads section should be visible but the Generate and Refresh\nbuttons should be disabled with a tooltip.\n3. Log in as `leads_no_access_user` and navigate to the Entity Analytics\npage. The leads section should not be rendered.\n\n> Note: For steps 2 and 3 to be meaningful, lead generation should have\nbeen enabled first (via a full-access user) so the index exists. The\nprivilege check is role-based and works even before the index is\ncreated.","sha":"4aec4fe4cc1aeaba2a4dff558f338bc3491efd29"}}]}] BACKPORT--> Co-authored-by: Abhishek Bhatia <117628830+abhishekbhatia1710@users.noreply.github.com>
…nerate button based on ES index permissions (elastic#266586) ## Summary Follow-up to elastic#265956, addressing the review request by @ymao1 in [this comment](elastic#265956 (review)). Closes elastic/security-team#17123 Adds permission-aware UI behaviour to the leads section based on the user's Elasticsearch index-level access to `.entity_analytics.entity-leads-*`: - **No read access**: the entire leads section is hidden - **Read but no write access**: leads are shown, but the Generate and Refresh buttons are disabled with a tooltip explaining the permission requirement - **Full access**: no change, UI behaves as before ### How it works A new internal API route `GET /internal/entity_analytics/leads/privileges` checks `read` and `write` privileges on the leads index pattern for the current user (using `checkPrivilegesDynamicallyWithRequest`). The result is fetched once on page load inside `useHuntingLeads` and drives the UI state. ## Screenshots Generate button disabled (no write access) <img width="1722" height="783" alt="Screenshot 2026-04-30 at 1 54 51 PM" src="https://github.com/user-attachments/assets/eade852a-f204-49f3-a96d-08e64913049c" /> Leads section hidden (no read access) <img width="1919" height="903" alt="Screenshot 2026-04-30 at 1 54 02 PM" src="https://github.com/user-attachments/assets/99649213-f63e-4304-b76a-4cfc922bb987" /> ## Testing ### Test users to create Use the Kibana Dev Console (`Stack Management > Dev Tools`) to create the following users. **1. Full access user** (read + write on leads index) ``` POST /_security/role/leads_full_access { "indices": [ { "names": [".entity_analytics.entity-leads-*"], "privileges": ["read", "write", "create_index"] } ] } POST /_security/user/leads_full_user { "roles": ["kibana_admin", "leads_full_access"] } ``` **2. Read-only user** (read on leads index, no write) ``` POST /_security/role/leads_read_only { "indices": [ { "names": [".entity_analytics.entity-leads-*"], "privileges": ["read"] } ] } POST /_security/user/leads_read_user { "roles": ["kibana_admin", "leads_read_only"] } ``` **3. No access user** (no leads index permissions) ``` POST /_security/user/leads_no_access_user { "roles": ["kibana_admin"] } ``` ### Test steps 1. Log in as `leads_full_user` and navigate to the Entity Analytics page. The leads section should be fully visible with Generate and Refresh buttons enabled. 2. Log in as `leads_read_user` and navigate to the Entity Analytics page. The leads section should be visible but the Generate and Refresh buttons should be disabled with a tooltip. 3. Log in as `leads_no_access_user` and navigate to the Entity Analytics page. The leads section should not be rendered. > Note: For steps 2 and 3 to be meaningful, lead generation should have been enabled first (via a full-access user) so the index exists. The privilege check is role-based and works even before the index is created.



Summary
This PR improves the error handling in the Lead Generation feature when Elasticsearch index-level permissions are missing. Previously,
security_exceptionerrors from ES were silently swallowed, resulting in either empty data responses (misleading 200s) or unhelpful 500 errors. After this change, those errors propagate as proper HTTP 403 responses with an actionable message.Background
The Lead Generation feature stores leads in
.entity-analytics.entity-leads-*indices. Two separate permission layers protect these APIs:securitySolution+${APP_ID}-entity-analytics) — enforced by the Kibana framework before the handler runs. Missing this results in a Kibana-level 403.esClient.asCurrentUserexecutes a query. Missingread/writeaccess on the leads indices causes ES to return asecurity_exceptionwith status 403.Layer 1 was already correctly configured on all routes. Layer 2 was silently absorbed.
What was broken
In
lead_data_client.ts, thefindLeads,getStatus, andcreateLeadsmethods all had catch blocks that swallowed every error:A user without
.entity-analytics.entity-leads-*read access would get back an empty list with a 200 OK instead of a 403 "access denied" response. ForcreateLeads, the write would silently fail with only a log warning.API flow before fix
API flow after fix
index_not_found_exceptioncontinues to be swallowed and returns an empty result, which is the intended behaviour before the indices are first created.Testing steps (before fix)
securitySolutionfeature access but no access to.entity-analytics.entity-leads-*ES indices.GET /internal/entity_analytics/leads— returns200 { leads: [], total: 0 }instead of403.GET /internal/entity_analytics/leads/status— returns200 { isEnabled: false, indexExists: false, ... }instead of403.Testing steps (after fix)
Same setup:
3.
GET /internal/entity_analytics/leads->403 security_exception: indices:data/read/search is unauthorized4.
GET /internal/entity_analytics/leads/status->403 security_exception: ...5. UI: toast notification displays the permission error rather than showing empty leads.