Model Resource Fields in Permissions
To implement field-level authorization, you can specify a permission per action per field on the "parent" resource. This approach can be straightforward in terms of your policy, but might require more complex client-side processing.
For alternative approaches and more context, see Field-Level Authorization.
Implementation overview
In this approach, you encode field-level authorization into permissions on the
"parent" resource. To make this permission's purpose clear, we recommend
demarcating the field and the permission with some character, such as a .
.
resource Account { permissions = [ # resource-level permissions "read", "update", # field-level permissions "email.read", "email.update", ]; "read" if "update"; "email.read" if "read"; "email.update" if "update";}
Another way to think about "fields in permissions" is that it lets you create "logical resources" by mentioning them in other resource's permissions. However, these logical resources are never explicitly referenced in your policy and only exist implicitly. This contrasts with the "fields as resources" approach which explicitly creates resources for fields.
A benefit of this approach is that you can get all of a user's permissions on a
resource with the
actions
subcommand, and then manipulate the return values to determine the fields they
can access.
However, this approach has a few downsides:
- Application code might need to manipulate data from Oso to make authorization decisions.
- Policies must provide explicit sets of fields, as well as the permissions you want to allow on them. This introduces a multiplicative number of permissions to manage.
Example
We'll model a social app with an Account
whose fields we want to apply
granular access to––specifically two rules that are simpler to model using
field-level authorization.
User role | Special case |
---|---|
community_admin | Can update other Account s' username fields, but no other fields. |
visitor | Can read others Account s, but none of their fields. |
Policy
To accomplish the conditions stated above, we'll include:
-
Permissions per action per field:
resource Account {permissions = [# resource-level permissions"read", "update",# field-level permissions"username.read", "username.update","email.read", "email.update",]; -
RBAC per action per field:
# username## username.update"username.update" if "owner";"username.update" if "admin" on "parent";"username.update" if "community_admin" on "parent";## username.read"username.read" if "username.update";"username.read" if "member" on "parent";
This policy shows all of the pieces working together.
actor User {}resource Organization { roles = ["visitor", "member", "community_admin", "admin"]; permissions = ["read", "update"]; # Role implication # visitor < member < community_admin < admin "visitor" if "member"; "member" if "community_admin"; "community_admin" if "admin"; # RBAC "update" if "admin"; "read" if "visitor";}resource Account { permissions = [ # resource-level permissions "read", "update", # field-level permissions "username.read", "username.update", "email.read", "email.update", ]; relations = { parent: Organization, owner: User }; # RBAC # Resource-level permissions # # relation | read | update # --------------------------|------|-------- # owner | x | x # admin on parent | x | x # community_admin on parent | x | x # member on parent | x | - # visitor on parent | x | - "update" if "owner"; "update" if "admin" on "parent"; # "update" is a higher-level permission than "username.update", so apply it # to community_admin. "update" if "community_admin" on "parent"; "read" if "update"; "read" if "visitor" on "parent"; # Field-level permissions # # relation | username | email # --------------------------|--------------|---------------- # owner | read, update | read, update # admin on parent | read, update | read, update # community_admin on parent | read, update | read # member on parent | read | read # visitor on parent | - | - # username ## username.update "username.update" if "owner"; "username.update" if "admin" on "parent"; "username.update" if "community_admin" on "parent"; ## username.read "username.read" if "username.update"; "username.read" if "member" on "parent"; # email ## email.update "email.update" if "owner"; "email.update" if "admin" on "parent"; ## username.read "email.read" if "email.update"; "email.read" if "community_admin" on "parent"; "email.read" if "member" on "parent";}test "Fields in permissions" { setup { # admin has_role(User{"alice"}, "admin", Organization{"example"}); has_relation(Account{"alice"}, "owner", User{"alice"}); has_relation(Account{"alice"}, "parent", Organization{"example"}); # community_admin has_role(User{"bob"}, "community_admin", Organization{"example"}); has_relation(Account{"bob"}, "owner", User{"bob"}); has_relation(Account{"bob"}, "parent", Organization{"example"}); # member has_role(User{"charlie"}, "member", Organization{"example"}); has_relation(Account{"charlie"}, "owner", User{"charlie"}); has_relation(Account{"charlie"}, "parent", Organization{"example"}); # visitor has_role(User{"dana"}, "visitor", Organization{"example"}); has_relation(Account{"dana"}, "owner", User{"dana"}); has_relation(Account{"dana"}, "parent", Organization{"example"}); } # anyone can update any field of their own account assert allow(User{"charlie"}, "update", Account{"charlie"}); assert allow(User{"charlie"}, "username.update", Account{"charlie"}); assert allow(User{"dana"}, "update", Account{"dana"}); assert allow(User{"dana"}, "username.update", Account{"dana"}); # admins can update all fields in all accounts assert allow(User{"alice"}, "username.update", Account{"bob"}); assert allow(User{"alice"}, "email.update", Account{"bob"}); assert allow(User{"alice"}, "username.update", Account{"charlie"}); # community admins have resource-level update permissions assert allow(User{"bob"}, "update", Account{"alice"}); assert allow(User{"bob"}, "update", Account{"dana"}); # community admins can only update usernames, but can read all fields assert allow(User{"bob"}, "username.update", Account{"alice"}); assert allow(User{"bob"}, "username.update", Account{"charlie"}); assert_not allow(User{"bob"}, "email.update", Account{"alice"}); assert_not allow(User{"bob"}, "email.update", Account{"dana"}); assert allow(User{"bob"}, "email.read", Account{"alice"}); # members have read access to all fields assert allow(User{"charlie"}, "username.read", Account{"alice"}); assert allow(User{"charlie"}, "email.read", Account{"dana"}); assert_not allow(User{"charlie"}, "email.update", Account{"dana"}); # visitors only have read access to others' accounts assert allow(User{"dana"}, "read", Account{"alice"}); assert allow(User{"dana"}, "read", Account{"charlie"}); assert_not allow(User{"dana"}, "update", Account{"charlie"}); # visitors have no field-level access assert_not allow(User{"dana"}, "username.read", Account{"alice"}); assert_not allow(User{"dana"}, "email.read", Account{"charlie"});}
Client
The benefit of fields in permissions is that you can derive field-level
authorization using the actions
subcommand.
To determine a community_admin
's permissions on an account that is not their
own:
oso-cloud actions User:bob Account:alice
email.readreadupdateusername.readusername.update
With that output, manipulate the text to determine which fields the user can read:
oso-cloud actions User:bob Account:alice | awk -F '.' '/.*\.read/ {print $1}'
emailusername
Or update:
oso-cloud actions User:bob Account:alice | awk -F '.' '/.*\.update/ {print $1}'
username