Replace context facts with local authorization

Replace context facts with local authorization

Rather than converting your authorization data to facts and sending it to Oso with every authorization request, you can use Local Authorization to tell Oso how to construct a query that will let you fetch that data directly when you need it to resolve an authorization request. Setup requires two things:

  1. A configuration file that maps your facts to SQL queries
  2. Using the distributed check API in your application code

Write the configuration file

You configure Local Authorization by passing a yaml file to the Oso Cloud client when you instantiate it. This configuration file lists the fact signatures that you use for authorization queries and associates them with the SQL that generates the facts from your application data.

Recall that you send up to three context facts to Oso for any "read repository" authorization request:


has_relation(Repository: repoId, "parent", Organization: orgId)
has_role(User: userId, role, Organization: orgId)
has_role(User: userId, role, Repository: repoId)

These are the fact signatures that you'll include in the config file.

oso-data-bindings.yml

facts:
"has_relation(Repository:_, String:parent, Organization:_)":
query: 'SELECT id, "orgId" FROM "Repository"'
"has_role(User:_, String:_, Organization:_)":
query: 'SELECT "userId", role, "orgId" FROM "OrgRole"'
"has_role(User:_, String:_, Repository:_)":
query: 'SELECT "userId", role, "repoId" FROM "RepositoryRole"'
sql_types:
Organization: integer
Repository: integer
User: integer

There are a few things to note:

  • Any value that is returned by the query is represented in the fact signature by using the wildcard character (_).
  • Any value that is not returned from a query must be explicitly specified in the fact signature by its type and value (e.g. String:parent).
  • The sql_types: section is optional, but strongly recommended.

Use the distributed check API

Now you can provide this configuration file to the Oso Cloud client when you instantiate it and use the Distributed Check API to resolve authorization requests without passing the data as facts to Oso Cloud. In the Typescript SDK, this means that you'll use authorizeLocal in place of authorize.

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> {
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() };
// 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 authorizedOso = rows[0].allowed;
console.log(
`User:${user.id} read Repository:${repo.id}: inline: ${authorizedInline}; Oso: ${authorizedOso}`
);
return authorizedInline;
}

The major changes are highlighted:

  • The osoClient instantiation has been modified to include the oso-data-bindings.yml file.
  • The call to authorize() has been replaced with a call to authorizeLocal()
  • The query returned from authorizeLocal() is executed against the database to resolve the authorization request

You'll also notice that the canReadRepo() function is noticeably smaller. This is because we don't need any code in that function to get role data and convert it to facts. That's all handled by the query returned from authorizeLocal() now.

You can write the query that authorizeLocal() returns to a log if you'd like to inspect it.

Now let's confirm that the results from Oso Cloud are still correct.


backend | User:1 read Repository:1: inline: false; Oso: false
backend | User:1 read Repository:2: inline: true; Oso: true
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: true
backend | User:1 read Repository:7: inline: false; Oso: false
backend | User:1 read Repository:8: inline: false; Oso: false
backend | User:1 read Repository:9: inline: false; Oso: false
backend | User:1 read Repository:10: inline: false; Oso: false

Everything looks good! All that remains is to replace the original authorization logic with the Oso Cloud version.