Relations providing caveat contexts
# spicedb
w
Hi. I've stuck on how to implement something, and I think the solution is something that would be useful to add to SpiceDB.
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 -
Copy code
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 -
Copy code
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. I tried adding the following definitions to the above schema -
Copy code
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 relation is written with a partially-bound role caveat where the actual_role is provided. - e.g.
Copy code
org:o1#role_assignee@user:u1[role{"actual_role":"rolename"}]
However, this doesn't work, as missing caveat parameters can only be provided in the check permission context - and in this case, it's the policy_binding that should hold the knowledge on what the missing caveat parameter
expected_role
should be.
The Proposal When writing a relationship, allow a context of parameters to be supplied. This would allow a caveated relation to be evaluated in different contexts within the same request, without requiring the request to pass in parameters. The schema language would be extended to require a relation provides a context containing specific parameters -
Copy code
definition policy_binding {
  relation satisfied: resource#has_role_constraint providing expected_role | ...  
}
And writing relationships would allow a context to be supplied
Copy code
policy_binding:r1p3#satisfied@resource:r1#has_role_constraint[{"expected_role":"rolename"}
The parameters in the relationship context would come into scope once the relation is traversed (and mask any previous values for that parameter).
On a separate note, it would be useful in the schema to be able to specify that a caveat should be partially-bound - e.g. to be able to write
Copy code
relation role_assignee: user with role(actual_role)
Sorry - I know that was a long one! Thanks for taking the time to read this, and I'd appreciate any feedback - including alternate approaches.
j
> as missing caveat parameters can only be provided in the check permission context
this is not true - you can specify context when writing a relationship as well
it works exactly as you suggested
except it isn't "enforced"
so you can specify the context parameters on either the relationship or the check context
if on both, the relationship's version takes precedence
the
providing
syntax is an interesting idea though
I'd suggest opening an issue for that
it should be noted it only works on the relationship that is caveated itself
it does not apply on arbitrary walks
w
Yes - I understand that you can provide context when writing the caveated relationship (I think my example uses this). I guess the 'providing' syntax to supply context on arbitrary walks is the nub of my proposal - I'll write up a proposal issue for it.
I'd be interested if this simplifies some other usecases too, as well as my (admittedly complex) current problem 🙂
j
supporting it on arbitrary walks would be (fairly) easy to do
but there becomes an issue of which takes precedence
w
3 Views