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: Allow any relation to provide parameter values for caveats #1528

Open
winstaan74 opened this issue Sep 7, 2023 · 1 comment
Open
Labels
area/caveats Affects caveated relationships kind/proposal Something fundamentally needs to change

Comments

@winstaan74
Copy link

Problem Statement

Caveats allow a relationship to be defined conditionally - a caveated relationship is only considered to be present if the caveat function evaluates to true.

When writing a caveated relationship, values can be supplied - these are provided as parameters to the caveat function at runtime, and allow for a partial binding of data.

The remaining parameters required by a caveat function are taken from the values provided by the CheckPermissionRequest.

Providing the remaining parameters only from the request limits the usefulness of caveats as a language feature.

In particular, the value of each parameter is constant for the whole request - it is not possible to provide different values for a parameter for different branches of a permission-check walk through the graph.

Solution Brainstorm

Behaviour

When writing any relationship, allow a context of values to be provided.

These values would be in scope for walks that traverse the relation - and provided as parameters to caveats encountered on the walk.

Precedence

Values provided by a relationship would take precedence over values with the same name provided by previously-encountered relations or the CheckPermissionRequest.

In turn, the values would be masked by values with the same name in subsequent relationships - including values provided in a caveated relationship.

Schema

Optionally, the schema language could be extended to indicate that a relation allows values to be provided -

   relation r: resource providing *

Or, perhaps the schema language should be more precise and enumerate the values that must be provided -

caveat role(expected_role: String, actual_role: String) {
  expected_role == actual_role
}
...
   relation r: resource providing (expected_role, ... )

Although this is clearer, it may be too prescriptive, and is asymmetric to how caveated relations are currently described (where the parameters required to be partially-bound are unspecified) -

  relation role_assignee: user with role

Perhaps the parameter values to be partially-bound could be specified here too -

  relation role_assignee: user with role(actual_role)

Or may be simpler to stick with a more relaxed treatment of values in the schema.

@winstaan74 winstaan74 added the kind/proposal Something fundamentally needs to change label Sep 7, 2023
@winstaan74
Copy link
Author

Here's an example that motivates need for this feature -

Background:
I'm converting an existing feature-rich ad-hoc authorisation system to SpiceDB.

In the system, admin users can create Policies that define the permissions granted to users over resources. Each policy declares constraints that limit when the policy applies.

Some example constraints (of which there are many) are: if the current user is in the same organisation as the resource; if the current user is the manager of the resource; if the current user is the task assignee of the resource, and so on. The policies will slowly change over time, as does the organisation hierarchy and sets of users.

I've already reimplemented much of the existing system in SpiceDB, taking an approach inspired by the 'Google IAM' example in the playground.

Here's a simplified schema in terms of just a single permission 'read' and the three constraints mentioned above -

definition user {}

definition org {
  relation parent_org: org
  relation manager: user
  relation members: user

  permission manager_constraint = manager + parent_org->manager_constraint
  permission members_constraint = members + parent_org->members_constraint
}

definition resource {
  relation parent_org: org
  relation task_assignee: user
  relation policies: policy_binding

  permission manager_constraint = parent_org->manager_constraint
  permission members_constraint = parent_org->members_constraint
  permission task_assignee_constraint = task_assignee

  permission read = policies->read
}

definition policy_binding {
  relation satisfied: resource#task_assignee_constraint | resource#members_constraint | resource#manager_constraint

  permission read = satisfied
}

The main thing to note is that a resource delegates to policy_bindings for permission calculation - and the policy bindings call back into one of the constraints on the resource itself.

That is, assuming a policy p1 that is constrained to managers, and a policy p2 that is constrained to taskAssignees. Then, for a resource r1 , the following relationships from the resource to policy bindings and back again are written -

resource:r1#policies@policy_binding:r1p1
resource:r1#policies@policy_binding:r1p2
policy_binding:r1p1#satisfied@resource:r1#members_constraint
policy_binding:r1p2#satisfied@resource:r1#task_assignee_constraint

And this seems to work well. (although the policy_bindings need to be redone for all resources when a policy is changed)

The Problem,
The system allows admin users to define Roles, and then assign users to these roles for different parts of the organisation hierarchy. The collection of active roles slowly changes over time, as does user assignment to these roles.

Policies can be constrained to only apply to users who are assigned to a role.

Unfortunately this can't be implemented the same way as the constraints above, because roles themselves are dynamic - so there isn't a fixed xxx_constraint permission that can be related to the policy_binding#satisfies relation.

I've tried, unsuccessfully, to solve this using a caveat, adding the following definitions to the above schema -

caveat role(expected_role: String, actual_role: String) {
  expected_role == actual_role
}

definition org {
  relation role_assignee: user with role

  permission has_role_constraint = role_assignee + parent_org->has_role_constraint
  ...
}

definition resource {
  permission has_role_constraint = parent_org->has_role_constraint
  ...
}

definition policy_binding {
  relation satisfied: resource#has_role_constraint | ...  
}

To represent a role assignment, a relationship is written with a partially-bound role caveat where the actual_role is provided -

org:o1#role_assignee@user:u1[role{"actual_role":"rolename"}]

However, this doesn't work, because missing caveat parameters can only be provided in the CheckPermissionContext - and in this case, it's the policy_binding that should hold the knowledge on what the missing caveat parameter expected_role should be.

In fact, there may be multiple policy_bindings involved in a single permission check, each of which would supply a different value for the expected_role parameter. Hence the proposal to be able to provide caveat values on other relations.

@jzelinskie jzelinskie added the area/caveats Affects caveated relationships label Sep 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/caveats Affects caveated relationships kind/proposal Something fundamentally needs to change
Projects
None yet
Development

No branches or pull requests

2 participants