Quickstart

oso helps developers build authorization into their applications. If you’ve never used oso before and want to get up-and-running quickly, this guide is for you.

In general, it takes less than 5 minutes to add oso to an existing application and begin writing an authorization policy.

In this guide, we’re going to add oso to our project, write our first policy, create a simple web server with no authorization, and write some rules for it. We encourage you to code along in your local environment!

Expenses Application

Our application serves data about expenses submitted by users.

To start with, we have a simple Expense class, and some stored data in the EXPENSES dictionary.

expense.py (link)
from dataclasses import dataclass


@dataclass
class Expense:
    amount: int
    description: str
    submitted_by: str


EXPENSES = {
    1: Expense(500, "coffee", "alice@example.com"),
    2: Expense(5000, "software", "alice@example.com"),
    3: Expense(50000, "flight", "bhavik@example.com"),
}
expense.rb (link)
class Expense
  attr_reader :amount, :description, :submitted_by

  def initialize(amount:, description:, submitted_by:)
    @amount = amount
    @description = description
    @submitted_by = submitted_by
  end
end

EXPENSES = {
  1 => Expense.new(amount: 500, description: "coffee", submitted_by:   "alice@example.com"),
  2 => Expense.new(amount: 5000, description: "software", submitted_by: "alice@example.com"),
  3 => Expense.new(amount: 50_000, description:"flight", submitted_by: "bhavik@example.com")
}
expense.java (link)
public class Expense {
    public int amount;
    public String description;
    public String submittedBy;

    public static Expense[] EXPENSES = {
        new Expense(500, "coffee", "alice@example.com"),
        new Expense(5000, "software", "alice@example.com"),
        new Expense(50000, "flight", "bhavik@example.com"),
    };


    public Expense(int amount, String description, String submittedBy) {
        this.amount = amount;
        this.description = description;
        this.submittedBy = submittedBy;
    }

    public String toString() {
        return String.format("Expense(amount=%d, description=%s, submittedBy=%s)",
            this.amount, this.description, this.submittedBy);
    }
}

We’ll need our application to be able to control who has access to this data. Before we add a web server and start making some requests, lets see if we can get some basic authorization in place!

Adding oso

Installation

In order to write our first authorization policy, we first need to add oso to our application. If you don’t already have it installed, go ahead and do so now:

oso v0.3.0 supports Python versions >= 3.6

$ pip install oso==0.3.0

oso v0.3.0 supports Ruby versions >= 2.4

$ gem install oso-oso -v 0.3.0

oso v0.3.0 supports Java versions >= 10

Go to the Maven Repository and download the latest jar.

Either add this to your Java project libraries, load using your IDE, or build from the command line with:

$ javac -cp oso-0.3.0.jar:. Expense.java

Now that we’ve installed oso, let’s see how to make some basic authorization decisions.

Decisions, decisions…

The Oso instance exposes a method to evaluate allow rules that takes three arguments, actor, action, and resource:

actor = "alice@example.com"
resource = EXPENSES[1]
oso.is_allowed(actor, "GET", resource)
actor = 'alice@example.com'
resource = EXPENSES[1]
OSO.allowed?(actor: actor, action: 'GET', resource: resource)
String actor = "alice@example.com";
Expense resource = Expenses.EXPENSES[1];
boolean allowed = oso.isAllowed(actor, "GET", resource);

The above method call returns true if the actor "alice@example.com" may perform the action "GET" on the resource EXPENSES[1]. We’re using "GET" here to match up with the HTTP verb used in our server, but this could be anything.

Note

For more on actors, actions, and resources, check out Glossary.

oso’s authorization system is deny-by-default. Since we haven’t yet written any policy code, Alice is not allowed to view expenses. To see that in action, start a REPL session and follow along:

Run: python

>>> from expense import *
>>> from oso import Oso
>>> oso = Oso()
>>> alice = "alice@example.com"
>>> expense = EXPENSES[1]
>>> oso.is_allowed(alice, "GET", expense)
False

We can create a new policy file, and explicitly allow Alice to GET any expense…

expenses.polar
allow("alice@example.com", "GET", _expense);

…which we can load into our oso instance:

>>> oso.load_file("expenses.polar")

…and now Alice has the power…

>>> oso.is_allowed(alice, "GET", expense)
True

…and everyone else is still denied:

>>> oso.is_allowed("bhavik@example.com", "GET", expense)
False

Run: irb

irb(main):001:0> require './expense'
=> true
irb(main):002:0> require 'oso'
=> true
irb(main):003:0> OSO = Oso.new
=> #<Oso::Oso:0x00007ffc9c8c6b58>
irb(main):004:0> alice = "alice@example.com"
=> "alice@example.com"
irb(main):005:0> expense = EXPENSES[1]
=> #<Expense:0x00007ffc9c916388 @amount=500, @description="coffee", @submitted_by="alice@example.com">
irb(main):006:0> OSO.allowed?(actor: alice, action: "GET", resource: expense)
=> false

We can create a new policy file, and explicitly allow Alice to GET any expense…

expenses.polar
allow("alice@example.com", "GET", _expense);

…which we can load into our oso instance:

irb(main):005:0> OSO.load_file("expenses.polar")
=> #<Set: {"expenses.polar"}>

…and now Alice has the power…

irb(main):005:0> OSO.allowed?(actor: alice, action: "GET", resource: expense)
=> true

…and everyone else is still denied:

irb(main):006:0> OSO.allowed?(actor: "bhavik@example.com", action: "GET", resource: expense)
=> false

To follow along, either try using jshell (requires Java version >= 9) or copy the follow code into a main method in Expense.java.

Run: jshell --class-path oso-0.3.0.jar Expense.java

Expense.java
import com.osohq.oso.Oso;

public class Expense {
    // ...

    public static void main(String[] args) throws Exception {
        Oso oso = new Oso();
        String alice = "alice@example.com";
        Expense expense = Expense.EXPENSES[1];
        System.out.println(oso.isAllowed(alice, "GET", expense));
    }
}

Should output:

false
jshell> import com.osohq.oso.Oso;

jshell> Oso oso = new Oso();
oso ==> com.osohq.oso.Oso@55b699ef

jshell> String alice = "alice@example.com"
alice ==> "alice@example.com"

jshell> Expense expense = Expense.EXPENSES[1]
expense ==> Expense(amount=5000, description=software, submittedBy=alice@example.com)

jshell> oso.isAllowed(alice, "GET", expense)
$12 ==> false

We can create a new policy file, and explicitly allow Alice to view expenses

expenses.polar
allow("alice@example.com", "GET", _expense);

We can load into our oso instance, and then see that Alice has the power and everyone else is still denied:

Expense.java
public static void main(String[] args) throws Exception {
    Oso oso = new Oso();
    oso.loadFile("expenses.polar");
    String alice = "alice@example.com";
    String bhavik = "bhavik@example.com";
    Expense expense = Expense.EXPENSES[1];
    System.out.println(oso.isAllowed(alice, "GET", expense));
    System.out.println(oso.isAllowed(bhavik, "GET", expense));
}

Should output:

true
false
jshell> oso.loadFile("expenses.polar")

jshell> oso.isAllowed(alice, "GET", expense)
$14 ==> true

jshell> oso.isAllowed("bhavik@example.com", "GET", expense)
$15 ==> false

Note

Each time you load a file, it will load the policy without clearing previously loaded rules. Be sure to clear oso using the clear method or create a new instance if you want to try adding a few new rules.

When we ask oso for a policy decision via allow, the oso engine searches through its knowledge base to determine whether the provided actor, action, and resource satisfy any allow rules.

In the above case, we passed in alice as the actor, "GET" as the action, and EXPENSE[1] as the resource, satisfying the allow("alice@example.com", "GET", _expense); rule. When we pass in "bhavik@example.com" as the actor, the rule no longer succeeds because the string "bhavik@example.com" does not match the string "alice@example.com".

Authorizing HTTP Requests

Now that we are confident we can control access to our expense data, let’s see what it would look like in a web server. Our web server contains some simple logic to filter out bad requests and not much else.

In lieu of setting up real identity and authentication systems, we’ll use a custom HTTP header to indicate that a request is “authenticated” as a particular user. The header value will be an email address, e.g., "alice@example.com". We’ll pass it to allow as the actor and we’ll use the HTTP method as the action.

Finally, the resource is the expense retrieved from our stored expenses.

Here is the code for our web server. The highlighted lines show where we added oso:

server.py (link)
from http.server import HTTPServer, BaseHTTPRequestHandler
from oso import Oso

from expense import Expense, EXPENSES

oso = Oso()
oso.load_file("expenses.polar")


class RequestHandler(BaseHTTPRequestHandler):
    def _respond(self, msg, code=200):
        self.send_response(code)
        self.end_headers()
        self.wfile.write(str(msg).encode())
        self.wfile.write(b"\n")

    def do_GET(self):
        actor = self.headers.get("user", None)
        action = "GET"

        try:
            _, resource_type, resource_id = self.path.split("/")
            if resource_type != "expenses":
                return self._respond("Not Found!", 404)
            resource = EXPENSES[int(resource_id)]
            if oso.is_allowed(actor, action, resource):
                self._respond(resource)
            else:
                self._respond("Not Authorized!", 403)
        except (KeyError, ValueError) as e:
            self._respond("Not Found!", 404)


server_address = ("", 5050)
httpd = HTTPServer(server_address, RequestHandler)
if __name__ == "__main__":
    print("running on port", httpd.server_port)
    httpd.serve_forever()
server.rb (link)
require "oso"
require "webrick"

require "./expense"

OSO ||= Oso.new
OSO.load_file("expenses.polar")

server = WEBrick::HTTPServer.new Port: 5050
server.mount_proc "/" do |req, res|
  actor = req.header["user"]&.first
  action = req.request_method
  _, resource_type, resource_id = req.path.split("/")
  resource = EXPENSES[resource_id.to_i]

  if resource_type != "expenses" || resource.nil?
    res.body = "Not Found!"
  elsif OSO.allowed?(actor: actor, action: action, resource: resource)
    res.body = resource.inspect
  else
    res.body = "Not Authorized!"
  end
end
server.start if __FILE__ == $0
Server.java (link)
import java.io.*;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.*;
import com.osohq.oso.Oso;

public class Server implements HttpHandler {
    private Oso oso;

    public Server() throws Exception {
        oso = new Oso();
        oso.loadFile("expenses.polar");
    }

    private void respond(HttpExchange exchange, String message, int code) throws IOException {
        exchange.sendResponseHeaders(code, message.length() + 1);
        OutputStream outputStream = exchange.getResponseBody();
        outputStream.write(message.getBytes());
        outputStream.write("\n".getBytes());
        outputStream.flush();
    }

    @Override
    public void handle(HttpExchange exchange) throws IOException {
        try {
            String actor = exchange.getRequestHeaders().get("user").get(0);
            String action = exchange.getRequestMethod();
            String[] request = exchange.getRequestURI().toString().split("/");
            if (!request[1].equals("expenses")) {
                respond(exchange, "Not Found!", 404);
                return;
            }
            Integer index = Integer.parseInt(request[2]) - 1;
            Expense resource = Expense.EXPENSES[index];
            if (oso.isAllowed(actor, action, resource)) {
                respond(exchange, resource.toString(), 200);
            } else {
                respond(exchange, "Not Authorized!", 403);
            }
        } catch (Exception e) {
            respond(exchange, "Not Found!", 404);
        }
    }

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 5050), 0);
        server.createContext("/", new Server());
        server.start();
        System.out.println("Server running on " + server.getAddress());
    }
}

If the request path matches the form /expenses/:id and :id is the ID of an existing expense, we respond with the expense data. Otherwise, we return "Not Found!".

Let’s use cURL to check that everything’s working. We’ll first start our server…

$ python server.py
running on port 5050
$ ruby server.rb
[2020-07-15 00:35:52] INFO  WEBrick 1.3.1
[2020-07-15 00:35:52] INFO  ruby 2.4.10 (2020-03-31) [x86_64-linux]
[2020-07-15 00:35:52] INFO  WEBrick::HTTPServer#start: pid=537647 port=5050
$ javac -cp oso-0.3.0.jar:. Server.java
$ java -cp oso-0.3.0.jar:. Server
Server running on /127.0.0.1:5050

…and then, in another terminal, we can test everything works by making some requests:

$ curl -H "user: alice@example.com" localhost:5050/expenses/1
Expense(amount=500, description='coffee', submitted_by='alice@example.com')
$ curl -H "user: bhavik@example.com" localhost:5050/expenses/1
Not Authorized!

If you aren’t seeing the same thing, make sure you created your policy correctly in expenses.polar.

Rules Over Dynamic Data

It’s nice that Alice can view expenses, but it would be really onerous if we had to write a separate rule for every single actor we wanted to authorize. Luckily, we don’t!

Let’s replace our static rule checking that the provided email matches "alice@example.com" with a dynamic one that checks that the provided email ends in "@example.com". That way, everyone at Example.com, Inc. will be able to view expenses, but no one outside the company will be able to:

expenses.polar
allow(actor, "GET", _expense) if
    actor.endswith("@example.com");

We bind the provided email to the actor variable in the rule head and then perform the .endswith("@example.com") check in the rule body. If you noticed that the .endswith call looks pretty familiar, you’re right on — oso is actually calling out to the str.endswith method defined in the Python standard library. The actor value passed to oso is a Python string, and oso allows us to call any str method from Python’s standard library on it.

And that’s just the tip of the iceberg. You can register any application object with oso and then leverage it in your application’s authorization policy. In the next section, we’ll update our existing policy to leverage the Expense class defined in our application.

expenses.polar
allow(actor, "GET", _expense) if
    actor.end_with?("@example.com");

We bind the provided email to the actor variable in the rule head and then perform the .end_with?("@example.com") check in the rule body. If you noticed that the .end_with? call looks pretty familiar, you’re right on — oso is actually calling out to the String#end_with? method defined in the Ruby standard library. The actor value passed to oso is a Ruby string, and oso allows us to call any String method from Ruby’s standard library on it.

And that’s just the tip of the iceberg. You can register any application object with oso and then leverage it in your application’s authorization policy. In the next section, we’ll update our existing policy to leverage the Expense class defined in our application.

expenses.polar
allow(actor, "GET", _expense) if
    actor.endsWith("@example.com");

We bind the provided email to the actor variable in the rule head and then perform the .endsWith("@example.com") check in the rule body. If you noticed that the .endsWith call looks pretty familiar, you’re right on — oso is actually calling out to the String.endsWith? method defined in the Java standard library. The actor value passed to oso is a Java string, and oso allows us to call any String method from Java’s standard library on it.

And that’s just the tip of the iceberg. You can register any application object with oso and then leverage it in your application’s authorization policy. In the next section, we’ll update our existing policy to leverage the Expense class defined in our application.

Once we’ve added our new dynamic rule and restarted the web server, every user with an @example.com email should be allowed to view any expense:

$ curl -H "user: bhavik@example.com" localhost:5050/expenses/1
Expense(...)

If a user’s email doesn’t end in "@example.com", the rule fails, and they are denied access:

$ curl -H "user: bhavik@foo.com" localhost:5050/expenses/1
Not Authorized!

Writing Authorization Policy Over Application Data

At this point, the higher-ups at Example.com, Inc. are still not satisfied with our access policy that allows all employees to see each other’s expenses. They would like us to modify the policy such that employees can only see their own expenses.

To accomplish that, we can replace our existing rule with:

expenses.polar
allow(actor, "GET", expense) if
    expense.submitted_by = actor;
expenses.polar
allow(actor, "GET", expense) if
    expense.submitted_by = actor;
expenses.polar
allow(actor, "GET", expense) if
    expense.submittedBy = actor;

Behind the scenes, oso looks up the submitted_by field on the provided Expense instance and compares that value against the provided actor. And just like that, an actor can only see an expense if they submitted the expense.

Now Alice can see her own expenses but not Bhavik’s:

$ curl -H "user: alice@example.com" localhost:5050/expenses/1
Expense(...)
$ curl -H "user: alice@example.com" localhost:5050/expenses/3
Not Authorized!

And vice-versa:

$ curl -H "user: bhavik@example.com" localhost:5050/expenses/1
Not Authorized!
$ curl -H "user: bhavik@example.com" localhost:5050/expenses/3
Expense(...)

We encourage you to play around with the current policy and experiment with adding your own rules!

For example, if you have Expense and User classes defined in your application, you could write a policy rule in oso that says a User may approve an Expense if they manage the User who submitted the expense and the expense’s amount is less than $100.00:

allow(approver, "approve", expense) if
    approver = expense.submitted_by.manager
    and expense.amount < 10000;

In the process of evaluating that rule, the oso engine would call back into the application in order to make determinations that rely on application data, such as:

  • Which user submitted the expense in question?

  • Who is their manager?

  • Is their manager the approver?

  • Does the expense’s amount field contain a value less than $100.00?

Note

For more on leveraging application data in an oso policy, check out Application Types.

Summary

We just went through a ton of stuff:

  • Installing oso.

  • Setting up our app to enforce the policy decisions made by oso.

  • Writing authorization rules over static and dynamic application data.

What’s next