惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
GbyAI
GbyAI
S
SegmentFault 最新的问题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
Visual Studio Blog
WordPress大学
WordPress大学
Hugging Face - Blog
Hugging Face - Blog
B
Blog
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
雷峰网
雷峰网
爱范儿
爱范儿
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
U
Unit 42
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
Jina AI
Jina AI
P
Proofpoint News Feed
A
About on SuperTechFans
I
InfoQ
F
Fortinet All Blogs
L
LangChain Blog
T
Tailwind CSS Blog

Cerbos - All Posts

Authentik vs Keycloak: Self-hosted IdP comparison Mapping business requirements to authorization policy for automotive Fine-grained authorization for AI gateways EIC 2026: Stop counting agents, protect what they can touch Agent skill for writing authorization policies in Claude Desktop Identity security in 2026 EIC 2026 takeaways: the identity stack built for humans will not hold up for AI agents Already have authentication? Here's the authorization layer you still need. Tokens are authorization decisions: a guide to policy-driven token issuance What is a Runtime Authorization Platform It's a dimmer switch, not a kill switch. How CISOs are rethinking AI agent governance From maps to bitmaps (and from bitmaps to bitmaps) AuthZEN, Shared Signals, SCIM Events, IPSIE: Notes from the OpenID Enterprise Panel How do you update authorization policies without redeploying your application? IIW42 recap: Where agent authorization got real Cerbos PDP v0.52.0/v0.53.0: Engine performance, security hardening, and CEL path functions Authorization Management Platforms: what they do, how they work, and where they fit PocketOS AI coding agent deleted a production database in 9 seconds Non-Human Identity management still has a blind spot Supabase alternative in 2026: Best open source auth options Benefits of on-premise authorization: Why enterprises are moving toward self-hosted Authorization policies: How to write, test, and validate them (faster with AI) Agent skill for writing authorization policies How much does it cost to build authorization in-house? Why centralized authorization governance reduces incident response time OPA alternative Why AI agents make authorization a right now problem Modernizing legacy application authorization: why it’s your biggest security blind spot How to add authorization to legacy applications without code changes 5 authorization blind spots auditors find, and how to fix them
Query plan adapter for Elasticsearch (Java)
Alex Olivier · 2026-02-20 · via Cerbos - All Posts

Externalized authorization separates access control logic from application code. Instead of scattering if statements and role checks across your codebase, you define policies centrally and query a decision engine at runtime. Cerbos is purpose-built for this: your policies reside as code in version control, and applications call Cerbos to determine whether a given principal can perform a specific action on a particular resource.

This works well for individual access checks, but most applications also need to answer a different question: "which resources can this user see?" The naive approach -- fetch everything, then call checkResources on each row -- does not scale. You end up reading data that the user will never be allowed to see, wasting database I/O and application memory.

Cerbos solves this with the PlanResources API. Instead of evaluating a specific resource instance, PlanResources uses partial evaluation to analyze your policies and return a query plan - an abstract syntax tree (AST) that describes the conditions under which access is granted. The plan contains the same operators and attribute references that your policies use, but structured as a tree that can be mechanically translated into any query language. The result is one of three outcomes:

  • Always allowed: the user can access every resource of this type, no filter needed.
  • Always denied: the user has no access at all, return an empty set.
  • Conditional: Cerbos returns an expression tree. Translate it into your database's filter syntax and let the database do the work.

The conditional case is where query plan adapters come in. They walk the AST, map Cerbos attribute paths to your schema's field names, and produce a native filter that your database client can execute directly. Authorization logic stays in your policies, and the database applies it at the query layer -- aligned with indexes and query optimization, not burning cycles in application code.

Today, we are releasing a query plan adapter for Elasticsearch.

Why Elasticsearch?

Elasticsearch is the backbone of search and analytics infrastructure across industries. When applications need to search, filter, and aggregate large volumes of data, Elasticsearch provides the query engine. However, authorization in Elasticsearch is typically handled at the application layer - fetching results and then filtering in code - or through Elasticsearch's own document-level security, which couples access rules to the cluster configuration rather than relying on application-level policies.

The adapter bridges this gap. Authorization policies stay in Cerbos, and the adapter translates them into native Elasticsearch Query DSL that runs inside the cluster, leveraging indexes, filter caching, and query optimization. No post-fetch filtering in application code.

How it works

ElasticsearchQueryPlanAdapter.toElasticsearchQuery takes a Cerbos PlanResourcesResult and a field map that associates Cerbos attribute paths with Elasticsearch field names. It walks the expression tree and returns a Map<String, Object> that serializes directly to Elasticsearch Query DSL JSON.

import dev.cerbos.queryplan.elasticsearch.ElasticsearchQueryPlanAdapter;
import dev.cerbos.queryplan.elasticsearch.ElasticsearchQueryPlanAdapter.Result;

Map<String, String> fieldMap = Map.of(
    "request.resource.attr.status", "status",
    "request.resource.attr.department", "department",
    "request.resource.attr.ownerId", "ownerId"
);

PlanResourcesResult plan = cerbos.plan(
    Principal.newInstance("user1", "USER"),
    Resource.newInstance("document"),
    "view"
);

Result result = ElasticsearchQueryPlanAdapter.toElasticsearchQuery(plan, fieldMap);

