Skip to content

Cases implicit privileges provider - #152714

Merged
michaelolo24 merged 22 commits into
elastic:mainfrom
michaelolo24:cases-implicit-privileges-provider
Jul 7, 2026
Merged

michaelolo24 merged 22 commits into
elastic:mainfrom
michaelolo24:cases-implicit-privileges-provider

Conversation

@michaelolo24

@michaelolo24 michaelolo24 commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a second ImplicitPrivilegesProvider to the x-pack/plugin/kibana plugin (introduced in #148331 for Alerting V2), this one for Kibana Cases.

When a user holds a Kibana application privilege whose stored definition includes a cases:<owner>/getCase action, this provider implicitly grants read on the .cases*, .cases-activity*, and .cases-attachments* index patterns (which match each index literal and any future sibling/reindexed indices owned by Cases), DLS-scoped by the owner field always and the top-level space_id field for non-wildcard space resources.

Unlike Alerting V2, Cases documents carry two independent scoping dimensions — the owning solution (owner: securitySolution / observability / cases) and the Kibana space (space_id) — and each owner has its own action. So the DLS query for every grant always filters on owner, plus space_id unless the role holds the wildcard resource (*) for that owner.

A role granting cases:observability/getCase on space:marketing can therefore run FROM .cases-activity in ES|QL and see only Observability-owned case documents whose space_id equals marketing, with no explicit index privilege configuration in the role.

Background

Builds on:

The .cases* indices, their top-level space_id / owner document fields, and the cases:<owner>/getCase actions this provider keys on are all created on the Kibana side by the Cases "as data" V2 work — useful for reviewers to see where the index patterns, document fields, and actions come from:

This is the Cases piece of Phase 2 of the As-Data RBAC initiative.

What this adds

  • KibanaCasesImplicitPrivilegesProvider - the ImplicitPrivilegesProvider implementation for Cases, registered alongside the alerts provider via KibanaPlugin#getImplicitPrivilegesProviders. Resources are grouped by owner and emitted as one DLS grant per owner: owner-only when the role holds the wildcard resource for that owner, owner + space_id otherwise. Same dual-track matching as the alerts provider — a role block grants an owner's action if either its privileges[] names a stored Kibana privilege whose action set matches, or its privileges[] are themselves action patterns that match (e.g. cases:securitySolution/*, *) — and application-name matching honors wildcards (kibana-*, *).
  • Owner values, index names, and action strings intentionally mirror the Kibana definitions and are commented with their source paths (cases/common/constants, cases/server/cases_analytics_v2/constants.ts, and the authorization_core cases actions/privileges). Keep them in sync if those change.
  • .github/CODEOWNERS - per-file override co-owning KibanaCasesImplicitPrivilegesProvider (and its test) with @elastic/kibana-cases, following the convention feat(kibana,security): introduce kibana x-pack plugin to manage Kibana-specific implicit privileges #148331 set for the alerts provider (co-owned with @elastic/response-ops).
  • Unit tests (KibanaCasesImplicitPrivilegesProviderTests) covering the owner/space grouping, the wildcard-resource (all-spaces) path, resolved-name vs raw-pattern matching, non-Kibana-application / wildcard-app-name paths, and the generated DLS query shape.
  • javaRestTest integration test (KibanaCasesImplicitPrivilegesIT) exercising the end-to-end grant against a running cluster.
  • Also folds in review follow-ups to KibanaAlertsImplicitPrivilegesProvider (e.g. switching from Automatons to StringMatcher) and its changelog entry.

azasypkin and others added 8 commits June 22, 2026 10:50
…les-cluster-test

Add javaRestTest for Kibana implicit privileges
Adds an ImplicitPrivilegesProvider for the Kibana Cases-as-data
indices (.cases, .cases-activity, .cases-attachments), scoped by both
Kibana space (space_id) and owning solution (owner: cases,
observability, securitySolution) via the cases:<owner>/getCase
actions. Also drops the redundant x-pack-core extendedPlugins entry
flagged in outstanding review feedback on elastic#148331, since
x-pack-security already extends it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@elasticsearchmachine elasticsearchmachine added v9.5.0 external-contributor Pull request authored by a developer outside the Elasticsearch team labels Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Preview links for changed docs

⏳ Building and deploying preview... View progress

This comment will be updated with preview links when the build is complete.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Important: Docs version tagging

👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version.

We use applies_to tags to mark version-specific features and changes.

Expand for a quick overview

When to use applies_to tags:

✅ At the page level to indicate which products/deployments the content applies to (mandatory)
✅ When features change state (e.g. preview, ga) in a specific version
✅ When availability differs across deployments and environments

What NOT to do:

❌ Don't remove or replace information that applies to an older version
❌ Don't add new information that applies to a specific version without an applies_to tag
❌ Don't forget that applies_to tags can be used at the page, section, and inline level

🤔 Need help?

@michaelolo24
michaelolo24 force-pushed the cases-implicit-privileges-provider branch from a596bf5 to 3c961c9 Compare July 2, 2026 12:15
michaelolo24 and others added 4 commits July 2, 2026 14:17
…ssons to the Cases provider

Mirrors three fixes that landed on elastic#148331 this week, applied only to
the Cases-specific files (not the shared plugin scaffold):

- Fast-path return when the role has no application privileges at
  all, avoiding an unnecessary scan of stored privileges.
- Replace stream().anyMatch() + list allocation with a plain for-loop
  for the resolved-name matching path (JIT-friendlier).
- Hand-roll the DLS queries via XContentBuilder rather than
  QueryBuilders.termQuery/termsQuery, which always serialize a
  "boost":1.0 field that would otherwise leak into the query
  surfaced via GET /_security/role/<name>?include_implicit=true.

Deliberately not applied: the KibanaPlugin -> KibanaSecurityPlugin
rename and the shared x-pack/qa test tweak, since both are part of
elastic#148331's own surface rather than cases-focused code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aPlugin

Merging main (which now has elastic#148331, including the KibanaPlugin ->
KibanaSecurityPlugin rename) left both classes present: the stale
KibanaPlugin.java carried our Cases provider registration, while the
new KibanaSecurityPlugin.java (correctly wired into module-info.java,
the SecurityExtension SPI file, and build.gradle) only had the alerts
provider. Delete the orphan and register KibanaCasesImplicitPrivilegesProvider
alongside KibanaAlertsImplicitPrivilegesProvider on the real class.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…loop

Was being rebuilt up to three times per role block (once per owner
action) even though the underlying privileges array never changes -
StringMatcher.of constructs an Automaton for wildcard patterns, so
that cost shouldn't be paid three times over. Build it once per block
and reuse it across all three action checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelolo24
michaelolo24 marked this pull request as ready for review July 3, 2026 11:36
@michaelolo24
michaelolo24 requested a review from a team as a code owner July 3, 2026 11:36
@michaelolo24
michaelolo24 requested a review from legrego July 3, 2026 11:36
@elasticsearchmachine elasticsearchmachine added the needs:triage Requires assignment of a team area label label Jul 3, 2026
@michaelolo24 michaelolo24 added the Team:Security Meta label for security team label Jul 3, 2026
@elasticsearchmachine elasticsearchmachine removed the Team:Security Meta label for security team label Jul 3, 2026
@slobodanadamovic slobodanadamovic added >feature :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC Team:Security Meta label for security team and removed needs:triage Requires assignment of a team area label labels Jul 6, 2026
@slobodanadamovic
slobodanadamovic requested a review from a team July 6, 2026 13:58
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-security (Team:Security)

@slobodanadamovic
slobodanadamovic requested review from ebarlas and removed request for a team July 6, 2026 14:39
michaelolo24 and others added 5 commits July 6, 2026 10:58
testGetPrivilegesForApiKeyWorksIfItDoesNotHaveAssignedPrivileges hardcodes
the exact privilege set for a superuser-equivalent API key. elastic#148331 already
had to add an entry here for the alerts provider's implicit grant, since a
superuser's wildcard application privilege resolves through every
registered ImplicitPrivilegesProvider. Adding KibanaCasesImplicitPrivilegesProvider
means a second entry now appears - one .cases*/.cases-activity*/.cases-attachments*
grant with three per-owner DLS queries (cases, observability, securitySolution),
positioned before the alerts entry per the actual observed CI output.

Confirmed via repo-wide grep that no other test hardcodes this index
pattern, so this is the only assertion affected by the second provider.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the alerts provider's rewrite for the new ImplicitPrivilegesProvider
signature: getImplicitIndicesPrivileges(Collection<ResolvedApplicationPrivilege>)
replaces the old (RoleDescriptor, Collection<ApplicationPrivilegeDescriptor>)
pair. CompositeRolesStore now resolves each grant's action automaton once via
ApplicationPrivilege.get(...) - covering both the resolved-name and
raw-action-pattern paths, plus wildcard application-name expansion - and
passes the resolved ApplicationPrivilege + resources to every provider, so
providers no longer rebuild a matcher per role block.

Collapses collectResourcesByOwner to a single flat loop over the resolved
grants, testing privilege.predicate().test(action) per owner action. The
dual-track matching, the per-block StringMatcher construction, and the
now-redundant empty-applicationPrivileges fast path are all gone - that
work now lives upstream in ApplicationPrivilege.get and CompositeRolesStore.

Tests updated to build ResolvedApplicationPrivilege fixtures via a resolve()
helper (mirroring the alerts test's own helper) that calls
ApplicationPrivilege.get(...) exactly as CompositeRolesStore does, so the
existing owner-isolation, multi-owner, and wildcard test coverage exercises
the real resolution path rather than hand-rolled matching.

Verified: 24/24 unit tests, 1/1 IT test, full :x-pack:plugin:kibana:check
green, and re-ran ApiKeyRestIT.testGetPrivilegesForApiKeyWorksIfItDoesNotHaveAssignedPrivileges
(fixed earlier for the second-provider surface) to confirm the SPI refactor
doesn't change the observable query shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@ebarlas ebarlas left a comment

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.

The changes look great overall! They exactly mirror #148331, so I don't have much feedback.

One notable difference in docs/changelog/. This PR ought to have a changelog entry as well.

area: Security
issues: []
pr: 152714
summary: Contribute implicit index privileges for Kibana Cases from the `x-pack-kibana`
  plugin
type: feature


public class KibanaCasesImplicitPrivilegesProviderTests extends ESTestCase {

private static final String[] CASES_INDICES = { ".cases*", ".cases-activity*", ".cases-attachments*" };

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.

These index patterns are redundant. .cases* covers both .cases-activity* and .cases-attachments*.

static final String GET_CASE_ACTION_OBSERVABILITY = "cases:observability/getCase";
static final String GET_CASE_ACTION_CASES = "cases:cases/getCase";

static final Map<String, String> GET_CASE_ACTIONS_BY_OWNER = Map.of(

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.

This is a mapping from action -> owner.

OWNER_BY_GET_CASE_ACTION would be a clearer name

"privileges": [
"read"
],
"query": [

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.

This literal comparison is brittle, because it relies on Set<BytesReference> query ordering in GetUserPrivilegesResponse production code.

Consider using a local test utility to sort before comparing.

@SuppressWarnings("unchecked")
private static void sortIndicesQueryLists(Map<String, Object> privilegesResponse) {
    final List<Map<String, Object>> indices = (List<Map<String, Object>>) privilegesResponse.get("indices");
    for (Map<String, Object> index : indices) {
        final List<String> query = (List<String>) index.get("query");
        if (query != null) {
            query.sort(null);
        }
    }
}

- Simplify CASES_INDICES to a single ".cases*" pattern - it already
  covers ".cases-activity*" and ".cases-attachments*" since they share
  the prefix, so listing all three was redundant. Updated the provider,
  its unit tests, and the IT test's assertions (which located and
  verified the implicit grant by matching on ".cases-activity*" in the
  "names" list, no longer present now that "names" is a single entry).
- Rename GET_CASE_ACTIONS_BY_OWNER -> OWNER_BY_GET_CASE_ACTION - the map
  goes action -> owner, and the old name read the other way around.
- Fix ApiKeyRestIT's brittle literal comparison: GetUserPrivilegesResponse
  assembles the merged "query" list (and, as a superuser role with both
  providers active now exercises, the outer "indices" list itself) from
  Sets internally, so neither has a guaranteed order. Normalize both the
  actual and expected sides to a canonical order before comparing,
  rather than asserting on either - the reviewer's suggested query-list
  sort alone wasn't sufficient once the outer list's hash bucketing also
  shifted after the CASES_INDICES simplification.

Verified: 24/24 Cases unit tests, 1/1 Cases IT test, full
:x-pack:plugin:kibana:check green, and ApiKeyRestIT passing across 5
randomized iterations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelolo24
michaelolo24 requested a review from ebarlas July 7, 2026 04:43
@ebarlas

ebarlas commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

buildkite benchmark this with es-security-has-privileges-api

@infra-vault-gh-plugin-prod

infra-vault-gh-plugin-prod Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

💚 Build Succeeded

This build ran two es-security-has-privileges-api benchmarks to evaluate performance impact of this PR.

History


Set<String> spaceIds = resources.stream()
.filter(r -> r.startsWith(RESOURCE_PREFIX))
.map(r -> r.substring(RESOURCE_PREFIX.length()))

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.

One thing that's worth considering is what the behaviour should be if a role has "resources": ["space:*"] or even space:d*? As it is currently implemented it would interpret everything after space: as literal space ID. Which means that for space:* it would do a literal match on space_id=*. Should wildcards be handled in space IDs?

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.

Ah, yea, that would be problematic because in this scenario a user wouldn't have access to these indices even though they have access to all of the spaces 🤔 . How would you recommend going about checking this? A special wildcard case?

* concrete and settled by equality; a residual wildcard (e.g. {@code "kibana-*"} or
* {@code "*"} with no matching stored descriptor) is matched with an automaton.
*/
private static boolean applicationMatchesKibana(String application) {

@slobodanadamovic slobodanadamovic Jul 7, 2026 •

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.

nit: This method is same as in KibanaAlertsImplicitPrivilegesProvider. We should consider creating a shared util class to avoid duplication. The same goes for space ID parsing. It's another good candidate to refactor into its own util method. Can be done in a followup PR.

@michaelolo24
michaelolo24 merged commit 6a4b581 into elastic:main Jul 7, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>enhancement external-contributor Pull request authored by a developer outside the Elasticsearch team :Security/Authorization Roles, Privileges, DLS/FLS, RBAC/ABAC Team:Security Meta label for security team v9.5.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants