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
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' usernamefields, but no other fields. | 
| visitor | Can read accounts, but none of their fields. | 
Policy
To accomplish the conditions stated above, we'll include:
- 
A Fieldresource:resource Field {permissions = ["read", "update"];"read" if "update";}
- 
allow_fieldrules to correlateAccounts and theirFields, e.g.# allow owners to update fields with a "parent" relationship with the accountallow_field(user: User, "update", account: Account, field: Field) ifhas_relation(account, "owner", user) andhas_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) and  # safeguard to check that user does have update on the account.  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) and  # safeguard to check that user does have read on the account.  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
Accounts:
# 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 Fieldusing 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) iforg matches Organization andhas_role(user, "admin", org) andhas_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) ifhas_relation(account, "owner", user) andhas_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.