actor User {}
resource Organization {
  roles = ["visitor", "member", "community_admin", "admin"];
  permissions = ["read", "update"];
  "visitor" if "member";
  "member" if "community_admin";
  "community_admin" if "admin";
  "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 which fields exist
has_relation(Field{"username"}, "parent", _: Account);
has_relation(Field{"email"}, "parent", _: Account);
# Allow owners to update their own fields
allow_field(user: User, "update", account: Account, field: Field) if
  has_relation(account, "owner", user) and
  has_relation(field, "parent", account);
# Allow admins to update any field, even those whose relationship with an
# account is not defined
allow_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 community admins to update only usernames
allow_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
  has_permission(user, "update", account) and
  has_relation(field, "parent", account);
# Allow members to read all fields
allow_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
  has_permission(user, "read", account) and
  has_relation(field, "parent", account);
test "admins can update all fields" {
  setup {
    has_role(User{"bob"}, "admin", Organization{"acme"});
    has_relation(Account{"amy"}, "parent", Organization{"acme"});
  }
  assert allow_field(User{"bob"}, "update", Account{"amy"}, Field{"username"});
  assert allow_field(User{"bob"}, "update", Account{"amy"}, Field{"email"});
}
test "community admins can only update usernames" {
  setup {
    has_role(User{"jim"}, "community_admin", Organization{"acme"});
    has_relation(Account{"amy"}, "parent", Organization{"acme"});
  }
  assert allow_field(User{"jim"}, "update", Account{"amy"}, Field{"username"});
  assert_not allow_field(User{"jim"}, "update", Account{"amy"}, Field{"email"});
}
test "members can only read fields" {
  setup {
    has_role(User{"jim"}, "member", Organization{"acme"});
    has_relation(Account{"amy"}, "parent", Organization{"acme"});
  }
  assert allow_field(User{"jim"}, "read", Account{"amy"}, Field{"username"});
  assert_not allow_field(User{"jim"}, "update", Account{"amy"}, Field{"email"});
}