Learn by doing

Here are a collection of practice-oriented guides for using Oso.

Each guide is focused around implementing specific functionality in your application.

Build Role-Based Access Control (RBAC)

Build role-based access control (RBAC) with Oso's built-in authorization modeling features.

Filter Data
Filter Data When you call authorize(actor, action, resource) , Oso evaluates the allow rule(s) you have defined in your policy to determine if actor is allowed to perform action on resource. For example, if jane wants to "edit" a document, Oso may check that jane = document.owner. But what if you need the set of all documents that Jane is allowed to edit? For example, you may want to render them as a list in your application. One way to answer this question is to take every document in the system and call is_allowed on it. This isn’t efficient and many times is just impossible. There could be thousands of documents in a database but only three that have the owner "steve". Instead of fetching every document and passing it into Oso, it’s better to ask the database for only the documents that have the owner "steve". Using Oso to filter the data in your data store based on the logic in your policy is what we call “Data Filtering”. You can use data filtering to enforce authorization on queries made to your data store. Oso will take the logic in the policy and turn it into a query for the authorized data. Examples could include an ORM filter object, an HTTP request or an elastic-search query. The query object and the way the logic maps to a query are both user defined. Data filtering is initiated through two methods on Oso. authorized_resources returns a list of all the resources a user is allowed to do an action on. The results of a built and executed query. authorized_query returns the query object itself. This lets you add additional filters or sorts or any other data to it before executing it. You must define how to build queries and a few other details when you register classes to enable these methods. Implementing data filtering Query Functions There are three Query functions that must be implemented. These define what a query is for your application, how the logic in the policy maps to them, how to execute them and how to combine two queries. Build a Query build_query takes a list of Filters and returns a Query Filters are individual pieces of logic that must apply to the data being fetched. Filters have a kind, a field and a value. Their meaning depends on the kind field. Eq means that the field must be equal to the value. Neq means that the field must not be equal to the value. In means that the field must be equal to one of the values in value. Value will be a list. Nin means that the field must not be equal to one of the values in value. Value will be a list. Contains means that the field must contain the value. This only applies if the field is a list. The condition described by a Filter applies to the data stored in the attribute field of a resource. The field of a Filter may be nil, in which case the condition applies to the resource directly. Execute a Query exec_query takes a query and returns a list of the results. Combine Queries combine_query takes two queries and returns a new query that returns the union of the other two. For example if the two queries are SQL queries combine could UNION them. If they were HTTP requests combine_query could put them in an array and could handle executing an array of queries and combining the results. You can define functions that apply to all types with set_data_filtering_query_defaults. Or you can pass type specific ones when you register a class. Fields The other thing you have to provide to use data filtering is type information for registered classes. This lets Oso know what the types of an object’s fields are. Oso needs this information to handle specializers and other things in the policy when we don’t have a concrete resource. The fields are a map from field name to type. Example In this example we’ll model access to code repositories in a simple Git hosting application. data_filtering_example_a.rb # We'll use ActiveRecord in this example, but data filtering can be used with any ORM require 'active_record' require 'sqlite3' require 'oso' DB_FILE = '/tmp/test.db' Relation = Oso::Relation class Repository < ActiveRecord::Base include QueryConfig # This module adds build/exec/combine query functions for the class end class User < ActiveRecord::Base include QueryConfig has_many :repo_roles end class RepoRole < ActiveRecord::Base include QueryConfig belongs_to :user belongs_to :repository, foreign_key: :repo_id end def init_db File.delete DB_FILE if File.exist? DB_FILE db = SQLite3::Database.new(DB_FILE) db.execute <<-SQL create table users ( id varchar(16) not null primary key ); SQL db.execute <<-SQL create table repositories ( id varchar(16) not null primary key ); SQL db.execute <<-SQL create table repo_roles ( id integer not null primary key autoincrement, name varchar(16) not null, repo_id varchar(16) not null, user_id varchar(16) not null ); SQL ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: DB_FILE ) end For each class we need to register it and define the query functions. data_filtering_example_a.rb def init_oso oso = Oso.new oso.register_class( Repository, fields: { id: String, } ) oso.register_class( User, fields: { id: String, } ) oso.register_class( RepoRole, fields: { name: String, } ) oso end # This mixin automatically defines query functions, # so we don't have to pass build_query, exec_query, and # combine_query into register_class. module QueryConfig def self.included(base) base.instance_eval do # Turn a filter into a param hash for #where query_clause = lambda do |f| if f.field.nil? { primary_key => f.value.send(primary_key) } else { f.field => f.value } end end # ActiveRecord automatically turns array values in where clauses into # IN conditions, so Eq and In can share the same code. @filter_handlers = { 'Eq' => ->(query, filter) { query.where query_clause[filter] }, 'In' => ->(query, filter) { query.where query_clause[filter] }, 'Neq' => ->(query, filter) { query.where.not query_clause[filter] } } @filter_handlers.default_proc = proc do |k| raise "Unsupported filter kind: #{k}" end @filter_handlers.freeze # Create a query from an array of filters def self.build_query(filters) filters.reduce(all) do |query, filter| @filter_handlers[filter.kind][query, filter] end end # Produce an array of values from a query def self.exec_query(query) query.distinct.to_a end # Merge two queries into a new query with the results from both def self.combine_query(one, two) one.or(two) end end end end Then we can load a policy and query it. policy_a.polar actor User {} resource Repository { permissions = ["read", "push", "delete"]; roles = ["contributor", "maintainer", "admin"]; "read" if "contributor"; "push" if "maintainer"; "delete" if "admin"; "maintainer" if "admin"; "contributor" if "maintainer"; } allow(actor, action, resource) if has_permission(actor, action, resource); has_role(user: User, role_name: String, repository: Repository) if role in user.repo_roles and role.name = role_name and role.repo_id = repository.id; data_filtering_example_a.rb def example init_db oso = init_oso ios = Repository.create id: 'ios' oso_repo = Repository.create id: 'oso' demo_repo = Repository.create id: 'demo' leina = User.create id: 'leina' steve = User.create id: 'steve' RepoRole.create user: leina, repository: oso_repo, name: 'contributor' RepoRole.create user: leina, repository: demo_repo, name: 'maintainer' oso.load_files(['policy_a.polar']) results = oso.authorized_resources(leina, 'read', Repository) raise unless results == [demo_repo, oso_repo] end example Relations Often you need data that is not contained on the object to make authorization decisions. This comes up when the role required to do something is implied by a role on it’s parent object. For instance, you want to check the organization for a repository but that data isn’t embedded on the repository object. You can add a Relation type to the type definition that states how the other resource is related to this one. Then you can access this field in the policy like any other field and it will fetch the data when it needs it (via the query functions). Relations are a special type that tells Oso how one Class is related to another. They specify what the related type is and how it’s related. kind is either “one” or “many”. “one” means there is one related object and “many” means there is a list of related objects. other_type is the class of the related objects. my_field Is the field on this object that matches other_field. other_field Is the field on the other object that matches this_field. The my_field / other_field relationship is similar to a foreign key. It lets Oso know what fields to match up with building a query for the other type. Example This time our data will be a little more complicated in order to model a more sophisticated policy. data_filtering_example_b.rb require 'active_record' require 'sqlite3' require 'oso' DB_FILE = '/tmp/test.db' Relation = Oso::Relation class Organization < ActiveRecord::Base include QueryConfig end class Repository < ActiveRecord::Base include QueryConfig belongs_to :organization, foreign_key: :org_id end class User < ActiveRecord::Base include QueryConfig has_many :repo_roles has_many :org_roles end class OrgRole < ActiveRecord::Base include QueryConfig belongs_to :user belongs_to :organization, foreign_key: :org_id end class RepoRole < ActiveRecord::Base include QueryConfig belongs_to :user belongs_to :repository, foreign_key: :repo_id end def init_db File.delete DB_FILE if File.exist? DB_FILE db = SQLite3::Database.new(DB_FILE) db.execute <<-SQL create table organizations ( id varchar(16) not null primary key ); SQL db.execute <<-SQL create table users ( id varchar(16) not null primary key ); SQL db.execute <<-SQL create table repositories ( id varchar(16) not null primary key, org_id varchar(16) not null ); SQL db.execute <<-SQL create table repo_roles ( id integer not null primary key autoincrement, name varchar(16) not null, repo_id varchar(16) not null, user_id varchar(16) not null ); SQL db.execute <<-SQL create table org_roles ( id integer not null primary key autoincrement, name varchar(16) not null, org_id varchar(16) not null, user_id varchar(16) not null ); SQL ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: DB_FILE ) end We now have two sets of query functions. Our build_query function depends on the class but our exec_query and combine_query functions are the same for all types so we can set them with set_data_filtering_query_defaults. data_filtering_example_b.rb def init_oso oso = Oso.new oso.register_class( Organization, fields: { id: String } ) oso.register_class( Repository, fields: { id: String, organization: Relation.new( kind: 'one', other_type: Organization, my_field: 'org_id', other_field: 'id' ) } ) oso.register_class( User, fields: { id: String, } ) oso.register_class( RepoRole, fields: { name: String, } ) oso.register_class( OrgRole, fields: { name: String, } ) oso end module QueryConfig def self.included(base) base.instance_eval do # Turn a constraint into a param hash for #where query_clause = lambda do |f| if f.field.nil? { primary_key => f.value.send(primary_key) } else { f.field => f.value } end end # ActiveRecord automatically turns array values in where clauses into # IN conditions, so Eq and In can share the same code. @filter_handlers = { 'Eq' => ->(query, filter) { query.where query_clause[filter] }, 'In' => ->(query, filter) { query.where query_clause[filter] }, 'Neq' => ->(query, filter) { query.where.not query_clause[filter] } } @filter_handlers.default_proc = proc do |k| raise "Unsupported filter kind: #{k}" end @filter_handlers.freeze # Create a query from an array of filters def self.build_query(filters) filters.reduce(all) do |query, filter| @filter_handlers[filter.kind][query, filter] end end # Produce an array of values from a query def self.exec_query(query) query.distinct.to_a end # Merge two queries into a new query with the results from both def self.combine_query(one, two) one.or(two) end end end end policy_b.polar actor User {} resource Organization { permissions = ["add_member", "read", "delete"]; roles = ["member", "owner"]; "add_member" if "owner"; "delete" if "owner"; "member" if "owner"; } # Anyone can read. allow(_, "read", _org: Organization); resource Repository { permissions = ["read", "push", "delete"]; roles = ["contributor", "maintainer", "admin"]; relations = { parent: Organization }; "read" if "contributor"; "push" if "maintainer"; "delete" if "admin"; "maintainer" if "admin"; "contributor" if "maintainer"; "contributor" if "member" on "parent"; "admin" if "owner" on "parent"; } has_relation(organization: Organization, "parent", repository: Repository) if repository.organization = organization; has_role(user: User, role_name: String, repository: Repository) if role in user.repo_roles and role.name = role_name and role.repo_id = repository.id; has_role(user: User, role_name: String, organization: Organization) if role in user.org_roles and role.name = role_name and role.org_id = organization.id; allow(actor, action, resource) if has_permission(actor, action, resource); data_filtering_example_b.rb def example init_db oso = init_oso osohq = Organization.create id: 'osohq' apple = Organization.create id: 'apple' ios = Repository.create id: 'ios', organization: apple oso_repo = Repository.create id: 'oso', organization: osohq demo_repo = Repository.create id: 'demo', organization: osohq leina = User.create id: 'leina' steve = User.create id: 'steve' OrgRole.create user: leina, organization: osohq, name: 'owner' oso.load_files(['policy_b.polar']) results = oso.authorized_resources(leina, 'read', Repository) raise unless results == [oso_repo, demo_repo] end example Evaluation When Oso is evaluating data filtering methods it uses queries to fetch objects. If there are multiple types involved it will make multiple queries and substitute in the results when needed. In the above example we are fetching Repositories, but we are basing our fetch on some information about their related Organization. To resolve the query Oso first fetches the relevant Organizations (based in this case on role assignments), and then uses the Relation definition to substitute in their ids to the query for Repositories. This is the main reason to use Relations, they let Oso know how different classes are related so we can resolve data filtering queries. Relation fields also work when you are not using data filtering methods and are just using authorize or another method where you have an object to pass in. In that case the query functions are still called to get related objects so if you’re using a Relation to a type, you must define query functions for that type. Limitations There are a few limitations to what you can do while using data filtering. You can not call any methods on the passed in resource and you can not pass the resource as an argument to any methods. Many cases where you would want to do this are better handled by Relation fields. Some Polar expressions are not supported. not, cut and forall are not allowed in policies that want to use data filtering. Numeric comparisons with the < > <= and >= are not currently supported either. Relations only support matching on a single field. For example, relating a Student to their classmates with matching school_id and homeroom_id fields isn’t currently possible.
Enforce an Oso Policy
Enforce an Oso Policy To use an Oso policy in your app, you’ll need to “enforce” it. A policy is useless without an app that consults the policy on user actions. For most apps, policies can be enforced on multiple “levels”: Resource-level: is the user allowed to perform this action on a particular resource? Field-level: which fields on this object can the user read? Which ones can they update? Request-level: should this user even be able to hit this endpoint, regardless of the resources it involves? Oso provides an API to enforce authorization at all levels, each of which are described in this guide. We recommend starting out by reading about resource-level enforcement.
Write Rules

