Sorry for the long post. I've been struggling a bi...
# help
j
Sorry for the long post. I've been struggling a bit to think of how to model policies. I think it's because I would intuitively think to model this hierarchy:
Copy code
Acme           
       ┌──  ───────┐    
       ▼           ▼    
    Internal   Customers
┌───   │           │    
▼      ▼           ▼    
Eng  Sales   WileECoyote
  ┌─── │ ────┐          
  ▼    ▼     │          
AMER  EMEA  APAC
With a scope like
acme.internal.sales.apac
And I would intuitively think some resource policy at that scope should only be permitted if and only if each "node" when AND'ed together permitted the request. In fact, this is essentially what I'm doing in our apps' API currently without Cerbos. But this is not how Cerbos works. Cerbos evaluates scopes starting from the most specific to least specific and the first match wins. So a policy file such as
resource/acme/internal/sales/apac/user.yaml
is extremely dangerous as this policy could easily have an EFFECT_ALLOW that basically ignored and bypassed all parent levels (e.g. allowing a principal that is not internal to delete a user). Of course we can create unit tests, but now your unit test for an APAC resource would have to have all permutations of the hierarchy in order to ensure it doesn't accidentally grant access. This scope modeling only seems to work if your leaf nodes (more specific nodes) only have EFFECT_DENY defined. I can see that's how the multi-tenant sass recipe was done here: https://github.com/cerbos/demo-multitenant-saas/blob/main/policies/resource/regional/purchase_order.yaml where purchase orders are denied if the resource is NOT in the principal's allowed regions. But it seems to go against how authz should be defined in that everything should be DENY by default unless explicitly allowed. But here we have to model our policies as ALLOWing unless explicitly DENIED. Also, in the github example, it seems we would have to repeat this regional check on all resources which seems error prone. Our app doesn't have such complex needs currently as we're about to launch our initial MVP, but the other products at our org do and we will surely have such requirements in the future. So this isn't really a call for assistance as much as just a stream of thoughts. I guess my big fear is let's say we're 3 years in with all small volume clients and then we land a big client in which the complexity causes us to remodel all of our existing clients' policies!
s
Hi Jonathan. Stream of consciousness' are always welcome, thanks for sharing. It sounds to me that your concerns are largely around the potential for making mistakes when authoring policies? Regardless of that, though, I wonder if there might be an alternative way to think about modelling the policies (and bear with me, I might not understand your original point). On this:
So a policy file such as
resource/acme/internal/sales/apac/user.yaml
is
extremely dangerous as this policy could easily have an EFFECT_ALLOW that basically ignored and bypassed all parent levels (e.g. allowing a principal that is not internal to delete a user).
In which scenarios would you require additional access rules that wouldn't already be evaluated in the pre-existing parent scopes? If (say) we're writing rules specific to internal users, could we not write those rules within the policy scoped with
acme.internal
? You could infer department/region/user etc as runtime attributes, and use them within conditions in the rules in that policy? In other words, as a general rule-of-thumb, we should define rules as high up the hierarchy chain as is possible. Scope hierarchies are more for the case where we want to explicitly
ALLOW
an action on a resource which is more granularly relevant to that particular scope, rather than as an override for a common action. In other words, don't reason about it as you would inheritance within an object-oriented language, rather, a superset or extension of the existing rules. By default, when evaluating, any unmatched
actions
are issued a
DENY
(even if they are evaluated against more layers in the scope hierarchy before this is reached). Does this make any sense or was I wildly off the mark?
j
In which scenarios would you require additional access rules that wouldn't already be evaluated in the pre-existing parent scopes?
I think the catch is the parent scopes aren't evaluated if an EFFECT_ALLOW is already returned in a child scope. But thanks I think your response added some clarity (especially the bit about not thinking in OO terms). I keep thinking of "internal" as some sort of node that can have rules that apply to all resources. But I need to drop that mindset. It does seem like we need to repeat a ton of conditions in each resource (and test for them). In particular the tenant check is a bit worrying. It would be nice if by default the tenant check is done for all requests and cross-cutting concerns are handled as exceptions. The examples typically show a single derived role "customer" that handles the tenant association and no further condition is needed in each resource. But our clients typically have ~30 roles on average that are unique to that client. So that approach doesn't seem to work for us. e.g. seems weird to create 30 derived roles. Or do we create a single derived role and add a condition for every action?
s
Yeah, derived roles work nicely in the case where we want to infer a 1:1 mapping (to a tenant or otherwise). When we're talking about defining rules with different outcome per tenant-specific roles, it get's a bit more fiddly. > It would be nice if by default the tenant check is done for all requests and cross-cutting concerns are handled as exceptions. > But our clients typically have ~30 roles on average that are unique to that client. I wonder if we could solve the tenant check with variables. There would still be some repetition (you'd declare the condition in each scoped policy rule), but it'd be considerably more streamlined. Something like:
Copy code
# export_variables.yaml
---
apiVersion: api.cerbos.dev/v1
exportVariables:
  name: foobar
  definitions:
    isTenant: R.attr.tenant_id == P.attr.tenant_id
Then in the tenant scoped resource policies:
Copy code
# scoped_resource_policy.yaml
---
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: some_resource
  scope: "acme.internal.sales.apac.user"
  variables:
    import:
      - foobar
  rules:
    - actions: ["create"]
      effect: EFFECT_ALLOW
      roles: ["user"]
      condition:
        match:
          all:
            of:
              - expr: V.isTenant
              # Other tenant specific conditions
              - ...
j
Thanks Sam. That was what I thinking to do as well. But it feels wrong and error prone. We'll have ~70 resources now for our MVP but far more as we develop the full product. And then assuming 2-3 rules per resource that's hundreds of duplicate condition checks. I suppose in this case it actually seems like it would be easier to create the 30 derived roles. I think this is why I was struggling with the mental model and trying to fit it into more of an OO model 🙂
s
Understood. I've made a note of this conversation -- there's potential for new functionality in the near future which might impact how this could be implemented, but it's slightly too early to offer particulars as the goalposts are still moving. I'll let you know when it solidifies!
j
Sounds good. The workaround for us for our MVP would be to do the tenant association verification in our app as we're currently doing anyway. This way we don't run into that issue where we need N derived roles for now. It will definitely be an issue in the future. Our product is loan servicing and there's some orgs that do sub-servicing (e.g. one parent org does the servicing for 200-300 companies). So we'll have tenant and sub-tenants with regional concerns and we'll likely be back at square one where we need numerous derived roles or condition checks per resource. Looking forward to see what's in store! Trying to make this work for us as I prefer the way Cerbos works over competitors that have that complex data plane that needs to constantly sync.
👍 1
s
Another potential workaround could be to include the tenant association check (by default) with other specific checks in each request to Cerbos? It could be batched up in the list of resources passed to the CheckResources API (using the tenant-wide scope). You'd need to
AND
the results to ensure both (or all) checks passed, though. Not ideal, but perhaps slightly more optimal for now 🤷‍♂️
👍 1