Skip to main content
In this tutorial, we’ll cover the three main pieces involved in adding authorization to a real world app with Oso Cloud.
  • Modeling an authorization policy
  • Declaring authorization as facts’
  • Enforcing authorization decisions
This guide is for engineers after completing the quickstart and want to explore how Oso Cloud fits into a production application. The hypothetical app we will explore in this guide is GitCloud, a GitHub/GitLab-like app architected into two microservices. The first is a Python service that manages accounts, organizations, and repositories. The second is a Node.js service that manages GitCloud Jobs, a Continuous Integration / Continuous Deliver (CI/CD) offering. First, we use Oso Cloud’s enforcement APIs to answer authorization queries. For example, “is User:alice allowed to "read" the Repository:foo repository?” Then, we model an authorization policy for Oso Cloud using common Polar building blocks. Finally, we’ll see how to maintain authorization data in Oso Cloud via fact management.

Initial setup

First we need to install Oso Cloud SDKs for GitCloud’s two services
# In the Python project
pip install oso-cloud

# In the Node.js project
yarn add oso-cloud
The clients are stateless, so we initialize a global singleton in each service to avoid repeating the API key:
__init__.py
# In the Python service
import os 

from oso_cloud import Oso
oso = Oso(api_key=os.environ.get("OSO_AUTH", None))
app.js
// In the Node.js service
const { Oso } = require("oso-cloud");
const oso = new Oso("https://api.osohq.com/", process.env["OSO_AUTH"]);
In the Python service, we create a few helpers for calling Oso Cloud’s authorize, list, and actions enforcement APIs. This will avoid some repetitiveness across the various controllers we have to update:
def authorize(action: str, resource: Any) -> bool:
    actor = current_user()
    resource = object_to_oso_value(resource)
    return oso.authorize(actor, action, resource)

def list_resources(action: str, resource_type: str) -> List[str]:
    if g.current_user is None:
        return []
    return oso.list(
        current_user(),
        action,
        resource_type
    )

def actions(resource: Any) -> List[str]:
    if g.current_user is None:
        return []
    actor = current_user()
    resource = object_to_oso_value(resource)
    return oso.actions(actor, resource)
We also define a few helpers for converting our domain objects into the oso_cloud.Value objects that Oso Cloud accepts:
def current_user() -> oso_cloud.Value:
    if g.current_user is None:
        raise Unauthorized
    return object_to_oso_value(g.current_user)

def object_to_oso_value(obj: Any) -> oso_cloud.Value:
    if isinstance(obj, dict):
        return {"type": obj["type"], "id": str(obj["id"])} # make sure ids are stringified
    elif isinstance(obj, User):
        return {"type": "User", "id": str(obj.username)}
    else:
        return {"type": obj.__class__.__name__, "id": str(obj.id)}
With setup complete, you will add authorization checks to our app.

Enforcement

Next, we enforce authorization across both services. Enforcement happens in the logic reacting to the authorization decision in Oso. For example in a HTTP application, 403 Forbidden is returned when the authorization decision is False. 200 OK is returned when the authorization decision is True. We begin with enforcement to implement Oso Cloud incrementally and observe the authorization results change as we progress.
Testing your enforcement setup When implementing authorization enforcement for the first time, it can be helpful to confirm that the integration is working as expected before introducing more policy logic.We’ll do that by implementing a policy that allows everything, then one that denies everything, then observe how the app responds.Oso Cloud’s default (empty) policy denies everything which should return true.With the “allow everything” policy, every request for an access-controlled resource should return true.With the “deny everything” (or empty) policy, every request should return false.If your app behaves this way, it means enforcement is working correctly and your authorization API calls are implemented properlyImportant: This is a testing-only strategy. It should never be used in production environments. Always remember to replace these placeholder policies with your real authorization logic.To push an “allow everything” policy to Oso Cloud via the CLI for testing:
echo "allow(_, _, _);" > authorization.polar
oso-cloud policy authorization.polar
To push a “deny everything” policy:
echo "" > authorization.polar
oso-cloud policy authorization.polar
Some authorization systems support both allow(...) if ...; and deny(...) if ...; semantics. In Oso Cloud, the idiomatic way to express this is by using an allow rule with negation. For example:
allow(actor: Actor, action: String, resource: Resource) if
  has_permission(actor, action, resource) and
  not deny(actor, action, resource);
