authzed / spicedb

Open Source, Google Zanzibar-inspired database for scalably storing and querying fine-grained authorization data
https://authzed.com/docs
Apache License 2.0
5.1k stars 277 forks source link

Proposal: Caveated relationships #386

Closed jakedt closed 1 year ago

jakedt commented 2 years ago

Goal

Let people handle their abac-y style needs in a Zanzibar first way.

Use Cases

Proposal

Allow small fragments of policy to be associated with individual relationships in a new field called “caveats”. As we attempt to evaluate permissions these pieces of policy will be combined and surfaced as immutable caveats that apply to the result sets as they are collected. Before the result is returned to the user, a final policy is assembled and evaluated against user-supplied attributes, and a final decision is made.

Because the caveats are immutable and apply to the sub-problem, they can be cached at every level of the decision making process.

Examples

Union Caveat

Schema and Relationships

definition user {}

definition document {
   relation writer: user
   relation reader: user

   permission edit = writer
   permission view = reader + edit
}
document:firstdoc#writer@user:tom[day($date) == Monday]
document:firstdoc#reader@user:tom

Resolution

Exclusion Caveat

Schema and Relationships

definition user {}

definition document {
   relation banned: user
   relation reader: user

   permission view = reader - banned
}
document:firstdoc#banned@user:tom[day($date) == Monday]
document:firstdoc#reader@user:tom
document:firstdoc#reader@user:jerry

Resolution

Intersection Caveat

Schema and Relationships

definition user {}

definition document {
   relation billing_enabled: user
   relation reader: user

   permission view = reader & billing_enabled
}
document:firstdoc#reader@user:tom[day($date) != Saturday, day($date) != Sunday]
document:firstdoc#billing_enabled@user:tom

Resolution

Open Questions

Previous Work

aka-emi commented 2 years ago

Adding an example of such policy:

  1. Given object Type:1 and subject Type:2
  2. Object has the permission to tag subject iff object was created after subject
derwolfe commented 2 years ago

This would be extremely useful for my use-cases as my current authorization system has identities with multiple attributes (e.g. app name, a few stable attributes like region, etc.)

I am curious about a few things:

  1. how many caveats could a given membership have?
  2. how would these caveats be passed to a given permission check? would this be new api surface to pass some arbitrary collection of KV pairs?
aboucher51 commented 2 years ago

many of my use cases involve a situation where I need to give contextually specific permission, where a specific user X can send specific object Y only to specific destination Z

enriquedacostacambio commented 2 years ago

One of the other zanzibar-like implementations seems to be addressing this by taking in "contextual tuples" in the "check permission" endpoint, which means the check behaves as if those relationships existed in the graph. I don't like that approach because it puts back authz logic in the check invoker, i think yours is better, however, I wonder if the comparison language proposed here will be expressive enough to handle all possible needs. I think this downside would be mitigated if the available comparison functions are pluggable.

derwolfe commented 2 years ago

Here is a bit more detail on the use-cases I'm looking to support:

Use-case 1: multiple/optional attributes

The requestor would supply an app identity with

Application {
    Name: app,
    Stack: foo,
    Detail: bar,
    Instance-id: i-12358,
    Region: us-east-1,
    Executor: sys2,
    ExtendedAttributes: {“k1”: “123”, “k2”: “bar”, “k3”: “6790”}
}

We’d write a relation to spice similar to the following:

idstore/group:authsvc#member@spin/caveated_app:app[detail="bar" stack="foo"]

When a requestor makes a permission check, in this instance checking for #member in authsvc, spice would build a path to the app and then check for a relationship with matching caveats. Unspecified caveats would be treated as a match. A relation with a complete match would result in an allow.

Use-case 2: complex attributes

We do have certain cases where more complex attributes can be present, specifically within an application’s extended attributes. This is an untyped json blob; though currently all properties are string-ly typed.

In addition to simple attribute matching demonstrated in use-case 1, we’d also want to match on subsets of these complex attributes

