Hello all! I am trying to wrap my head around a sc...
# help
n
Hello all! I am trying to wrap my head around a scenario with cerbos and would love some help! Everything I've seen with the docs and the API of cerbos treats authorization requests as a principal trying to act on specific/known resource(s) (the resource IDs must be known and sent to cerbos when checking). My application has scenarios that don't nicely fit with that and I am wondering what I am missing. For example: • There is a page in the app that displays all *resource_x*'s for a user to browse and manage. Only a principal with the role_x may view it. How might I write a cerbos policy that implements this authz check? Am I required to fetch a list of all resource_x IDs to send along to cerbos? What if there are thousands of the resource? That seems like unnecessary overhead. • I am finding the majority of the authorization checks our app is needing to make are not for a principal accessing a specific resource (by its ID), but for a principal looking to access some collection of a resource (some hundreds or thousands of some resource type) Any help or ideas would be greatly appreciated! Thank you 🙂
a
Hey @Nabil - you have run into the big shift when moving to decoupled authorization, handling the listing/filtering instances of a resource. The Cerbos approach to this is a partial evaluation of policy using what we call a Query Plan. This is a secondary endpoint which returns a set of filters that need to be met to return just the instances of a resource that a given principal would have permissions to. You read more about this here https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan
We have prebuilt adapters for Prisma and SQLAlcahmey ORMs if you are using either of those otherwise the filter response is an AST which can be easily adapted into which ever storage engine you are using.
n
Hey Alex! Thanks for the quick response. This is very interesting.. definitely a big shift in how I'm used to approaching this! I am working with RAW SQL query strings in my apps and APIs, so if time permits I might try to co-opt what you have from your other libraries into one that suits our needs. I'm curious if this approach of returning filters is going to be the long term strategy for cerbos handling the many-resource scenario? It seems like to pull authorization out into cerbos at this level, the database schema will need to be designed and setup to facilitate it in a manageable way. I'm not sure what that will look like on our side yet. I'm curious if you and the team have given thought to that?
a
We believe this model is actually better suited for the longer term with many resource types and complex authz requirements, relying just on hard coded queries will require a developer to go and change the logic every time there is a change in the authz rules - in every part of the app that fetches data - this isn’t scalable, especially if the authz is managed by different teams. We are yet to run into a situation where the DB schema has been an issue when it comes to applying filter so would be curious to see where you see a potential problem in your setup so we can work to solve it.
n
Thanks Alex. I will share my learnings as I move forward with prototyping
Hey Alex. I have a more specific question related to this that I'm hoping you might have an answer for... I have been looking at your example app that uses query plans and the docs you sent above and I think I understand it decently. I'm trying to use the example app as inspiration for what I am trying to do within our app but I'm still not able to figure out how I can do what I want in cerbos. I will try to explain the situation below: In the example app, you have a resource called
contact
which has an owner, department, etc. The
/contacts
endpoint uses a query plan to build a filter for grabbing contacts for a user. What would you change about your cerbos policy for a
contact
if a
contact
didn't have a singular
ownerId
but instead had a list of user ids that had ownership (or access) to it?
References: • https://github.com/cerbos/express-prisma-cerbos/blob/main/cerbos/policies/common_roles.yamlhttps://github.com/cerbos/express-prisma-cerbos/blob/main/src/index.ts#L75
The actual scenario I am looking to implement in my company's app is that of many resources have many users have access to them. I have previously implemented this with join/pivot tables that map
userId
to
resourceId
and then I join against that table for every single query by a user for some list of resources. This is very painful and what cerbos looks to assist with using query plans that return filters. I hope this makes sense. Thank you for your time and consideration!
I have this scenario working using prisma but it relies on join tables and using prisma's some filter, which doesn't seem to be supported by the
@cerbos/orm-prisma
adapter
I guess more generally in data modelling language, I need I want cerbos to return to me a filter for resources with a 1-to-many and many-to-many relationship
I seem to have a way to implement this using cerbos. My solution is to treat the join/pivot table as a resource and then define a cerbos policy for it. Then my queries can use that filter, but I have query that table directly (in prisma) and do mapping after the fact to get to the data I actually want. An example: • There is a resource (and db table) called
tenant
that has an id, name, and a list of users who have access to that tenant. • I represent that list of users with another resource (and db table) called
tenant_user
which has a _tenant_id_ and _user_id_. The _tenant_id_ is a foreign key back into the
tenant
table. • I define a cerbos policy for the
tenant_user
resource with a rule like this:
Copy code
- actions:
        - read
      effect: EFFECT_ALLOW
      roles:
        - user
      condition:
        match:
          expr: request.resource.attr.user_id == request.principal.id