Here, deny is just a placeholder—like is_banned(actor) or any other condition that should override normal access.

Authorizing an action on a specific resource

Most operations in GitCloud involve a user performing an action on a specific resource. For example:
  • Viewing a repository
  • canceling a job
  • updating an organization
For these scenarios, we add enforcement by calling Oso Cloud’s authorize() API, which checks if an actor is allowed to perform an action on a specific resource. For example, here’s an updated handler for viewing a specific organization:
def show(org_id):
    if not authorize("read", {"type": "Organization", "id": org_id}):
        raise NotFound
    org = g.session.get_or_404(Organization, id=org_id)
    return org.as_json()
We ask Oso Cloud if the current user is allowed to "read" the organization. We use the same pattern for most of the endpoints across both services.
Note that we chose to raise a NotFound error instead of Forbidden. This ensures that someone who doesn’t have access to the repository cannot tell if it exists.

Authorizing an action on a collection of resources

The second most common operation in GitCloud is the “index” endpoint. It displays a collection of resources, such as organizations that a user belongs to or issues opened on a particular repository. The naïve implementation to add enforcement on an index endpoint loops over the collection of resources loaded from the database. Each iteration then uses Oso Cloud’s authorize() API to request an authorization decision. Oso Cloud can wrap those authorization checks into a single efficient list filtering query: the list() API. While authorize() poses the question “can actor perform action on resource?”, list() asks “for which resources of type resource_type can actor perform action?”
For a deeper dive into the list() endpoint, head over to the list filtering guide.
Oso Cloud’s list() API behaves similar to authorize() except that it takes a resource type, e.g., the "Organization" type instead of a specific {"type": "Organization", "id": 1} instance. As an example of adding enforcement for a collection of resources, here is the updated index handler for organizations:
def index():
    authorized_ids = list_resources("read", "Organization")
    orgs = (
        g.session.query(Organization)
        .filter(Organization.id.in_(authorized_ids))
        .order_by(Organization.id)
    )
    return jsonify([o.as_json() for o in orgs])
Oso Cloud returns which organizations the current user can "read" as a the collection of IDs. That collection of IDs can be used to load organization objects from the service database. Fetching a collection of authorized IDs from Oso Cloud’s list() API and then loading those resources from the database is a pattern that can be reused across various endpoints.

Conditional UI elements

The final enforcement pattern covered in this tutorial is conditionally displaying UI elements based on the current user’s permissions. For example, this pattern might gray out a button if the user isn’t allowed to perform the action represented. Imagine this pattern in action with the GitCloud user interface (UI) that includes a page for respository administrators to add and remove users. Users who aren’t administrators should not see any links to it inside the UI. To fetch the set of actions that the current user is allowed to perform on a repository efficiently , use Oso Cloud’s actions() API. Here’s the updated handler for viewing a specific repository:
def show(org_id, repo_id):
    if not authorize("read", {"type": "Repository", "id": repo_id}):
        raise NotFound
    repo = g.session.get_or_404(Repository, id=repo_id, org_id=org_id)
    json = repo.as_json()
    json["permissions"] = actions(repo)
    return json
The call to actions() will return a list of actions that the current user is allowed to perform on the repo repository. We include these in the JSON response, so that our UI can check for the "manage_members" permission. Based on the permission, the UI shows or hides the link to the admin page as necessary.

Modeling

Now that enforcement is implemented across our services, we will model our policy and push it to Oso Cloud.
Polar files can be kept in version control. It also support test for rigorous CI/CD to prevent regressions.
First, we establish the policy we need to express. We will start with Role-based Access Control - a common pattern found in many authorization systems.

Standard RBAC

Role-based Access Control establishes that a user can perform an action on a resource if they have a role for that resource. We’ll start with a simple RBAC base policy.
authorization.polar
actor User {}

resource Repository {
  roles = ["reader"];
  permissions = ["read"];

  "read" if "reader";
}
Actors with the "reader" role on a specific repository can "read" that repository. As we add additional roles and permissions governing access to repositories, we’ll add them to the resource block. New RBAC rules are declared with the <permission> if <role>; form. We employ the exact same pattern to fill out the base policy for the other access-controlled types in our app: organizations, issues, jobs, and users.

Resource hierarchies / multitenancy

Hierarchy patterns often emerge in applications like our GitCloud example. One such hierarchy would be an organization having many repositories and a repository having many issues and jobs. In these cases, access flows from parent resources to child resources. If a user has the "member" role on the ACME organization, the user should have the same access to one of ACME’s repositories as a user with the "reader" role on that specific repository. The naïve implementation grants each "member" an explicit "reader" role on each repository in the ACME org. Instead of managing so many extra roles, we can define a relationship between the Organization and Repository types. In it, we declare that an actor with the "member" role on a repository’s parent organization can do anything a "reader" can do on the repository. The policy is updated with to reflect this hierarchy.
authorization.polar
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = ["reader"];
  permissions = ["read"];
  relations = { organization: Organization };

  "read" if "reader";

  "reader" if "member" on "organization";
}

Sharing

In our hypothetical GitCloud app, users should be able to invite others to organizations and repositories. Managing the roles of users who have access to a particular organization or repository is another requirement. This is pattern is common and can be implemented with resource ownership in Polar.
authorization.polar
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = [
    "reader",
    "admin",
  ];
  permissions = [
    "read",
    "manage_members",
  ];
  relations = { organization: Organization };

  "read" if "reader";

  "manage_members" if "admin";

  "reader" if "member" on "organization";
}

Ownership

The final pattern we implement for GitCloud is ownership. For this example, we will implement authorization for closing issues. A user should be able to close any issue they create. Looking at the Ownership in RBAC example, the easiest way to represent ownership is with a role on a resource.
authorization.polar
actor User {}

resource Organization {
  roles = ["member"];
  permissions = ["read", "create_repositories"];

  "create_repositories" if "member";
  "read" if "member";
}

resource Repository {
  roles = [
    "reader",
    "admin",
  ];
  permissions = [
    "read",
    "manage_members",
  ];
  relations = { organization: Organization };

  "read" if "reader";

  "manage_members" if "admin";

  "reader" if "member" on "organization";
}

resource Issue {
  roles = ["creator"];
  permissions = ["read", "close"];
  relations = { repository: Repository };

  "close" if "creator";

  "read" if "reader" on "repository";
}

Pushing the policy to Oso Cloud

Finally, we’re ready to push our initial policy, saved as authorization.polar, with the Oso Cloud CLI:
oso-cloud policy authorization.polar
There are many patterns that apply to GitCloud. If you’re looking for a challenge or to improve your authorization understanding, try cloning the GitCloud repo and implementing a new pattern from the Polar documentation.

Data management

The final step of implementing authorization with Oso Cloud is to provide authorization data as facts. In the modeling step, we defined the abstract authorization policy for GitCloud. Now we need to start provide role assignments (User:alice has the "admin" role on Repository:foo) and relationships (Organization:acme is the "organization" of Repository:foo) to Oso Cloud. When we create a new resource in GitCloud, such as a new repository or organization, we must supply Oso Cloud data about role assignments and relationships pertaining to the new resource. For example, when a user creates a new repository, Oso Cloud requires facts that the user has the "admin" role on that repository and an "organization" relationship between the repository and its parent organization exists. In Oso Cloud, role assignments persist as has_role facts of the form has_role(actor, role, resource), e.g., has_role(User:alice, "admin", Repository:foo). Similarly, relationships are represented as has_relation facts of the form has_relation(related_resource, relation, resource), e.g., has_relation(Repository:foo, "organization", Organization:acme). To provide the pair of new facts to Oso Cloud when creating a new repository, we’ll use the Python client’s batch().
def create(org_id):
    org_value = {"type": "Organization", "id": str(org_id)}
    if not authorize("read", org_value):
        raise NotFound
    if not authorize("create_repositories", org_value):
        raise Forbidden("you do not have permission to create repositories")

    payload = request.get_json(force=True)

    repo = Repository(name=payload["name"], org_id=org_id)
    g.session.add(repo)
    g.session.commit()

    repo_value = {"type": "Repository", "id": str(repo.id)}
    with oso.batch() as txt:
        tx.insert({ "name": "has_relation",
                    "args": [repo_value, "organization", org_value] })
        tx.insert({ "name": "has_role",
                     "args": [current_user(), "admin", repo_value] })
    return repo.as_json(), 201
