Rather than converting your authorization data to facts and sending it to Oso
with every authorization request, you can use Local
Authorization. This will instruct Oso to
construct a query to fetch that data directly from your database.
Setup with two requirements:
- A configuration file that maps your facts to SQL queries
- Use the Local Authorization API in your application code
Write the configuration file
You configure Local Authorization with a yaml file passed to the Oso Cloud
client on instantiation. This configuration file lists the fact signatures
used for your authorization queries. It also 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 to include in the config file.
local_authorization_config.yaml
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
Note the following:
- Any value returned by the query is represented in the fact signature by using the wildcard character (
_
).
- Any value 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 Local Authorization API
You provide this configuration file to the Oso Cloud client when you
instantiate it. Then you use the Local Authorization
API to make authorization decisions
without passing data to Oso Cloud.
In the Typescript SDK, you use authorizeLocal
in place of authorize
.
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("local_authorization_config.yaml"),
});
// 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 local_authorization_config.yaml
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;
}
Major changes are highlighted:
- The
osoClient
instantiation is modified to include the local_authorization_config.yaml
file.
- The call to
authorize()
is replaced with a call to authorizeLocal()
- The query returned from
authorizeLocal()
is executed against the database to resolve the authorization request
Notice that the canReadRepo()
function is smaller. This is because we do not need code in that function
to get role data and convert it to facts. It is 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.
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
The results are identical to our previous authorization code.
All that remains is to replace the original authorization logic with the Oso Cloud version.