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 a sample version control application. You can follow along with that if you'd like, or you can use examples from your own code. Expand the section below for the starting state that we'll base our examples on.

Starting code for examples.
accounts.ts

import { Request, Router } from "express";
import { Organization, Repository, User } from "@prisma/client";
import { currentUser, currentUserId, withAuthn } from "../../authn";
export const accountsRouter = Router({ mergeParams: true });
type WithPermissions<T> = T & { permissions?: string[] };
// Sessions
accountsRouter.post("/session/login", async (req, res) => {
let user: User | null = null;
if (req.body.username) {
// look up
user = await req.prisma.user.findUnique({
where: {
username: req.body.username,
},
});
} else {
user = (
await req.prisma.user.findMany({
take: 1,
skip: Math.floor(Math.random() * 100), // sure!
})
)[0];
}
if (user === null) {
// @ts-ignore
req.session.user_id = null;
// @ts-ignore
req.session.current_username = null;
res.status(401).send("User does not exist");
return;
} else {
// @ts-ignore
req.session.user_id = user.id;
// @ts-ignore
req.session.current_username = user.username;
res.status(201).send(user);
}
});
accountsRouter.get("/session", withAuthn, async (req, res) => {
// @ts-ignore
const user_id = req.session.user_id;
if (user_id !== null && user_id !== undefined) {
const user = await req.prisma.user.findUnique({
where: {
id: user_id,
},
});
res.send(user);
return;
}
res.send(null);
});
accountsRouter.delete("/session/logout", async (req, res) => {
// @ts-ignore
req.session.user_id = null;
// @ts-ignore
req.session.current_username = null;
res.status(204).send();
});
// Users
accountsRouter.get("/users/:username", withAuthn, async (req, res) => {
const user = await req.prisma.user.findUnique({
where: { username: req.params.username },
});
if (user) {
res.send(user);
} else {
res.status(404).send();
}
});
accountsRouter.get("/users/:username/repos", withAuthn, async (req, res) => {
const user = await req.prisma.user.findUnique({
where: { username: req.params.username },
include: { repoRoles: { include: { repo: true } } },
});
res.send(user?.repoRoles.map((rr) => rr.repo));
});
accountsRouter.get("/users/:username/orgs", withAuthn, async (req, res) => {
const user = await req.prisma.user.findUnique({
where: { username: req.params.username },
include: { orgRoles: { include: { org: true } } },
});
res.send(user?.orgRoles.map((or) => or.org));
});
// Orgs
accountsRouter.get("/orgs", withAuthn, async (req, res) => {
const orgs = await req.prisma.organization.findMany({
orderBy: { id: "asc" },
});
res.send(orgs);
});
accountsRouter.post("/orgs", withAuthn, async (req, res) => {
const org = await req.prisma.organization.create({
data: {
name: req.body.name,
OrgRole: {
create: {
role: "admin",
user: {
connect: {
id: currentUserId(req),
},
},
},
},
},
});
if (org) {
res.status(201).send(org);
} else {
res.status(400).send();
}
});
accountsRouter.get("/orgs/:orgId", withAuthn, async (req, res) => {
const user = await currentUser(req);
let orgRole = user.orgRoles.find(
(role) => role.orgId == parseInt(req.params.orgId)
);
const org: WithPermissions<Organization> | null =
await req.prisma.organization.findUnique({
where: {
id: parseInt(req.params.orgId),
},
});
if (org) {
if (orgRole) {
org.permissions = ["view_members", "create_repositories"];
if (orgRole.role == "admin") {
org.permissions.push("delete");
}
} else {
org.permissions = [];
}
res.send(org);
} else {
res.status(404).send();
}
});
accountsRouter.delete("/orgs/:orgId", withAuthn, async (req, res) => {
const user = await currentUser(req);
let orgRole = user.orgRoles.find(
(role) => role.orgId == parseInt(req.params.orgId)
);
if (orgRole && orgRole.role != "admin") {
res.status(403).send("Forbidden");
return;
}
const deleteIssues = req.prisma.issue.deleteMany({
where: { repo: { orgId: parseInt(req.params.orgId) } },
});
const deleteRepoRoles = req.prisma.repositoryRole.deleteMany({
where: {
repo: { orgId: parseInt(req.params.orgId) },
},
});
const deleteRepos = req.prisma.repository.deleteMany({
where: { orgId: parseInt(req.params.orgId) },
});
const deleteOrgRoles = req.prisma.orgRole.deleteMany({
where: {
orgId: parseInt(req.params.orgId),
},
});
const deleteOrg = req.prisma.organization.delete({
where: {
id: parseInt(req.params.orgId),
},
});
await req.prisma.$transaction([
deleteIssues,
deleteRepoRoles,
deleteRepos,
deleteOrgRoles,
deleteOrg,
]);
res.status(204).send();
});
accountsRouter.get("/orgs/:orgId/user_count", withAuthn, async (_req, res) => {
res.send("100");
});
// Repos
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);
});
accountsRouter.post("/orgs/:orgId/repos", withAuthn, async (req, res) => {
const user = await currentUser(req);
const orgRole = user.orgRoles.find(
(orgRole) => orgRole.orgId == parseInt(req.params.orgId)
);
if (!orgRole) {
res.status(403).send("Forbidden");
return;
}
const repo = await req.prisma.repository.create({
data: {
orgId: parseInt(req.params.orgId),
name: req.body.name,
description: "",
},
});
res.status(201).send(repo);
});
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"];
if (orgRole?.role == "admin" || repoRole?.role == "admin") {
repo.permissions.push(
"view_members",
"manage_members",
"delete",
"create_issues",
"manage_issues"
);
} else if (repoRole?.role == "maintainer") {
repo.permissions.push(
"view_members",
"delete",
"create_issues",
"manage_issues"
);
} else if (repoRole?.role == "editor") {
repo.permissions.push("create_issues", "manage_issues");
} else if (repoRole?.role == "reader") {
repo.permissions.push("create_issues");
}
res.send(repo);
} else {
res.status(404).send();
}
}
);
accountsRouter.delete(
"/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)
);
if (
!(
orgRole?.role == "admin" ||
repoRole?.role == "admin" ||
repoRole?.role == "maintainer"
)
) {
res.status(403).send("Forbidden");
return;
}
const deleteRoles = req.prisma.repositoryRole.deleteMany({
where: {
repoId: parseInt(req.params.repoId),
},
});
const deleteIssues = req.prisma.issue.deleteMany({
where: { repo: { id: parseInt(req.params.repoId) } },
});
const deleteRepo = req.prisma.repository.delete({
where: { id: parseInt(req.params.repoId) },
});
await req.prisma.$transaction([deleteRoles, deleteIssues, deleteRepo]);
res.status(204).send();
}
);
// Role assignments
// not actually used on the frontend
accountsRouter.get(
"/orgs/:orgId/unassigned_users",
withAuthn,
async (req, res) => {
const unassigned_users = await req.prisma.user.findMany({
where: {
// users who do not have roles on this org
orgRoles: {
none: {
orgId: parseInt(req.params.orgId),
},
},
},
});
res.send(unassigned_users);
}
);
accountsRouter.get(
"/orgs/:orgId/role_assignments",
withAuthn,
async (req, res) => {
const assignments = await req.prisma.orgRole.findMany({
where: {
orgId: parseInt(req.params.orgId),
},
include: {
user: true,
},
orderBy: {
id: "asc",
},
});
res.send(assignments);
}
);
accountsRouter.patch(
"/orgs/:orgId/role_assignments",
withAuthn,
async (req, res) => {
const role = await req.prisma.orgRole.update({
where: {
orgId_userId: {
orgId: parseInt(req.params.orgId),
userId: parseInt(req.body.user_id),
},
},
data: {
role: req.body.role,
},
include: {
user: true,
},
});
res.send(role);
}
);
accountsRouter.delete(
"/orgs/:orgId/role_assignments",
withAuthn,
async (req, res) => {
await req.prisma.orgRole.delete({
where: {
orgId_userId: {
orgId: parseInt(req.params.orgId),
userId: parseInt(req.body.user_id),
},
},
});
res.status(204).send();
}
);
accountsRouter.get(
"/orgs/:orgId/repos/:repoId/role_assignments",
withAuthn,
async (req, res) => {
const assignments = await req.prisma.repositoryRole.findMany({
where: {
repoId: parseInt(req.params.repoId),
},
include: {
user: true,
},
orderBy: {
id: "asc",
},
});
res.send(assignments);
}
);
accountsRouter.patch(
"/orgs/:orgId/repos/:repoId/role_assignments",
withAuthn,
async (req, res) => {
const role = await req.prisma.repositoryRole.update({
where: {
repoId_userId: {
repoId: parseInt(req.params.repoId),
userId: parseInt(req.body.user_id),
},
},
data: {
role: req.body.role,
},
include: {
user: true,
},
});
res.send(role);
}
);
accountsRouter.delete(
"/orgs/:orgId/repos/:repoId/role_assignments",
withAuthn,
async (req, res) => {
await req.prisma.repositoryRole.delete({
where: {
repoId_userId: {
repoId: parseInt(req.params.repoId),
userId: parseInt(req.body.user_id),
},
},
});
res.status(204).send();
}
);
// Role choices
accountsRouter.get("/org_role_choices", withAuthn, async (_req, res) => {
res.send(["member", "admin"]);
});
accountsRouter.get("/repo_role_choices", withAuthn, async (_req, res) => {
res.send(["reader", "editor", "maintainer", "admin"]);
});

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

  1. Viewing a single repository
  2. Listing all the repositories that the user can see

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.