Unlocking efficient authZ with Cerbos’ Query Plan
Tags: tutorial, howto, cybersecurity, security
Published: 2023-09-06 12:48:08

By centralizing fine-grained access control, developers can now decouple authorization (authZ) from their primary business logic. Cerbos embodies this approach and delivers uniform security protocols across different services and APIs. 

Although Cerbos efficiently handles the majority of access control decisions for standard application requirements, challenges arise when constructing a list of resources accessible only to the current principal. While Cerbos APIs can manage batch requests, filtering a vast number of records becomes inefficient. This is especially true if extracting a large dataset from the source results in discarding most of the data post-filtering.

This challenge is something that Cerbos developers tackled ever since [v0.12](https://cerbos.dev/blog/authorization-aware-data-fetching-cerbosctl-improvements-and-more-cerbos-v0-12-release), with the experimental introduction of the [Query Plan API](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan). In this piece, we’ll showcase why it is such a beneficial feature.

## What is the challenge exactly?
You see, how Cerbos works is that it enables developers to transition from hardcoded authorization logic within their source code to external, human-readable policies that are auditable and version-controlled. These policies can be swiftly updated without necessitating a re-deployment of the dependent applications. As a stateless service, Cerbos operates through a straightforward API. This API evaluates provided data about the action, the principal (be it a user or a service), and the targeted resource. Based on the active policy rules, the service then returns either an ALLOW or DENY decision.

As noted earlier, while Cerbos efficiently manages most access control decisions, certain specific scenarios require a distinct approach. Let’s look at the following example.


In this day and age, social media is overwhelmingly everywhere around us, hence relatable in terms of our general understanding of how they work. Having said that, let's consider the environment of a random social media platform where users post content, and others can view and interact with these posts based on their connections, settings, and privacy rules. For the sake of this article, we can name it Cerbook.

When a user logs in to Cerbook, they are presented with a feed containing posts from their connections. However, not every post by a connection is viewable by the user due to privacy settings. For instance, some posts may be restricted to close friends, others could be set to a specific group, and some may be public. A challenge for the platform is to efficiently filter and present only those posts that the user is authorized to view, without having to fetch all posts and then filter out the unauthorized ones.

If Cerbook would check permissions for every single piece of content, for every single user, on every single homepage tap, the efficiency of such checks might get underwhelming really quickly. Even with the usage of batch requests (supported by Cerbos API), improvements would be insignificant.

Therefore the Query Plan API.

## What is the Query Plan?
`Query plan` is a cornerstone of Cerbos’ efficiency for the above-described use cases, specifically designed to process access control requests optimally, ensuring rapid and accurate authorization decisions.

Experimentally introduced back in the [v0.12](https://cerbos.dev/blog/authorization-aware-data-fetching-cerbosctl-improvements-and-more-cerbos-v0-12-release), and generally accessible since [v0.16](https://cerbos.dev/blog/unified-api-query-plan-ga-python-and-rust-sdks-cerbos-v016-release), 
The introduction of the `query plan` feature in Cerbos addresses a fundamental challenge: as applications grow and the number of policies increases, the complexity and overhead of processing access control requests can escalate. Traditional authorization checks can become bottlenecks, especially when they are not optimized for large sets of diverse policies. The `query plan` acts as an optimizer, strategically evaluating which policies need to be checked, in what order, and how to do so most efficiently. By preemptively determining the best route to a decision, it minimizes unnecessary evaluations, reduces latency, and ensures that the system remains agile even as policy complexity grows. The end goal is clear: maintain high performance and rapid response times in authorization, irrespective of the application's scale or the granularity of its policies.

## Solving the challenge
In the case of Cerbook, the policy can be defined such that a user can view a post if the:
a. post is public.
b. post is set to `close_friends` and the user's relationship with the poster is `close`.
c. post is set for a specific group and the user is part of that group.

The above could be represented as the following Cerbos policy:

```yml
        actions: ["view"]
        roles: ["connected_user"]
        effect: EFFECT_ALLOW
        condition:
            match:
                any:
                    of:
                        - expr: request.resource.attr.privacy == "public"
                        - all:
                            of:
                                - expr: request.resource.attr.privacy == "close_friends"
                                - expr: request.principal.attr.friendship_level == "close"
                        - all:
                             of:
                                - expr: request.resource.attr.privacy == "group"
                                - expr: request.principal.attr.group_id in request.resource.attr.allowed_groups
```

When a user logs in and/or accesses their feed, Cerbook knows the user's attributes (like their friendship levels and group memberships). In order to efficiently parse through all the potential content, Cerbook sends a [request](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan) to the Cerbos query planner API with:
Principal information: The current user's attributes (friendship_level, group_id, etc.)
- Action: "view"
- Resource kind: "post"
- Any known attributes of the posts, if relevant.

The Cerbos API then evaluates the policy rules and responds with a CONDITIONAL filter based on the user's attributes, effectively translating the complex policy rules into a more easily digestible filter for the application to apply to the data fetch.

For instance, if the user has a friendship_level of "close" and is a part of group "123", the returned filter might be:
- post.privacy is "public"
- OR post.privacy is "close_friends"
- OR (post.privacy is "group" and post.allowed_groups includes "123")

This filter can then be directly applied to the data query, ensuring that only relevant, authorized posts are fetched and displayed to the user, making the whole process much more efficient and aligned with privacy settings.

Now, based on the defined policies and given inputs, it can pre-determine a majority of the conditions, ensuring that only the most critical constraints are relayed to the data retrieval layer. The challenge that remains is translating this set of conditions into a relevant WHERE clause or filter. 

To simplify this process, Cerbos developers have come up with adapters for popular Object-Relational Mapping (ORM) frameworks. You can check out for more details on the [query plan repo](https://github.com/cerbos/query-plan-adapters) - which also contains adapters for [Prisma](https://www.prisma.io/) and [SQLAlchemy](https://www.sqlalchemy.org/) - as well as a [fully functioning application](https://github.com/cerbos/express-mongoose-cerbos/) using [Mongoose](https://mongoosejs.com/) as its ORM.

## Conclusion
While Cerbos adeptly manages most access control decisions for typical application needs, difficulties emerge when creating a list of resources available exclusively to the current user or principal. Even though Cerbos' APIs are capable of handling batch requests, sifting through a large volume of records is not always efficient. This inefficiency is particularly noticeable when a significant portion of a retrieved dataset needs to be discarded after filtering.

An answer to that challenge is the Query Plan API. Initially introduced as an experimental feature in Cerbos [v0.12](https://cerbos.dev/blog/authorization-aware-data-fetching-cerbosctl-improvements-and-more-cerbos-v0-12-release), and upgraded to a stable status by the [v0.16](https://cerbos.dev/blog/unified-api-query-plan-ga-python-and-rust-sdks-cerbos-v016-release) release. During the period between these versions, significant enhancements were made to its internal logic, and it kept evolving till today. This evolvement ensures that developers can effectively manage access control while focusing on core application features.