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.
One permission this application needs to grant users is the ability to read a repository. That permission affects two operations:
- Viewing a single repository
- 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 itaccountsRouter.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 orgaccountsRouter.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.
Now you can just call this function when you need to authorize a user's request to read a repository.
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.