Use Oso Cloud for authorization

Authorize with Oso Cloud

Now it's time to use Oso Cloud for the live authorization calls. First, you'll remove the old code. After that, you have a number of options for enhancing the Polar code and for making your authorization even more robust.

Remove the old code

After deleting the inline authorization code from canReadRepo(), the function looks like this:

backend/src/authz.ts

import { resolve } from "path";
import { UserWithRoles } from "../authn";
import { Repository, PrismaClient } from "@prisma/client";
import { Oso } from "oso-cloud";
import * as 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 oso_url = process.env.OSO_URL
? process.env.OSO_URL
: "https://cloud.osohq.com";
const osoClient = new Oso(oso_url, process.env.OSO_API_KEY, {
dataBindings: resolve("oso-data-bindings.yml"),
});
// A user can read a repo if they have any role on the repo or its parent organization.
export async function canReadRepo(
prisma: PrismaClient,
user: UserWithRoles,
repo: Repository
): Promise<boolean> {
// entities for Oso
const osoUser = { type: "User", id: user.id.toString() };
const osoRepo = { type: "Repository", id: repo.id.toString() };
// Call authorizeLocal() to return a facts query
// derived from configuration in oso-data-bindings.yml
const query = await osoClient.authorizeLocal(osoUser, "read", osoRepo);
// Run the query
const rows = await prisma.$queryRawUnsafe<oso.AuthorizeResult[]>(query);
// Save the result to authorizedOso
const authorized = rows[0].allowed;
return authorized;
}

The authorization code does 4 things:

  • Instantiate the Oso Cloud client with a data mapping configuration file
  • Create User and Repository entities
  • Call authorizeLocal() to get a query that will yield the authorization result
  • Return the result of the query as the authorization result

And that's it. You've replaced a piece of homegrown authorization logic with Oso Cloud, and you did so without disrupting your existing functionality. The resulting authorization code is explicit, concise, and easy to understand. Where can you go from here?

Next steps

Add policy tests

Oso Cloud allows you to write tests to validate that your policy behaves as you expect. This gives you confidence that your existing functionality remains intact as you extend your policy. Add the following test to validate the "read repository" action:


test "a user with a role on a repository or its parent org can read the repo" {
setup {
has_role(User{"alice"}, "admin", Organization{"acme"});
has_role(User{"bob"}, "reader", Repository{"cool-app"});
has_relation(Repository{"cool-app"}, "parent", Organization{"acme"});
has_relation(Repository{"org-configs"}, "parent", Organization{"acme"});
}
assert has_permission(User{"alice"}, "read", Repository{"cool-app"});
assert has_permission(User{"alice"}, "read", Repository{"org-configs"});
assert_not has_permission(User{"alice"}, "read", Repository{"some-other-repo"});
assert has_permission(User{"bob"}, "read", Repository{"cool-app"});
assert_not has_permission(User{"bob"}, "read", Repository{"org-configs"});
assert_not has_permission(User{"bob"}, "read", Repository{"some-other-repo"});
}

Make use of Polar abstractions and shorthand rules

You won't want to leave your authorization logic as has_permission statements for long. Polar provides some powerful features that will make your authorization logic better encapsulated and more concise. Some of these are:

You should incorporate these next. For example, you could modify the existing Polar as follows:

backend/policy.polar

actor User {
}
resource Organization {
roles = ["admin", "member"];
}
resource Repository {
permissions = [
"read",
];
roles = [
"admin",
"maintainer",
"editor",
"reader"
];
relations = {
parent: Organization
};
"read" if role on "parent";
"read" if role;
}

This is functionally equivalent to the previous code, but now things like the available roles and the relationship between repositories and organizations are explicit. If you added a policy test, you can use that to confirm that the logic behaves the same.

Use feature flags

Because you run your existing logic alongside the Oso Cloud logic for a period of time when using this approach, you can use feature flags to migrate from one to the other. This allows you to do a phased rollout so you can monitor app behavior over time, or quickly restore the original functionality if something unexpected happens during the cutover.

Monitor query performance

Local authorization is fast, but as your application grows, you may find that database performance becomes a concern. Because this approach isolates the database operation to a single, distinct line of code, you can easily instrument it and monitor its performance. If you find that a given authorization query starts to become a bottleneck, you can centralize the data for that query in Oso Cloud, and offload the lookup to us.

When you centralize data in Oso Cloud, fact lookups happen on our servers. This reduces load on your application databases, but it requires you to do an initial sync of your data and then keep it up-to-date as it changes. We recommend keeping your data in your application databases for as long as you can so that you only need to maintain a single source of truth for authorization data.

Reach out

If you have any questions or want to bounce ideas off us as you go, we're always here to help and would love to hear from you. Just reach out on Slack (opens in a new tab) and one of our engineers will give you a hand.