Open winstaan74 opened 1 year ago
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.
I think it would be useful if this actually wasn't limited to relations providing that context. If you have multiple paths to an object, it means you would need to ensure all of those provide the necessary context attributes.
For example...
definition document {
relation folder: folder
relation label: label
permission read = folder->read + label->read
}
If read was conditional on attributes of the document, and you wanted to have those provided by spicedb rather than the user (e.g. to support resource-related caveats on LookupResources), you'd have to ensure all of the folder and label relations provided that context.
If we could attach those attributes to the document itself, maybe that problem goes away.
(I suppose you could possibly work around it by defining a sort of superfluous "container" type for the document, provide the folder/label relations there, so that for the actual document object there was only ever one path to it, at the cost of making the management of tuples more complicated.)
In our case, I think the policy we're trying to model doesn't strictly require this, it just might make migration a lot easier because it might allow something closer to a direct translation from the existing system.
I was looking for exactly this support and ended up here. It would be very useful if relations could supply parameters to be added to the context when traversed and have this accumulated context be used in caveats. My two cents, thanks.
Very impressed with SpiceDb. Thanks.
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 -
Or, perhaps the schema language should be more precise and enumerate the values that must be provided -
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) -
Perhaps the parameter values to be partially-bound could be specified here too -
Or may be simpler to stick with a more relaxed treatment of values in the schema.