idstore/group:authsvc#member@spin/caveated_app:app[detail="bar" stack="foo" extended_attributes=contains({“k1”: “123”, “k2”: “bar”})]

Again similar to use-case 1 the entire app identity would be supplied as part of the permission check and caveats would be required to match when specified.

vroldanbet commented 2 years ago

Hey folks! 👋🏻 We are starting to work on caveats and we would love to hear more real-life use-cases that will help us validate the implementation meets your needs. Feel free to share them here!

kie-ra commented 2 years ago

We have the following use case! A health insurance member might be a dependent on someone else's plan -- that person being the subscriber. We want a subscriber to be able to see all of their dependent's claims if the dependent is under 12 years old, and SOME of their dependent's claims (the ones not marked "sensitive") if they're between 12 and 18.

arashpayan commented 2 years ago

I also have a use case that has been popping up a lot.

If a resource is no longer mutable after some condition has occurred, that resource should not be editable by anyone. Some examples that I've encountered so far:

toddkazakov commented 2 years ago

At Tidepool, for compliance reasons, we have the need to support time delineated data sharing. We store tie series data from personal health devices (e.g. continuous glucose meters) and we allow our users to share their data with clinics to receive treatment. However, clinics must be able access data that they had access to even if the patient revokes the sharing relationship, because the data was used for providing treatment. So each sharing relationship needs to have a start_date attribute and optional end_date. When a patient revokes the sharing relationship we would need to set the end_date attribute and enforce access to data based on those attributes.

I believe our use case will be supported by the initial implementation but I thought it would be useful to share it anyway.

josephschorr commented 1 year ago

Remaining work:

lloydfischer commented 1 year ago

I'm trying to understand how caveats can help with the following use case. { caveat need_mfa (mfa:bool) { mfa == true }

caveat dont_need_mfa(mfa: bool) { true }

definition user{}

definition role { relation viewer: user }

definition resource { relation viewer: role with dont_need_mfa }

definition secure_resource { relation viewer: role with need_mfa } }

tuples with imaginary syntax

secure_resource:res1[need_mfa]#viewer@user:user1 resource:res2[dont_need_mfa]#viewer@user:user1

assertion

secure_resource:res1#viewer@user:user1 with {mfa:false} -> gives false resource:res2#viewer@user:user1 with {mfa:false} gives true

In other words the runtime context is on the user->role relation but the caveat is on the role->resource relation. The context is propagated down to the caveat.

The search through a path to join the object and subject seems fundamental to AuthZed and it seems that a caveat could be applied at any step in the process to see if the subject is part of the userset. But in order to make that judgement we need the subjects context.

Or am I missing something?

josephschorr commented 1 year ago

The search through a path to join the object and subject seems fundamental to AuthZed and it seems that a caveat could be applied at any step in the process to see if the subject is part of the userset. But in order to make that judgement we need the subjects context.

Can you clarify what you mean by "you need the subjects context" here?

lloydfischer commented 1 year ago

The search through a path to join the object and subject seems fundamental to AuthZed and it seems that a caveat could be applied at any step in the process to see if the subject is part of the userset. But in order to make that judgement we need the subjects context.

Can you clarify what you mean by "you need the subjects context" here?

the subject has an mfa status, that is their context. They are related to a role, without caveat. The role is related to a resource, with a caveat. The parameter to the resource caveat is the context of the subject.

Not to jump to solutions but it is as if the context from the original permission check passes through the resolution chain and is applied to every caveat that is encountered along the way based on some matching criteria. The chain could be arbitrarily long, and the permission check could have an arbitrary set of context N/V. When a caveat is encountered if a context name matches a caveat parameter name it is applied. If the caveat is satisfied the subject joins the set.

josephschorr commented 1 year ago

the subject has an mfa status, that is their context. They are related to a role, without caveat. The role is related to a resource, with a caveat. The parameter to the resource caveat is the context of the subject.

Yep, each has its own context, with the overall context being given in the CheckPermission call.