This results in an AST that I can translate into a WHERE clause (using your
@cerbos/orm-prisma
module in this case) and attach to my query. This situation is kinda hitting on my concern from my initial question in this thread. The database schema's design seems like it will invariably end up being coupled to how the app uses cerbos. Let's say I need to change what it means for a user to have access to a tenant sometime in the future: i.e. a tenant can be either enabled or disabled. I need to add this data (_is_enabled_) to my data schema in order to implement this new requirement. My first inclination is to add it to the
tenant
table since thats what the data is associated with. I now need to implement authz to allow only certain users to access tenants that are disabled. If I am thinking about this right, I would need to add an additional cerbos policy check for the resource
tenant
where I could specify that only a user with a certain role or whatever can access tenants that are disabled. I'd then need to combine the filters from these two cerbos query plan checks into the query I ultimately send to my database. The case above ^ seems to me like I smearing my authz logic around across my database, my app and then cerbos. If a third requirement came in that required another cerbos policy to be created, I'm not sure what advantages it'd be providing me at that point. The crux of my confusion is stemming from specifying authorization policies for resources that have relationships, particularly one-to-many and many-to-many.
I know this is a giant wall of text and I am sorry about that lol I tried to state my situation as concisely as I can... Your help here would be great because this use case seems to be one of the most common situations in our applications and I am on the hook for deciding whether or not to move forward with cerbos or not. So thank you for bearing with me!
a
Hey apologies been AFK today. Will look at this shortly and get back to you.
I’ll try to tackle each of your points in turn:
What would you change about your cerbos policy for a
contact
if a
contact
didn’t have a singular
ownerId
but instead had a list of user ids that had ownership (or access) to it?
Copy code
---
apiVersion: "api.cerbos.dev/v1"
description: |-
  Common dynamic roles used within the app
derivedRoles:
  name: common_roles
  definitions:
    - name: owner
      parentRoles: ["user"]
      condition:
        match:
          expr: >
            request.principal.id in request.resource.attr.ownerIds
If I am thinking about this right, I would need to add an additional cerbos policy check for the resource
tenant
where I could specify that only a user with a certain role or whatever can access tenants that are disabled. I’d then need to combine the filters from these two cerbos query plan checks into the query I ultimately send to my database.
On this point, Cerbos does as much upfront evaluation as it can based on the context that is in the request. If the policy were to look something like this: https://play.cerbos.dev/p/hh9bOeOwW63238213E0Bs90WCOFWf6R9
Copy code
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: default
  resource: tenant
  rules:

    - actions:
        - "view"
      effect: EFFECT_ALLOW
      roles:
        - ADMIN

    - actions:
        - "view"
      effect: EFFECT_ALLOW
      roles:
        - USER
      condition:
        match:
          all:
            of:
              - expr: request.resource.attr.enabled == true
              - expr: >
                  request.principal.id in request.resource.attr.ownerIds
If cerbos were to get a request with the principal having the role of ADMIN, then it will return a query plan as always allow response - thus not requiring any additional conditions in your query If cerbos were to get a request with the principal having the role of USER then it would return the query plan with conditions for checking that the enabled attribute of the tenant is true and the requests principal id is in the list of the owners for the tenantID like so:
Copy code
{
  "requestId": "query-plan",
  "action": "view",
  "resourceKind": "tenant",
  "filter": {
    "kind": "KIND_CONDITIONAL",
    "condition": {
      "expression": {
        "operator": "and",
        "operands": [
          {
            "expression": {
              "operator": "eq",
              "operands": [
                {
                  "variable": "request.resource.attr.enabled"
                },
                {
                  "value": true
                }
              ]
            }
          },
          {
            "expression": {
              "operator": "in",
              "operands": [
                {
                  "value": "user1"
                },
                {
                  "variable": "request.resource.attr.ownerIds"
                }
              ]
            }
          }
        ]
      }
    }
  }
}
There doesn’t have to a be a 1:1 mapping between the Cerbos attributes and the DB schema - and infact some implementations have put a layer between the app DB and Cerbos to handle the aggregation of the metadata across different microservices, it all comes down to how your system is designed. The prisma adapter not supporting some is an oversight, do you have an idea of how you would like to see it implemented? One way this could be done today is letting the adapater map the ‘nested’ fields into a some temporary value, and then before passing it into the prisma where call, remap it how you need. not ideal, but i think will work until we can come up with a neater design without the adapter needing to be aware of the full schema
n
Hey Alex! No worries at all please take your time, I appreciate you taking the time to assist. The example policy and query plan you send make sense, but the issue for me is still that I would be limited to the number of resources I could check at a time. I will be working with resources in the hundreds or thousands and cannot send those resource ids to cerbos for every request. As for the prisma adpater supporting `some`: I don't have any good ideas 😅 your idea of mapping nested fields into something accessible for our code using prisma is about as much as I can imagine. We want a structure like this ultimately:
Copy code
tenant_users: {
    some: {
        user_id: user.sub
    }
}
So maybe if in the
fieldNameRemapper
we could specify the nesting structure? Something like this:
Copy code
const filters = queryPlanToPrisma({
        queryPlan: tenantsQueryPlan,
        fieldNameMapper: {
            'request.resource.attr.user_id': 'tenant_users:some:user_id' // Some means of specifying the nesting
        }
    });
some
is one of three available filters for nested -to-many queries. There is `some`, `every`, and `none`.
a
Hey @Nabil having a think through this currently. Have you managed to make progress on yourself?
n
Cool yeah I have moved on to considering other scenario's authz implementation (a REST API in this this case) and seeing how cerbos might fit in there
a
If you want, I’m happy to jump on a call and go through your use case in more detail to help accelerate your process - use the same workshop link as before https://go.cerbos.io/workshop