If we only needed to create a single fact, we could use the tell() API.
We also need to update or delete facts from Oso Cloud in order to keep GitCloud’s authorization data up-to-date. For example, when a user is removed from an organization, we need to delete any role assignments in Oso Cloud. We could list and delete facts individually, but instead we’ll use Oso Cloud’s batch() API.
def org_delete(org_id):
    payload = request.get_json(force=True)
    org_value = {"type": "Organization", "id": str(org_id)}
    permissions = actions(org_value)
    if not "read" in permissions:
        raise NotFound
    elif not "manage_members" in permissions:
        raise Forbidden

    org = g.session.get_or_404(Organization, id=org_id)

    user = {"type": "User", "id": payload["username"]}
    with oso.batch() as tx:
        tx.delete([{"name": "has_role", "args": [user, None, org_value]}])

    return {}, 204
In the batch() API, the None argument acts as a wildcard, instructing Oso Cloud to delete any roles the user may have on the given organization. Using the batch() API is useful when there are multiple roles for a user. If we needed to delete a single fact, we use the delete() API instead. Additionally, if wildcards are undeed, you can delete many facts at once with pairing batch() with delete().

Using context facts

Sometimes it is helpful to be able to provide more authorization data to Oso Cloud when requesting an authorization decision. For example, the python GitCloud service is the only service that knows about issues which are far more numerous than repositories or organizations. Do we have to store issue-related facts in Oso Cloud? In situations like these, we can take advantage of context facts. Context facts are considered only for the duration of a single request. Rather than using the tell() and delete() APIs to sync data about issues to Oso Cloud, we can pass context facts into any of the authorization APIs, including authorize() and list(). Update our authorize() helper function to fetch authorization-relevant data about issues from our database and pass it along as context facts:
def authorize(action: str, resource: Any) -> bool:
    actor = current_user()
    resource = object_to_oso_value(resource)
    context_facts = []
    if resource["type"] == "Issue":
        context_facts = get_facts_for_issue(resource["id"])
    res = oso.authorize(actor, action, resource, context_facts)
    return res

def get_facts_for_issue(issue_id):
    issue = g.session.get_or_404(Issue, id=issue_id)

    issue_value = {"type": "Issue", "id": str(issue.id)}

    has_parent = {
        "name": "has_relation",
        "args": [
          issue_value,
          "repository",
          {"type": "Repository", "id": str(issue.repo_id)}
        ],
    }
    creator = {
        "name": "has_role",
        "args": [
            {"type": "User", "id": str(issue.creator_id)},
            "creator",
            issue_value,
        ],
    }
    facts = [has_parent, creator]

    return facts
Now we can call authorize("close", issue) in a handler to ensure that the current user is authorized to "close" a given issue. Our helper will pass along two critical has_role and has_relation facts so that Oso Cloud can make the correct authorization decision. Context facts are powerful, but they’re better suited for certain situations than others. Be sure to read more about context facts before using them in your application.

Summary

In this tutorial, we covered the three main pieces involved in adding authorization to a real world app with Oso Cloud:
  • Modeling a policy
  • Managing authorization data as facts
  • Enforcing authorization decisiosn
We used Oso Cloud’s authorize() and list() APIs to answer authorization questions posed about specific resources and collections of resources. We learned how to lean on Polar to model common authorization patterns in Oso Cloud. Finally, we learned how to keep authorization data synchronized in Oso Cloud via fact management APIs.

Next Steps

For next steps, if you’re interested in securing your own app with Oso Cloud, head over to the Enforcement guide. If you want to play around with GitCloud, clone the repo and try implementing some new authorization patterns from the Modeling Patterns reference.