Not to jump to solutions but it is as if the context from the original permission check passes through the resolution chain and is applied to every caveat that is encountered along the way based on some matching criteria. The chain could be arbitrarily long, and the permission check could have an arbitrary set of context N/V. When a caveat is encountered if a context name matches a caveat parameter name it is applied. If the caveat is satisfied the subject joins the set.

Sort of; what actually happens is that we build a caveat expression as we resolve the full path and, at the end, if the result is caveated (e.g. it has an expression), the whole expression is evaluated to determine whether the permission is has_permission or not.

lloydfischer commented 1 year ago

the subject has an mfa status, that is their context. They are related to a role, without caveat. The role is related to a resource, with a caveat. The parameter to the resource caveat is the context of the subject.

Yep, each has its own context, with the overall context being given in the CheckPermission call.

Not to jump to solutions but it is as if the context from the original permission check passes through the resolution chain and is applied to every caveat that is encountered along the way based on some matching criteria. The chain could be arbitrarily long, and the permission check could have an arbitrary set of context N/V. When a caveat is encountered if a context name matches a caveat parameter name it is applied. If the caveat is satisfied the subject joins the set.

Sort of; what actually happens is that we build a caveat expression as we resolve the full path and, at the end, if the result is caveated (e.g. it has an expression), the whole expression is evaluated to determine whether the permission is has_permission or not.

I can confirm, with pleasant surprise, this it "just works". Here's my schema, relationships and assertions. (its my own format, based on the playground download format with my extensions for caveats. I have a python program that parses this and interacts with the spicedb API. )

You can see that I have caveats at the resource level but apply the inputs at the user level, even though the user doesn't have a caveat. The context is being passed down and applied correctly to control access to the resource. Awesome!!

As an afterthought I made the secure resource to accept either kind of role and now I can declare at the time of defining the relationship. So res1 and res3 have different mfa requirements even though they are in the same type. Awesomer!!!

schema: |- caveat need_mfa(mfa string) { mfa == 'true' } caveat dont_need_mfa(mfa string) { mfa == mfa }

definition user{}

definition role { relation viewer: user }

definition resource { relation roles: role with dont_need_mfa permission viewer = roles->viewer }

definition secure_resource { relation roles: role with need_mfa | role with dont_need_mfa permission viewer = roles->viewer } relationships: |- secure_resource:res1#roles@role:role1[need_mfa] secure_resource:res3#roles@role:role1[dont_need_mfa] resource:res2#roles@role:role1[dont_need_mfa] role:role1#viewer@user:user1 assertions: assertTrue:

josephschorr commented 1 year ago

As an afterthought I made the secure resource to accept either kind of role and now I can declare at the time of defining the relationship. So res1 and res3 have different mfa requirements even though they are in the same type. Awesomer!!!

You can elide this by declaring that a role is allowed directly, without caveat:

relation roles: role with need_mfa | role

Then you can just write a relationship without the caveat at all:

secure_resource:res1#roles@role:role1[need_mfa]
secure_resource:res3#roles@role:role1

Will be a bit faster that way, since in the case where an mfa is not needed, the system can just elide caveat execution completely

lloydfischer commented 1 year ago

As an afterthought I made the secure resource to accept either kind of role and now I can declare at the time of defining the relationship. So res1 and res3 have different mfa requirements even though they are in the same type. Awesomer!!!

You can elide this by declaring that a role is allowed directly, without caveat:

relation roles: role with need_mfa | role

Then you can just write a relationship without the caveat at all:

secure_resource:res1#roles@role:role1[need_mfa]
secure_resource:res3#roles@role:role1

Will be a bit faster that way, since in the case where an mfa is not needed, the system can just elide caveat execution completely

There are two cases: 1) caveat or none, or 2) this caveat or that caveat. Both can be expressed

josephschorr commented 1 year ago

There are two cases: 1) caveat or none, or 2) this caveat or that caveat. Both can be expressed

Yep! For reference:

relation roles: role with need_mfa | role with another_caveat | role

Will allow either caveat or no caveat

josephschorr commented 1 year ago

Remaining items to remove experimental flag: