We recently shipped a Node.js sample apps repo with 2 apps demonstrating how to use Oso with Node.js. The first is a simple "Hello, World!" app that's a minimum viable example of making an Oso request. The second is a more substantial API called Oso Drive that replicates parts of Google Drive's authorization model with Oso.
Oso Drive is a simplified file sharing API. The fundamental data structures are files and folders: files belong to folders, and folders can belong to other folders. But there are also users and organizations, and whether a user can read or write a particular file depends on which folder the file is in, what organization the user is part of, and what roles the user has in their organization. In this blog post, you'll learn how to implement these authorization patterns in Oso using Oso Drive as an example.
Getting Started With Oso Drive
Oso Drive is a standalone Express app that exposes a simplified file management API that stores files and folders in memory.
The /api
folder contains 1 file for each of the API endpoints:
POST /createFile
: creates a new filePOST /createFolder
: creates a new folderGET /readFile
: reads an existing fileGET /readFolder
: reads an existing folderPUT /updateFile
: updates an existing file
To get started running Oso Drive, clone the sample apps repo from GitHub, and run npm install
from the oso-drive
directory. If you haven't already, create a free Oso Cloud account and set up an API key. Then copy the .env.example
file to .env
and update the OSO_CLOUD_API_KEY
entry to your Oso cloud API key. Your .env
file should look like the following:
OSO_CLOUD_API_KEY=e_49gxomitted_from_placeholder
Once your .env
file is set up, run npm run seed
. The seed
script will add the Oso Drive policy to your Oso Cloud environment, and create some sample files and folders that you can use for testing. Note that the seed
script will overwrite any existing policy you have in Oso Cloud, so don't run this script on an Oso Cloud environment that contains production data.
Once npm run seed
is done, run npm start
to start the API server. You should then be able to interact with the Oso Drive API using curl, Postman, or any other HTTP client. This blog post uses curl for ease of copy/paste. For example, the following curl command shows the output of trying to read file test.txt
as the user Peter
.
$ curl -H "authorization: Peter" 'http://localhost:3000/readFile?id=test.txt'
{
"file": {
"id": "test.txt",
"content": "hello world"
},
"users": {}
}
For simplicity, the Oso Drive app reads the user name directly the authorization
header.In a production application, you would use a JWT or some other access token pattern.
Node.js Authorization Based on Organization Roles
Oso Drive supports permissions based on the roles a user has on an organization. This is often the first kind of Role-Based Access Control (RBAC) that people encounter. Organizations have two roles: member
and admin
.
resource Organization {
roles = ["admin", "member"];
"member" if "admin";
}
Folders have an organization
relationship, which means that each folder belongs to at most one organization. You can define a policy that grants the writer
role to an organization's admins on any folder in the organization as follows.
resource Folder {
permissions = ["read", "write"];
roles = ["reader", "writer"];
relations = {
folder: Folder,
organization: Organization, # A folder belongs to at most 1 organization
owner: User
};
# Grant "writer" role if the user is an "admin" on this folder's organization
"writer" if "admin" on "organization";
}
For example, the user Bill
has the admin
role on the Initech
organization.That means Bill
can update the file tps-reports/tps-report.txt
:
$ curl -H 'content-type:application/json' -X PUT -d '{"id":"tps-reports/tps-report.txt", "content":"Needs work"}' -H "authorization: Bill" http://localhost:3000/updateFile
{
"file": {
"id": "tps-reports/tps-report.txt",
"folder": "tps-reports",
"content": "Needs work"
}
}
On the other hand, the user Samir
is a member of the Initech
organization, but not an admin. That means Samir
cannot update the file tps-reports/tps-report.txt.
$ curl -H 'content-type:application/json' -X PUT -d '{"id":"tps-reports/tps-report.txt", "content":"Hi Peter"}' -H "authorization: Samir" http://localhost:3000/updateFile
{"message":"Not authorized"}
Node.js Authorization Based on Files and Folders
Files are private by default in Oso Drive. For example, the user Tom
can't read the file tps-reports/tps-report.txt
.
$ curl -H "authorization: Tom" 'http://localhost:3000/readFile?id=tps-reports/tps-report.txt'
{
"message": "Not authorized"
}
The Oso Drive API determines whether a user can read a particular file based on whether the user has the read
permission on that file. The following is the relevant code from /api/readFile.js that uses Oso Cloud to determine whether a user can read a particular file.
const authorized = await oso.authorize(
{ type: 'User', id: userId },
'read',
{ type: 'File', id }
);
if (!authorized) {
return res.status(401).json({ message: 'Not authorized' });
}
Similarly, a user can only update a file if they have the write
permission on that file.
Here is the relevant code in /api/updateFile.js.
const authorized = await oso.authorize(
{ type: 'User', id: userId },
'write',
{ type: 'File', id }
);
if (!authorized) {
return res.status(401).json({ message: 'Not authorized' });
}
So how can you give a user permission to read or write a file? The simplest way is to give the user a role on that file. This is an example of a more granular form of RBAC: resource-specific roles. Oso Drive's authorization policy allows you to give a user a reader
role on a particular file that lets them read that file, and a corresponding writer
role.
resource File {
permissions = ["read", "write"];
roles = ["reader", "writer"];
"read" if "write";
"read" if "reader"; # Give a user the "reader" role to let them read a file
"write" if "writer";
}
The seed script gives the user Michael
the writer
role on the tps-reports/tps-report.txt
file. So the user Michael
can read tps-reports/tps-reports.txt
:
$ curl -H "authorization: Michael" 'http://localhost:3000/readFile?id=tps-reports/tps-report.txt'
{
"file": {
"id": "tps-reports/tps-report.txt",
"folder": "tps-reports",
"content": "TODO: write TPS report"
},
"users": {
"Michael": [
"writer"
]
}
}
Files also inherit roles from the folder they belong to. Files have a folder
relation, and role if role on folder;
means that a user with the reader
role on a folder also gets the reader
role for every file in that folder.
resource Folder {
permissions = ["read", "write"];
roles = ["reader", "writer"];
relations = {
folder: Folder
};
role if role on "folder";
"read" if "write";
"read" if "reader";
"write" if "writer";
}
resource File {
permissions = ["read", "write"];
roles = ["reader", "writer"];
relations = {
folder: Folder # A file optionally belongs to a folder
};
role if role on "folder"; # Inherit roles from the file's folder
"read" if "write";
"read" if "reader";
"write" if "writer";
}
The seed script gives the user Bob
the reader
role on the folder tps-reports
, and also indicates that the file "tps-reports/tps-report.txt" is in the folder "tps-reports". So that means the user Bob
can read the file tps-reports/tps-report.txt
, even though that user doesn't explicitly have the reader
role on that particular file.
$ curl -H "authorization: Bob" 'http://localhost:3000/readFile?id=tps-reports/tps-report.txt'
{
"file": {
"id": "tps-reports/tps-report.txt",
"folder": "tps-reports",
"content": "TODO: write TPS report"
},
"users": {
"Michael": [
"writer"
]
}
}
The /api/createFile
endpoint sets the file's folder using the following code.
await oso.tell(
'has_relation', // Fact type
{ type: 'File', id: file.id }, // Resource
'folder', // Relation
{ type: 'Folder', id: file.folder } // Actor
);
Node.js Authorization Based on Ownership
Relationship-based access control (ReBAC) is an authorization pattern where permissions are derived from relationships between resources, rather than just roles on a resource. Ownership is a common example of ReBAC: the user that created a file or folder should be able to write to that file or folder. Below is how you can represent that files have an owner, and the owner can write to that file.
resource File {
permissions = ["read", "write"];
roles = ["reader", "writer"];
relations = {
folder: Folder,
owner: User
};
"writer" if "owner"; # File owner can always write to the file
"read" if "write";
"read" if "reader";
"write" if "writer";
}
Oso Drive sets the file's owner when the file is created in /api/createFile. Below is the code that sets the file's owner.
await oso.tell(
'has_relation', // Fact type
{ type: 'File', id: file.id }, // Resource
'owner', // Relation
{ type: 'User', id: req.headers.authorization } // Actor
);
Node.js Authorization Based on Attributes
Users can also have permissions on a file based on the file's attributes, or even the folder's attributes. Attributes are any arbitrary properties associated with an actor or resource, and authorization that incorporates attributes is known, appropriately, as Attribute-Based Access Control (ABAC).
For example, in Oso Drive, files have a simple is_public
attribute: if a file has is_public
set, anyone can read that file. The test.txt
file is a public file, which is why Peter
was able to read it way back at the beginning of the post.
has_permission(_user: User, "read", file: File) if
is_public(file);
The is_readable_by_org
attribute grants the "reader"
role on a folder to any member of the folder's organization. The organization matches Organization
part ensures that the folder has an associated organization.
has_role(user: User, "reader", folder: Folder) if
organization matches Organization and
is_readable_by_org(folder) and
has_role(user, "member", organization);
Oso Drive also has an is_readable_by_org
attribute for files. Implementing this attribute for folders is trickier, because files don't have an organization relation, so you need to check both the folder
and the organization
.
has_role(user: User, "reader", file: File) if
organization matches Organization and
folder matches Folder and
is_readable_by_org(folder) and
has_relation(file, "folder", folder) and # Does file belong to right folder?
has_role(user, "member", organization); # Is this user a member of this org?
Try Authorization in Node.js for Yourself
Oso Drive demonstrates how to implement a complex authorization model in Oso Cloud with Node.js, including role-based, relationship-based, and attribute-based access control. Oso Drive's authorization model includes recursive relationships, role propagation, and other features that would make this model difficult to implement in Node.js code. Oso Cloud makes implementing RBAC, ReBAC, ABAC, and recursive structures much easier. Clone Oso's Node.js sample apps repo and try the Oso Drive sample app for yourself!