Jonathan Janisch
07/01/2024, 12:45 AMAcme
┌── ───────┐
▼ ▼
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!Sam Lock (Cerbos)
07/01/2024, 11:12 AMSo a policy file such asisresource/acme/internal/sales/apac/user.yaml
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?Jonathan Janisch
07/01/2024, 2:26 PMIn 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?
Sam Lock (Cerbos)
07/01/2024, 3:45 PM# 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:
# 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
- ...
Jonathan Janisch
07/01/2024, 5:24 PMSam Lock (Cerbos)
07/02/2024, 6:53 AMJonathan Janisch
07/02/2024, 6:34 PMSam Lock (Cerbos)
07/03/2024, 9:10 AMAND
the results to ensure both (or all) checks passed, though. Not ideal, but perhaps slightly more optimal for now 🤷♂️