switch (result) {
    case Result.AlwaysAllowed allowed -> {
        // Search without authorization filters
    }
    case Result.AlwaysDenied denied -> {
        // Return empty results
    }
    case Result.Conditional conditional -> {
        // Use conditional.query() in a bool.filter clause
        String json = objectMapper.writeValueAsString(
            Map.of("query", Map.of(
                "bool", Map.of("filter", List.of(conditional.query()))
            ))
        );
    }
}

Because the adapter returns a sealed Result type, the compiler enforces that every case is handled. The conditional branch produces a standard Elasticsearch query map that slots into a bool.filter clause -- keeping authorization out of relevance scoring so Elasticsearch can cache the filter and skip scoring overhead.

Nested object support

Real-world Elasticsearch schemas often contain arrays of nested objects -- tags with metadata, categories with subcategories, permissions with scopes. Cerbos policies can express conditions over these structures using collection operators like exists and all. The adapter translates these into Elasticsearch nested queries automatically.

Pass a Set<String> of field names that use Elasticsearch's nested mapping type:

Map<String, String> fieldMap = Map.of(
    "request.resource.attr.status", "status",
    "request.resource.attr.tags", "tags"
);

Set<String> nestedPaths = Set.of("tags");

Result result = ElasticsearchQueryPlanAdapter.toElasticsearchQuery(
    plan, fieldMap, nestedPaths
);

The adapter resolves lambda variables inside collection operators to the correct nested field paths. A policy condition like R.attr.tags.exists(tag, tag.name == "public") becomes:

{
  "nested": {
    "path": "tags",
    "query": {
      "term": { "tags.name": { "value": "public" } }
    }
  }
}

The all operator uses a double-negation pattern -- "there is no nested document that fails the condition" -- which Elasticsearch evaluates efficiently:

{
  "bool": {
    "must_not": [{
      "nested": {
        "path": "tags",
        "query": {
          "bool": {
            "must_not": [{ "term": { "tags.name": { "value": "public" } } }]
          }
        }
      }
    }]
  }
}

hasIntersection combined with map projects a field from nested objects and checks for overlap with a value list, producing a nested + terms query.

Full example

Consider a policy that allows users to view published documents, documents they own, or documents tagged with a category they have access to:

# policies/document.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: document
  version: default
  rules:
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["USER"]
      condition:
        match:
          any:
            of:
              - expr: request.resource.attr.status == "published"
              - expr: request.resource.attr.ownerId == request.principal.id
              - expr: >
                  request.resource.attr.tags.exists(tag,
                    tag.category == "department" && tag.value == request.principal.attr.department)

With the adapter:

import dev.cerbos.queryplan.elasticsearch.ElasticsearchQueryPlanAdapter;
import dev.cerbos.queryplan.elasticsearch.ElasticsearchQueryPlanAdapter.Result;

Map<String, String> fieldMap = Map.of(
    "request.resource.attr.status", "status",
    "request.resource.attr.ownerId", "ownerId",
    "request.resource.attr.tags", "tags"
);
Set<String> nestedPaths = Set.of("tags");

PlanResourcesResult plan = cerbos.plan(
    Principal.newInstance("alice", "USER")
        .withAttribute("department", "engineering"),
    Resource.newInstance("document"),
    "view"
);

Result result = ElasticsearchQueryPlanAdapter.toElasticsearchQuery(
    plan, fieldMap, nestedPaths
);

List<Document> documents = switch (result) {
    case Result.AlwaysAllowed ignored -> {
        yield esClient.search(s -> s.index("documents"), Document.class)
            .hits().hits().stream().map(Hit::source).toList();
    }
    case Result.AlwaysDenied ignored -> Collections.emptyList();
    case Result.Conditional conditional -> {
        // Combine authorization filter with a user search query
        String queryJson = objectMapper.writeValueAsString(
            Map.of("query", Map.of(
                "bool", Map.of(
                    "must", List.of(
                        Map.of("match", Map.of("title", userSearchTerm))
                    ),
                    "filter", List.of(conditional.query())
                )
            ))
        );
        yield esClient.search(
            s -> s.index("documents").withJson(new StringReader(queryJson)),
            Document.class
        ).hits().hits().stream().map(Hit::source).toList();
    }
};

The adapter produces a bool.should clause combining the three policy conditions -- a term query for status, a term query for owner, and a nested query for the tag match. Placed in bool.filter, Elasticsearch evaluates the authorization filter without scoring, caches it, and applies the user's relevance query separately in bool.must.

Custom operator overrides

The adapter ships with sensible defaults for every operator Cerbos can emit, but Elasticsearch schemas vary. If your string fields use text type instead of keyword, override the eq operator to use match instead of term:

Map<String, OperatorFunction> overrides = Map.of(
    "eq", (field, value) -> Map.of("match", Map.of(field, value)),
    "contains", (field, value) -> Map.of("match_phrase", Map.of(field, value))
);

Result result = ElasticsearchQueryPlanAdapter.toElasticsearchQuery(
    plan, fieldMap, overrides
);

Supported operators

The adapter covers the full range of operators that Cerbos can emit in a query plan:

  • Logical: and, or, not
  • Comparison: eq, ne, lt, gt, le, ge, in
  • String: contains, startsWith, endsWith
  • Existence: isSet, null checks (eq/ne with null)
  • Arrays: hasIntersection, size comparisons
  • Collections: exists, all, hasIntersection + map (via nested queries)

Get started

Copy the source files directly into your Java project from GitHub. The adapter has two dependencies: cerbos-sdk-java and protobuf-java. Full documentation, integration examples, and the test suite are in the repository. If you have questions or run into issues, join the Cerbos community Slack.