Skip to content

[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials#265956

Merged
abhishekbhatia1710 merged 3 commits intoelastic:mainfrom
abhishekbhatia1710:lead-gen-error-state-improvements
Apr 29, 2026
Merged

[Entity Analytics][Leads generation] Improve error states for ES index-level permission denials#265956
abhishekbhatia1710 merged 3 commits intoelastic:mainfrom
abhishekbhatia1710:lead-gen-error-state-improvements

Conversation

@abhishekbhatia1710
Copy link
Copy Markdown
Contributor

@abhishekbhatia1710 abhishekbhatia1710 commented Apr 28, 2026

Summary

This PR improves the error handling in the Lead Generation feature when Elasticsearch index-level permissions are missing. Previously, security_exception errors 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:

  1. Kibana feature privileges (securitySolution + ${APP_ID}-entity-analytics) — enforced by the Kibana framework before the handler runs. Missing this results in a Kibana-level 403.
  2. Elasticsearch index-level privileges — enforced by ES when esClient.asCurrentUser executes a query. Missing read/write access on the leads indices causes ES to return a security_exception with status 403.

Layer 1 was already correctly configured on all routes. Layer 2 was silently absorbed.


What was broken

In lead_data_client.ts, the findLeads, getStatus, and createLeads methods all had catch blocks that swallowed every error:

// findLeads (before)
} catch (e) {
  // checked only for index_not_found_exception; security_exception was also silently absorbed
  logger.error(`Unable to find leads due to error: ${e}`);
  return { leads: [], total: 0, page, perPage }; //  misleading 200 OK
}

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. For createLeads, the write would silently fail with only a log warning.

API flow before fix

GET /internal/entity_analytics/leads

returns { leads: [], total: 0 }   200 OK with empty data

API flow after fix

GET /internal/entity_analytics/leads
transformError -> HTTP 403
- > frontend useQuery onError → toast: "security_exception: indices:data/read/search (...)  
    is unauthorized for user [...]"

index_not_found_exception continues to be swallowed and returns an empty result, which is the intended behaviour before the indices are first created.


Testing steps (before fix)

  1. Create a Kibana role with securitySolution feature access but no access to .entity-analytics.entity-leads-* ES indices.
  2. Log in as a user with that role.
  3. GET /internal/entity_analytics/leads — returns 200 { leads: [], total: 0 } instead of 403.
  4. GET /internal/entity_analytics/leads/status — returns 200 { isEnabled: false, indexExists: false, ... } instead of 403.

Testing steps (after fix)

Same setup:
3. GET /internal/entity_analytics/leads -> 403 security_exception: indices:data/read/search is unauthorized
4. GET /internal/entity_analytics/leads/status -> 403 security_exception: ...
5. UI: toast notification displays the permission error rather than showing empty leads.

@abhishekbhatia1710 abhishekbhatia1710 self-assigned this Apr 28, 2026
@abhishekbhatia1710 abhishekbhatia1710 added Team:Entity Analytics Security Entity Analytics Team backport:version Backport to applied version labels v9.4.0 release_note:skip Skip the PR/issue when compiling release notes and removed Team:Entity Analytics Security Entity Analytics Team labels Apr 28, 2026
@abhishekbhatia1710 abhishekbhatia1710 changed the title feat(lead-generation): improve error states for ES index-level permission denials [Entity Analytics][Leads generation] Improve error states for ES index-level permission denials Apr 29, 2026
@abhishekbhatia1710 abhishekbhatia1710 force-pushed the lead-gen-error-state-improvements branch from a936793 to 2b49af3 Compare April 29, 2026 07:56
@abhishekbhatia1710 abhishekbhatia1710 added the Team:Entity Analytics Security Entity Analytics Team label Apr 29, 2026
@abhishekbhatia1710 abhishekbhatia1710 marked this pull request as ready for review April 29, 2026 07:57
@abhishekbhatia1710 abhishekbhatia1710 requested a review from a team as a code owner April 29, 2026 07:57
@infra-vault-gh-plugin-prod
Copy link
Copy Markdown

Pinging @elastic/security-entity-analytics (Team:Entity Analytics)

@abhishekbhatia1710 abhishekbhatia1710 force-pushed the lead-gen-error-state-improvements branch from 2df1b98 to d8afcf2 Compare April 29, 2026 08:54
@ymao1
Copy link
Copy Markdown
Contributor

ymao1 commented Apr 29, 2026

Testing steps (after fix)
Same setup:
3. GET /internal/entity_analytics/leads -> 403 security_exception: indices:data/read/search is unauthorized
4. GET /internal/entity_analytics/leads/status -> 403 security_exception: ...
5. UI: toast notification displays the permission error rather than showing empty leads.

Just wanted to check, I'm seeing the error toast but these routes still return a 200 because generate just returns an execution UUID and status returns the error within the status, which is then thrown in the UI. This ends up with the same result.

Screenshot 2026-04-29 at 9 57 31 AM Screenshot 2026-04-29 at 9 57 53 AM Screenshot 2026-04-29 at 9 58 05 AM

Copy link
Copy Markdown
Contributor

@ymao1 ymao1 left a comment

Choose a reason for hiding this comment

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

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?

@abhishekbhatia1710
Copy link
Copy Markdown
Contributor Author

abhishekbhatia1710 commented Apr 29, 2026

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:

  • GET /internal/entity_analytics/leads → was 200 with empty data, now 403 HTTP
  • GET /internal/entity_analytics/leads/status → was 200 with default values, now 403 HTTP
  • POST /internal/entity_analytics/leads/dismiss/{id} → was silently swallowed, now 403 HTTP
  • POST /internal/entity_analytics/leads/bulk_update → same as above

These are the routes where the HTTP 403 improvement applies.

Scenario B — Generate flow (fire-and-forget)

POST /generate always returns 200 immediately with an executionUuid, the actual pipeline runs in the background. This is by design and unchanged.

The improvement here is different:

  • Before: createLeads silently swallowed the security_exception, the pipeline reported success, lastError stayed null, the UI showed nothing (silent failure)
  • After: createLeads re-throws the security_exception, the pipeline's catch block captures it, lastError is set to the error message, the UI toast surfaces it

So what you observed , a 200 from status with lastError populated + a toast is exactly the correct post-fix behaviour for the generate flow.

I'll update the reproduction steps in the PR description to make this distinction clearer. Apologies for the confusion!

@kibanamachine
Copy link
Copy Markdown
Contributor

💛 Build succeeded, but was flaky

Failed CI Steps

Metrics [docs]

✅ unchanged

History

cc @abhishekbhatia1710

@abhishekbhatia1710 abhishekbhatia1710 merged commit 812db45 into elastic:main Apr 29, 2026
27 checks passed
@kibanamachine
Copy link
Copy Markdown
Contributor

Starting backport for target branches: 9.4

https://github.com/elastic/kibana/actions/runs/25117707078

@kibanamachine
Copy link
Copy Markdown
Contributor

💚 All backports created successfully

Status Branch Result
9.4

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

kibanamachine added a commit that referenced this pull request Apr 29, 2026
…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>
abhishekbhatia1710 added a commit that referenced this pull request May 4, 2026
…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.
kibanamachine added a commit that referenced this pull request May 4, 2026
…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>
seanrathier pushed a commit to seanrathier/kibana that referenced this pull request May 4, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:version Backport to applied version labels release_note:skip Skip the PR/issue when compiling release notes Team:Entity Analytics Security Entity Analytics Team v9.4.0 v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants