Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Caveated relationships #386

Closed
jakedt opened this issue Jan 25, 2022 · 19 comments
Closed

Proposal: Caveated relationships #386

jakedt opened this issue Jan 25, 2022 · 19 comments
Assignees
Labels
area/api v1 Affects the v1 API area/caveats Affects caveated relationships area/schema Affects the Schema Language kind/proposal Something fundamentally needs to change priority/1 high This is the top priority

Comments

@jakedt
Copy link
Member

jakedt commented Jan 25, 2022

Goal

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

Use Cases

  • Time
    • “I want to bound the amount of time that a user has this role”
    • “The cleaning staff should only be allowed to access the office between 5pm and 10pm on weekdays”
  • IP Address
    • “You can only access this site if you’re not in N.K.”
  • Money
    • “This user is an approver if the order value is < $50”

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

  • check::document:firstdoc#view@user:tom
    • check::document:firstdoc#reader@user:tom
      • <- true
    • check::document:firstdoc#edit@user:tom
      • check::document:firstdoc#writer@user:tom
        • <- true[day($date) == Monday]
      • <- true[day($date) == Monday]
    • <- true <- Union(true, true[day($date) == Monday])
  • check::document:firstdoc#edit@user:tom
    • check::document:firstdoc#edit@user:tom
      • check::document:firstdoc#writer@user:tom
        • <- true[day($date) == Monday]
      • <- true[day($date) == Monday]
    • <- true[day($date) == Monday]

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

  • check::document:firstdoc#view@user:tom
    • check::document:firstdoc#reader@user:tom
      • <- true
    • check::document:firstdoc#banned@user:tom
      • <- true[day($date) == Monday]
    • <- true[!(day($date) == Monday)] <- Exclusion(true, true[day($date) == Monday])
  • check::document:firstdoc#view@user:jerry
    • check::document:firstdoc#reader@user:tom
      • <- true
    • check::document:firstdoc#banned@user:tom
      • <- false
    • <- true <- Exclusion(true, false)

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

  • check::document:firstdoc#view@user:tom
    • check::document:firstdoc#reader@user:tom
      • <- true[day($date) != Saturday, day($date) != Sunday]
    • check::document:firstdoc#billing_enabled@user:tom
      • <- true
    • <- true[day($date) != Saturday, day($date) != Sunday] <- Intersection(true, true[day($date) != Saturday, day($date) != Sunday])

Open Questions

  • Can we have duplicate relationships where only the caveats differ?
  • Do we specify in the schema whether or not caveats can apply, and possibly which kinds of attributes they apply to?
  • Should we use a simple comparison language (like SQL where clauses) to be able to make more intelligent policy from overlapping or disjoint ranges?
  • Should we utilize the familiarity people may already have with OPA/rego?
  • How do caveats work with preconditions in write? Can caveats be supplied there too?
  • How does this work with lookup? Do we need to compute the caveat at each step?
  • How does this interact with the Tiger cache?
  • Can we unify the caveats for wildcard exceptions with this general purpose caveat system?
  • Who executes the policy, the client or the server?

Previous Work

@jakedt jakedt added priority/4 maybe This might get done someday area/schema Affects the Schema Language area/api v1 Affects the v1 API state/needs discussion This can't be worked on yet state/gauging interest This needs to be championed before being worked on labels Jan 25, 2022
@aka-emi
Copy link

aka-emi commented Feb 9, 2022

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
Copy link

derwolfe commented Apr 28, 2022

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
Copy link

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
Copy link

enriquedacostacambio commented Jun 3, 2022

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
Copy link

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 vroldanbet added priority/1 high This is the top priority and removed priority/4 maybe This might get done someday labels Aug 22, 2022
@vroldanbet
Copy link
Contributor

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
Copy link

kie-ra commented Sep 20, 2022

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
Copy link

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:

  • A legal_filing has a creator that is permitted to edit the filing, but once it has been submitted (submitted == true) the filing should not be editable even by the creator. Many other users should still be able to view it.
  • When working with a database that uses soft deletes (the records must be kept permanently for forensic purposes), nobody should be able to mutate a record once it has been soft deleted. However, they can still be viewed by forensic investigators.

@toddkazakov
Copy link

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.

@jzelinskie jzelinskie added the kind/proposal Something fundamentally needs to change label Nov 16, 2022
@josephschorr josephschorr removed state/needs discussion This can't be worked on yet state/gauging interest This needs to be championed before being worked on labels Nov 23, 2022
@josephschorr josephschorr self-assigned this Nov 23, 2022
@josephschorr
Copy link
Member

josephschorr commented Nov 23, 2022

Remaining work:

  • Determine a compact syntax for the YAML and Playground
  • Add support in the development package
  • Add support in the playground
  • Add support in zed

@lloydfischer
Copy link

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
Copy link
Member

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
Copy link

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
Copy link
Member

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
Copy link

lloydfischer commented Dec 15, 2022

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:
- secure_resource:res1#viewer@user:user1 with {mfa:true}
- secure_resource:res3#viewer@user:user1 with {mfa:false}
- secure_resource:res3#viewer@user:user1 with {mfa:true}
- resource:res2#viewer@user:user1 with {mfa:true}
- resource:res2#viewer@user:user1 with {mfa:false}
assertFalse:
- secure_resource:res1#viewer@user:user1 with {mfa:false}
assertConditional:
- resource:res2#viewer@user:user1
- secure_resource:res1#viewer@user:user1
- secure_resource:res3#viewer@user:user1

@josephschorr
Copy link
Member

josephschorr commented Dec 15, 2022

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
Copy link

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
Copy link
Member

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
Copy link
Member

josephschorr commented Jan 25, 2023

Remaining items to remove experimental flag:

  • Add a caveat cache
  • Change caveat lookup in the evaluation system to read in bulk, through the cache
  • Add caveat expression to diff engine and use to avoid writing caveats when unchanged (Only write caveats that have been possibly updated #1120)
  • Cleanup remaining TODOs where possible and change remainder to NOTEs (Remove TODOs in caveat CEL code #1121)
  • Remove experimental warnings from documentation
  • (nice to have) Better error messages via CEL (if support is added there)

josephschorr added a commit to josephschorr/spicedb that referenced this issue Feb 9, 2023
Caveats are now marked as fully supported and production ready!

Fixes authzed#386
josephschorr added a commit to josephschorr/spicedb that referenced this issue Feb 9, 2023
Caveats are now marked as fully supported and production ready!

Fixes authzed#386
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/api v1 Affects the v1 API area/caveats Affects caveated relationships area/schema Affects the Schema Language kind/proposal Something fundamentally needs to change priority/1 high This is the top priority
Projects
None yet
Development

No branches or pull requests