Send data to Oso Cloud as context facts

Send data to Oso Cloud as context facts

In order for Oso to render authorization decisions, it needs data from your application. Oso accepts data as facts: a lightweight, flexible format that complements logic written in Polar.

You can send facts to Oso Cloud along with an authorization request. Facts sent to Oso Cloud in this way are called context facts. The syntax depends on which client SDK you're using, so check the docs for your language for details. In the TypeScript SDK, you supply them as an optional 4th argument to the oso.authorize call.

src/authz.ts

import { UserWithRoles } from "../authn";
import { Repository } from "@prisma/client";
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 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);
// 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 osoOrg = { type: "Organization", id: repo.orgId.toString() };
// get roles for context facts
const osoOrgRole = user.orgRoles
.filter((orgRole) => orgRole.orgId == repo.orgId)
.pop();
const osoRepoRole = user.repoRoles
.filter((repoRole) => repoRole.repoId == repo.id)
.pop();
// define context facts
let contextFacts: [
string,
{ type: string; id: string },
string,
{ type: string; id: string }
][] = [
// fact format: has_relation(Repository: repoId, "parent", Organization: orgId)
["has_relation", osoRepo, "parent", osoOrg],
];
if (osoOrgRole) {
// has_role(User: userId, role, Organization: orgId)
contextFacts.push(["has_role", osoUser, osoOrgRole.role, osoOrg]);
}
if (osoRepoRole) {
// has_role(User: userId, role, Repository: repoId)
contextFacts.push(["has_role", osoUser, osoRepoRole.role, osoRepo]);
}
// authorize using oso.authorize
const authorizedOso = await osoClient.authorize(
osoUser,
"read",
osoRepo,
contextFacts
);
console.log(
`User:${user.id} read Repository:${repo.id}: inline: ${authorizedInline}; Oso: ${authorizedOso}`
);
return authorizedInline;
}

There's some new syntax here for defining context facts and sending them to Oso Cloud. If it's unfamiliar, you can learn more in the context facts documentation for your language. The important thing to know is that this code builds up an array of up to three facts:

  • A fact that establishes the relationship between the Repository and its parent Organization

    ["has_relation", osoRepo, "parent", osoOrg];

  • [Optional] A fact that establishes the User's role in the Organization

    ["has_role", osoUser, osoOrgRole.role, osoOrg];

  • [Optional] A fact that establishes the User's role on the Repository

    ["has_role", osoUser, osoRepoRole.role, osoRepo];

That array is then passed as the fourth argument to .authorize() to supply the facts to Oso Cloud at evaluation time.


const authorizedOso = await osoClient.authorize(
osoUser,
"read",
osoRepo,
contextFacts // <--- Context facts provided to .authorize()
);

Now the results from Oso Cloud are the same as the results from the original code.


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

Context facts are a handy way to provide your data to Oso Cloud, because you don't have to worry about building extra logic or tooling to synchronize your application data to Oso Cloud as it changes. You can just send it whenever you make a requests.

However, over time this creates a lot of additional network traffic as you make more requests. Much of that traffic is unnecessary, because this data generally doesn't change frequently.

The good news is that you can instead tell the Oso Cloud client how to convert data in your application database directly into facts when it needs that data to resolve an authorization request. This is called Local Authorization, and you'll set that up next.