Google Docs introduced many people to resource-specific roles––even if users never thought about it in those terms. This paradigm enables seemingly simple statements like "Alice is an editor on this document." Behind the scenes, this means Alice has a role ("editor") on a specific resource (the document). To provide this feature, your app needs tremendous flexibility in managing users, resources, and roles. As users become more sophisticated, they've come to expect resource-specific roles as table stakes for many types of applications.
While Google Docs and Drive might be the most familiar examples, many other applications rely on resource-specific roles for collaboration:
- Figma and other design tools let users share and collaborate on design files and components
- Code repositories like GitHub offer complex options to manage codebases, with both roles and individual permissions
- Kickstarter enables project collaboration, delegating permissions between project management, community management, and order fulfillment
Oso makes managing each of these resource sharing scenarios simple, flexible, and extensible.
Building Resource-Specific Roles with Polar
Oso's Polar language lets you express sharing relationships clearly and concisely. Here's a basic example of modeling resource-specific roles:
actor User { }
resource Resource {
permissions = ["view", "edit", "manage_share"];
roles = ["viewer", "editor", "admin"];
"viewer" if "admin";
"editor" if "admin";
"view" if "viewer";
"edit" if "editor";
"manage_share" if "admin";
}
This foundation makes it easy to define which users have which roles on which resources. We can demonstrate this using Polar's inline unit tests:
test "users with the reader role can read resources" {
setup {
has_role(User{"alice"}, "reader", Resource{"anvil"});
has_role(User{"bob"}, "reader", Resource{"anvil"});
}
assert allow(User{"alice"}, "read", Resource{"anvil"});
assert allow(User{"bob"}, "read", Resource{"anvil"});
# Bob does not have a role that can write
assert_not allow(User{"bob"}, "write", Resource{"anvil"});
}
Adding Group Sharing
This foundation is remarkably flexible. Need to add sharing for groups or teams? Just introduce Groups as a type of user and add another rule:
actor User { }
# A group is a kind of actor
actor Group { }
# Use the same Resource definition from above
resource Resource {...}
# Users inherit roles from groups
has_role(user: User, role: String, resource: Resource) if
group matches Group and
has_group(user, group) and
has_role(group, role, resource);
test "group members can read resources" {
setup {
# Groups
has_role(Group{"smiths"}, "reader", Resource{"anvil"});
has_group(User{"alice"}, Group{"smiths"});
has_group(User{"bob"}, Group{"smiths"});
# Resource-specific roles
has_role(User{"bob"}, "writer", Resource{"anvil"})
}
assert allow(User{"alice"}, "read", Resource{"anvil"});
assert allow(User{"bob"}, "read", Resource{"anvil"});
# Bob has an explicit writer role on Anvil
assert allow(User{"bob"}, "write", Resource{"anvil"});
}
This lets you easily share the same permissions with all members of a team at once. And what's great is that the flexibility means that you can also assign individual members of the teams a disjoint set of permissions without a problem.
Multi-tenancy with Sharing
In Oso, we refer to "tenants" as Organizations
. You can easily add organization support to this resource-sharing foundation using our relationship-based access shorthand:
actor User { }
resource Organization {
roles = ["member", "admin"];
permissions = ["read", "create_user"];
"member" if "admin";
"read" if "member";
"create_user" if "admin";
}
resource Resource {
permissions = ["view", "edit", "manage_share", "have_role"];
roles = ["viewer", "editor", "admin"];
relations = {
belongs_to: Organization
};
"have_role" if "member" on "belongs_to";
# same as above
...
}
test "organization members can have resource roles" {
setup {
has_role(User{"alice"}, "admin", Organization{"acme"});
has_relationship(Resource{"anvil"}, "belongs_to", Organization{"acme"});
}
assert allow(User{"alice"}, "have_role", Resource{"anvil"});
}
Why This Matters
The power of Oso's approach lies not just in writing these rules, but in maintaining and evolving them. As your application grows, you'll need to:
- Add new types of resources
- Support new sharing patterns
- Handle edge cases and special permissions
- Maintain consistency across your application
With Oso, these changes are localized to your authorization policy, not scattered throughout your application code. This separation of concerns makes your code more maintainable and easier to reason about.
See It in Action
If you're curious what a more complete example of building a resource sharing application with Oso looks like, check out our reference implementation of a file-sharing app on GitHub.
Ready to simplify your permissions management? Visit our docs and sign up for a free Oso developer account.