Extract a piece of authorization logic

Extract a piece of authorization logic

Identify the logic to extract

The first thing to do is to select a piece of authorization logic to move to Oso Cloud. For your first piece of code this should be:

  • Well understood
  • Low-impact
  • Straightforward to extract

Throughout this guide, we'll illustrate the process with code samples from a hypothetical version control application. You can follow along with those if you'd like, or you can use examples from your own code.

One permission this application needs to grant users is the ability to read a repository. That permission affects two operations:

  1. read a single repository
  2. list all the repositories that the user can read

You can see the logic in the corresponding get handlers:


// Return the requested repository if the user has permission to read it
accountsRouter.get(
"/orgs/:orgId/repos/:repoId",
withAuthn,
async (req, res) => {
const user = await currentUser(req);
const orgRole = user.orgRoles.find(
(orgRole) => orgRole.orgId == parseInt(req.params.orgId)
);
const repoRole = user.repoRoles.find(
(repoRole) => repoRole.repoId == parseInt(req.params.repoId)
);
const repo: WithPermissions<Repository> | null =
await req.prisma.repository.findUnique({
where: {
orgId: parseInt(req.params.orgId),
id: parseInt(req.params.repoId),
},
});
if (repo && (orgRole || repoRole)) {
repo.permissions = ["read"];
...
}
}
);


// List all repos that the user can read in the specified org
accountsRouter.get("/orgs/:orgId/repos", withAuthn, async (req, res) => {
let repos = await req.prisma.repository.findMany({
where: { orgId: parseInt(req.params.orgId) },
});
const user = await currentUser(req);
const memberOfOrg = user.orgRoles.some(
(orgRole) => orgRole.orgId == parseInt(req.params.orgId)
);
repos = repos.filter((repo) => {
return (
memberOfOrg ||
user.repoRoles.some((repoRole) => repoRole.repoId == repo.id)
);
});
res.send(repos);
});

This is fairly typical of home-grown authorization code:

  • The same logic is implemented in multiple places
  • It's implemented slightly differently in each place
  • It's not obvious that it's authorization logic

This is also a good candidate for early refactoring:

  • The logic is straightforward - a user can read a repository if:
    • They have any role on the repository
    • They have any role on the repository's parent organization
  • It only grants read access.
  • The logic is already somewhat encapsulated, so it's easy to pull out of its containing functions.

Extract the logic into a dedicated function.

Now that you've identified a piece of logic to refactor, you can extract it into a dedicated function. This will decouple the authorization logic from the surrounding application logic. Once you've done that, you'll be able to work with the authorization logic in isolation.

Create a function called canReadRepo() in a new file called authz.ts to encapsulate this logic. That will make it obvious which permission it governs.

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;
}

Now you can just call this function when you need to authorize a user's request to read a repository.

src/routes/accounts.ts

import { canReadRepo } from "../authz";
// List all repos that the user can read in the specified org
accountsRouter.get("/orgs/:orgId/repos", withAuthn, async (req, res) => {
let repos = await req.prisma.repository.findMany({
where: { orgId: parseInt(req.params.orgId) },
});
const user = await currentUser(req);
repos = repos.filter((repo) => {
return canReadRepo(user, repo);
});
res.send(repos);
});

src/routes/accounts.ts

// Return the requested repository if the user has permission to read it
accountsRouter.get(
"/orgs/:orgId/repos/:repoId",
withAuthn,
async (req, res) => {
const user = await currentUser(req);
const repo: WithPermissions<Repository> | null =
await req.prisma.repository.findUnique({
where: {
orgId: parseInt(req.params.orgId),
id: parseInt(req.params.repoId),
},
});
if (repo && canReadRepo(user, repo)) {
repo.permissions = ["read"];
...
}
}
);

Even this small change provides some meaningful benefits:

  • There is a dedicated home for authorization logic (src/authz.ts).
  • The logic that drives the "read repository" permission is easy to find, understand, and reason about.
  • That logic is only defined once.
  • The application code is cleaner.

Next, you'll implement the logic in Oso Cloud.