Learn about writing Oso policies - the source of truth for authorization logic.

Build Authorization for Resource Hierarchies
Build Authorization for Resource Hierarchies A resource hierarchy refers to a model with nested resources, where a user’s permissions and roles on a resource depend on the resource’s parent. Common examples of resource hierarchies include: File system permissions: access to a folder may grant access to documents within the folder Grouping resources by project: users have project-level roles and permissions that determine their access to resources within the project Organizations and multi-tenancy: top-level tenant/organization roles and permissions grant access to resources within the organization You can model resource hierarchies in Oso by defining relations between resources. You can write policies that use relations and query them to find out if a user has access to a single resource or to get a list of resources that a user has access to (using the data filtering feature). This guide uses an example resource hierarchy from our GitClub sample application. In GitClub, Organization is the top-level resource, and Repository resources are nested within organizations. Users have roles at both the organization and repository level. A user’s organization role grants them a default role on every repository within that organization. 1. Register resource types and relations The first step to modeling a resource hierarchy is to register the application types that represent the resources you are protecting. To make your implementation compatible with data filtering, you need to specify resource relations by creating Relation objects and passing them to register_class(). For more information on registering classes and relations for data filtering, see the data filtering guide. app.rb require 'oso' Relation = Oso::Relation oso = Oso.new # Register the Organization class oso.register_class( Organization, fields: { id: String } ) # Register the Repository class, and its relation to the Organization class oso.register_class( Repository, fields: { id: String, organization: Relation.new( kind: 'one', other_type: Organization, my_field: 'org_id', other_field: 'id' ) } ) 2. Declare parent relations After registering your resource types, you can define a resource block for each resource in your policy. Inside each block, you should declare the permissions and roles that are available on that resource type. For child resource types, also declare relations to parent resources. main.polarallow(actor, action, resource) if has_permission(actor, action, resource); resource Organization { permissions = ["read", "add_member"]; roles = ["member", "owner"]; } resource Repository { permissions = ["read", "push"]; roles = ["contributor", "maintainer", "admin"]; relations = { parent: Organization }; } Now that you’ve defined your resource relations in Polar, you can hook them up to the Relations you registered in Step 1 using has_relation rules: main.polarhas_relation(parent_org: Organization, "parent", child_repo: Repository) if parent_org = child_repo.organization; # use the `organization` relation we registered in Step 1 Tip Using registered Relations to access related resources from your policy, rather than using an arbitrary application field or method, ensures that data filtering queries will work with your policy. 3. Write rules using parent relations You now have all the plumbing in place to write rules that use parent relations. If you need to grant a role on a child resource based on a parent resource role, you can define a shorthand rule in the child resource block. For example, in GitClub the "owner" role on Organization resources grants a user the "admin" role on every Repository within the organization: main.polarallow(actor, action, resource) if has_permission(actor, action, resource); resource Organization { permissions = ["read", "add_member"]; roles = ["member", "owner"]; } resource Repository { permissions = ["read", "push"]; roles = ["contributor", "maintainer", "admin"]; relations = { parent: Organization }; "admin" if "owner" on "parent"; } You can also use shorthand rules to grant permissions based on parent resource roles and permissions. For example, we could add that users with the Organization "member" role can "read" every repository in the organization: main.polar# ... resource Repository { # ... "admin" if "owner" on "parent"; "read" if "member" on "parent"; } We could also modify that rule to say that users who have the Organization "read" permission have the "read" permission on every repository in the organization. main.polar# ... resource Repository { # ... "admin" if "owner" on "parent"; "read" if "read" on "parent"; }
More

Read more guides here

Set up a 1x1 with an Oso Engineer

Our team is happy to help you get started with Oso. If you'd like to learn more about using Oso in your app or have any questions, schedule a 1x1 with an Oso engineer.


Was this page useful?