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 roleSpecial case
community_adminCan update other Accounts' username fields, but no other fields.
visitorCan read others Accounts, 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.read
read
update
username.read
username.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}'


email
username

Or update:


oso-cloud actions User:bob Account:alice | awk -F '.' '/.*\.update/ {print $1}'


username

Related