Model Resource Fields as Resources
To implement field-level authorization, you can model each of a resource's fields as separate resources.
For alternative approaches and more context, see Field-Level Authorization.
Implementation overview
In this approach, you create an additional resource for any resource's fields. This typically involves creating new rules to express the relationship between the "parent" resource and its field resources.
resource Account { permissions = [ # resource-level permissions "read", "update", ]; "read" if "update";}resource Field {}allow_field(user: User, action: String, account: Account, field: Field) if ...
With this strategy, you can apply rules to specific fields by identifying them:
allow_field(user: User, action: String, account: Account, field: Field) if field = Field{"email"}...
You can also use the same Field
resource for all resources that have fields:
allow_field(user: User, action: String, wallet: Wallet, field: Field) if field = Field{"balance"}...
While this approach can be more complex to model, it is well integrated into
Oso's API. For example, it fits naturally into the
query
(opens in a new tab)
subcommand of Oso's clients (examples).
In contrast to the "fields in permissions" approach which implicitly creates resources by mentioning them in permissions, "fields as resources" explicitly creates a resource for fields.
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 accounts' username fields, but no other fields. |
visitor | Can read accounts, but none of their fields. |
Policy
To accomplish the conditions stated above, we'll include:
-
A
Field
resource:resource Field {permissions = ["read", "update"];"read" if "update";} -
allow_field
rules to correlateAccount
s and theirField
s, e.g.# allow owners to update fields with a "parent" relationship with the accountallow_field(user: User, "update", account: Account, field: Field)if has_relation(account, "owner", user)and has_relation(field, "parent", account);
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";}# Account permissions## relation | read | update# --------------------------|------|--------# owner | ✓ | ✓# admin on parent | ✓ | ✓# community_admin on parent | ✓ | ✓# member on parent | ✓ | -# visitor on parent | ✓ | -resource Account { permissions = ["read", "update"]; relations = { parent: Organization, owner: User }; "update" if "owner"; "update" if "community_admin" on "parent"; "read" if "update"; "read" if "visitor" on "parent";}# Field permissions## relation | read | update# --------------------------|------|--------# owner | ✓ | †# admin on parent | ✓ | ✓# community_admin on parent | ✓ | *# member on parent | ✓ | -# visitor on parent | - | -## †: owner can update only defined fields on their own account# *: community_admin can update only `Field{"username"}`resource Field { permissions = ["read", "update"]; "read" if "update";}# define the set of fields that existhas_relation(Field{"username"}, "parent", _: Account);has_relation(Field{"email"}, "parent", _: Account);# allow admins to update any field, even those whose relationship with an# account is not definedallow_field(user: User, "update", account: Account, _field: Field) if org matches Organization and has_role(user, "admin", org) and has_relation(account, "parent", org);# allow owners to update fields with a "parent" relationship with the accountallow_field(user: User, "update", account: Account, field: Field) if has_relation(account, "owner", user) and has_relation(field, "parent", account);# allow community admins to update only the username fieldallow_field(user: User, "update", account: Account, field: Field) if field = Field{"username"} and org matches Organization and has_role(user, "community_admin", org) and has_relation(account, "parent", org) # safeguard to check that user does have update on the account. and has_permission(user, "update", account) and has_relation(field, "parent", account);# allow members to read all fields, n.b. visitors cannot read any fieldsallow_field(user: User, "read", account: Account, field: Field) if org matches Organization and has_role(user, "member", org) and has_relation(account, "parent", org) # safeguard to check that user does have read on the account. and has_permission(user, "read", account) and has_relation(field, "parent", account);test "Fields as resources" { 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 defined fields of their own account assert allow_field(User{"alice"}, "update", Account{"alice"}, Field{"username"}); assert allow_field(User{"charlie"}, "update", Account{"charlie"}, Field{"email"}); assert allow_field(User{"dana"}, "update", Account{"dana"}, Field{"email"}); # admins can update all defined fields in all accounts assert allow_field(User{"alice"}, "update", Account{"bob"}, Field{"username"}); assert allow_field(User{"alice"}, "update", Account{"charlie"}, Field{"email"}); # admins can update all fields in all accounts, including those undefined. assert allow_field(User{"alice"}, "update", Account{"alice"}, Field{"abc"}); assert allow_field(User{"alice"}, "update", Account{"dana"}, Field{"xyz"}); # non-admin users cannot update undefined fields of thier own accounts. assert_not allow_field(User{"bob"}, "update", Account{"bob"}, Field{"xyz"}); # community admins can only update usernames, but can read all fields assert allow_field(User{"bob"}, "update", Account{"alice"}, Field{"username"}); assert_not allow_field(User{"bob"}, "update", Account{"alice"}, Field{"email"}); assert allow_field(User{"bob"}, "read", Account{"alice"}, Field{"email"}); assert_not allow_field(User{"bob"}, "update", Account{"dana"}, Field{"email"}); # members can only read fields from others' accounts assert allow_field(User{"charlie"}, "read", Account{"alice"}, Field{"username"}); assert allow_field(User{"charlie"}, "read", Account{"bob"}, Field{"email"}); assert_not allow_field(User{"charlie"}, "update", Account{"dana"}, Field{"email"}); # 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_field(User{"dana"}, "read", Account{"bob"}, Field{"username"}); assert_not allow_field(User{"dana"}, "read", Account{"charlie"}, Field{"email"}); assert_not allow_field(User{"dana"}, "update", Account{"charlie"}, Field{"email"}); # granted no permissions on fields directly assert_not allow(User{"alice"}, "read", Field{"email"}); assert_not allow(User{"alice"}, "update", Field{"username"}); assert_not allow(User{"bob"}, "update", Field{"username"}); assert_not allow(User{"charlie"}, "read", Field{"email"});}
Limit the set of valid Field identifiers
The policy defined has_relation
between some named Field
resources and all
Account
s:
# define the set of fields that existhas_relation(Field{"username"}, "parent", _: Account);has_relation(Field{"email"}, "parent", _: Account);
This provides the flexibility to:
- Allow actions on any
Field
using wildcard matches (_
):# allow admins to update any field, even those whose relationship with an# account is not definedallow_field(user: User, "update", account: Account, _field: Field)if org matches Organizationand has_role(user, "admin", org)and has_relation(account, "parent", org); - Require the field have a defined relation:
# allow owners to update fields with a "parent" relationship with the accountallow_field(user: User, "update", account: Account, field: Field)if has_relation(account, "owner", user)and has_relation(field, "parent", account);
However, not all policies will need to define a relation between explicit field identifiers and their parent resources.
Client
By modeling fields as resources
and introducing a new allow_field
rule,
we can use the Oso client query
subcommand to determine users' field-level
authorization for accounts.
To determine charlie
's permissions on alice
's Account
:
oso-cloud query allow_field User:bob _ Account:alice Field:_
allow_field(User:bob, String:read, Account:alice, Field:_)allow_field(User:bob, String:update, Account:alice, Field:username)
bob
can read any (_
) field from the alice
account, but can only update
the username
field.
For his own account, bob
can update all fields:
oso-cloud query allow_field User:bob _ Account:bob Field:_
allow_field(User:bob, String:read, Account:bob, Field:_)allow_field(User:bob, String:update, Account:bob, Field:_)allow_field(User:bob, String:update, Account:bob, Field:username)
The redundant update
permission comes from the fact that a community_admin
can edit their own username using their community_admin
privileges in addition
to the update
permissions granted to the account owner.