Implement the logic in Oso Cloud

Implement the logic in Oso Cloud

Now you'll implement the authorization logic in Oso Cloud. You'll run the Oso Cloud logic alongside your existing logic to confirm that the Oso Cloud implementation is correct before you replace the original code.

Convert the logic to Polar

In Oso Cloud, you express your application's authorization logic in Polar. Polar is a concise and flexible language that provides powerful abstractions for both authorization models and application entities. But for now, you'll implement the logic with simple permission checks in order to mirror the existing application logic. Once that works, you can improve the logic by adding the appropriate abstractions.

Recall the authorization logic from the previous step:

src/authz.ts

// A user can read a repo if they have any role on the repo or its parent organization.
function canReadRepo(user: UserWithRoles, repo: Repository): boolean {
const orgRole = user.orgRoles.some((orgRole) => orgRole.orgId == repo.orgId);
const repoRole = user.repoRoles.some(
(repoRole) => repoRole.repoId == repo.id
);
return orgRole || repoRole;
}

This says that a user has the read permission on a repository if:

  • they have a role on the repository or
  • they have a role on the repository's parent organization

Let's write this logic in Polar:

policy.polar

actor User {}
resource Organization {}
resource Repository {}
# A user has the read permission on a repository if ...
has_permission(user: User, "read", repository: Repository) if
# ... they have a role on the repository
has_role(user, _, repository);
# A user has the read permission on a repository if ...
has_permission(user: User, "read", repository: Repository) if
# ... they have a role on the repository's parent organization
organization matches Organization and
has_relation(repository, "parent", organization) and
has_role(user, _, organization);

An in-depth treatment of Polar is beyond the scope of this walkthrough, but there are a couple of things that are worth highlighting above:

  • You declare the entities that are involved in authorization logic as actors or resources.
    • An actor is the entity that is requesting a permission (e.g. User)
    • A resource is the entity upon which the actor is requesting the permission (e.g. Organization, Repository)
  • There are two has_permission statements that mirror the logic from our application.

Copy the Polar above and save it to your personal Oso Cloud Workspace (opens in a new tab) (you set this up in the quickstart).

Use the Oso Cloud SDK alongside the existing authorization code

Next, you'll use the Oso Cloud SDK to evaluate authorization requests in your application. Don't replace your existing logic immediately, though. Instead, add the check API call for your language alongside your existing logic so that you can compare the results. In TypeScript, this is authorize()

First, import and instantiate the Oso Cloud client.

src/authz.ts

import { Oso } from "oso-cloud";
// Make sure the API key is defined and instantiate the client
if (!process.env.OSO_API_KEY) {
throw "Missing OSO API key from environment";
}
const osoUrl = process.env.OSO_URL
? process.env.OSO_URL
: "https://cloud.osohq.com";
const osoClient = new Oso(osoUrl, process.env.OSO_API_KEY);

Then, add the authorize() call to the canReadRepo() function, alongside the existing logic.

The call to osoClient.authorize() in the following sample will not return the correct results until you send data to Oso Cloud in the next step. Don't use it in live environments without providing data.

src/auth.tz

// A user can read a repo if they have any role on the repo or its parent organization.
export async function canReadRepo(
user: UserWithRoles,
repo: Repository
): Promise<boolean> {
const orgRole = user.orgRoles.some((orgRole) => orgRole.orgId == repo.orgId);
const repoRole = user.repoRoles.some(
(repoRole) => repoRole.repoId == repo.id
);
const authorizedInline = orgRole || repoRole;
// entities for Oso
const osoUser = { type: "User", id: user.id.toString() };
const osoRepo = { type: "Repository", id: repo.id.toString() };
const authorizedOso = await osoClient.authorize(osoUser, "read", osoRepo);
console.log(
`User:${user.id} read Repository:${repo.id}: inline: ${authorizedInline}; Oso: ${authorizedOso}`
);
throw new Error(
"This is sample code that fails without sending the necessary data to Oso Cloud."
);
return authorizedInline;
}

The authorize() function accepts three arguments: an actor, an action, and a resource. It returns true if the actor has permission to perform the action on the resource and false otherwise.

  • The actor and resource arguments are represented by a type and an id.
    • This is how Oso represents entities in your application in the general case.
  • In the Node SDK, these entities take the form of an object with type and id properties.
    • User object: {type: "User", id: user.id.toString() }
    • Repository object: {type: "Repository", id: repo.id.toString() }
  • The permission argument is a string that contains the name of the permission.
    • "read"

In the sample above, the User and Repository objects are assigned to the osoUser and osoRepository variables, respectively. This simplifies the resulting osoClient.authorize() call.

If you're using a different client library, the form of the actor and resource arguments will be different. Check the documentation for your language's client for details.

Compare the results from Oso Cloud to the original code

By writing the results of the original code and the Oso Cloud SDK to a log, you can compare them to make sure that the results are the same before you switch to using Oso for live authorization. If you compare those results now, you'll see something like this:


backend | User:1 read Repository:1: inline: false; Oso: false
backend | User:1 read Repository:2: inline: true; Oso: false
backend | User:1 read Repository:3: inline: false; Oso: false
backend | User:1 read Repository:4: inline: false; Oso: false
backend | User:1 read Repository:5: inline: false; Oso: false
backend | User:1 read Repository:6: inline: true; Oso: false
backend | User:1 read Repository:7: inline: false; Oso: false

The calls to Oso always return false, even when the original code returns true. That's because Oso doesn't yet know which users have which roles, or which repositories belong to which organizations. To make that happen, you need to send data to Oso Cloud.