We recently announced a new open source project, Osgood, which aims to be a secure platform for running JavaScript on the server. This platform applies the Principle of Least Privilege to application code. One of the ways we enforce this is by limiting the types of operations an application can perform. For example, arbitrary network connections cannot be made and child processes cannot be executed.
Outbound HTTP requests are a first class citizen thanks to the fetch()
API. This means that CouchDB, a NoSQL database with an HTTP API, is a perfect match for performing application persistence with Osgood.
One of the biggest strengths of Osgood is the ability to specify policies on a per-route basis. This allows for very-fine security enforcement, allowing each Osgood Worker to only perform pre-approved operations.
Sample CRUD Application
Consider a simple CRUD application. This app represents a microservice within a larger organization. The service is essentially a facade in front of other services. It performs validation of the provided data, like enforcing username length. It limits database interactions, such as preventing arbitrary destructive queries from running. This app also decouples application code from the database implementation by transforming data into an ideal format. It also handles the database authentication, keeping the credentials on a trusted internal service and out of the client.
This microservice will have five endpoints:
- List Users (
GET /users
) - Create User (
POST /users
) - Get User (
GET /users/{user_id}
) - Delete User (
DELETE /users/{user_id}
) - Update User (
PUT /users/{user_id}
)
Note: If you'd like to follow along with this example, the code for the project is open source and available in our repository: IntrinsicLabs/osgood/examples/couchdb-rest
Application Configuration: app.js
Osgood Applications are configured using JavaScript. There is a global object called app
available for setting properties. The first one is interface
and is the name of the interface we want our application to bind to. The second one is port
and is the port we want to listen on.
There are also some methods available on the app
object for performing routing of incoming HTTP requests based on HTTP method and path patterns. For example, to route an incoming GET
request to the /users
endpoint, one can call app.get('/users', ...)
. The second argument to the routing functions is a path to the Osgood Worker file. The third argument is a function for configuring the route's policy.
Within the policy configuration functions we specify which URLs can be requested. These can be configured by calling methods like this: policy.outboundHttp.allowMETHOD(urlPattern)
. The urlPattern
uses the glob
syntax.
Note: This API is described in much more detail in our API Documentation.
This is what an Osgood Application file might look like for our CouchDB application:
app.interface = '0.0.0.0';
app.port = 8000;
app.get('/users', 'list.js', policy => {
policy.outboundHttp.allowGet('http://localhost:5984/users/_all_docs');
});
app.get('/users/:user_id', 'view.js', policy => {
policy.outboundHttp.allowGet('http://localhost:5984/users/*');
});
app.delete('/users/:user_id', 'delete.js', policy => {
policy.outboundHttp.allowGet('http://localhost:5984/users/*');
policy.outboundHttp.allowDelete('http://localhost:5984/users/*');
});
app.post('/users', 'create.js', policy => {
policy.outboundHttp.allowPost('http://localhost:5984/users');
});
app.put('/users/:user_id', 'update.js', policy => {
policy.outboundHttp.allowPut('http://localhost:5984/users/*');
});
We've now described all of the capabilities and have fully configured our application within a single file. With this configuration our application would not be able to, say, send an HTTP request to http://evil.co
, nor would the GET /users
route be able to perform a DELETE
operation against the users
collection in CouchDB.
Describing the capabilities up front is beneficial for two reasons. The straightforward reason is that it is secure. A side-effect is that application code is now much easier to audit. Imagine how quick those tedious GDPR audits could be if you had this list of I/O available for all your other apps.
Create User Worker: create.js
Our application has five operations that it can perform. In this post we'll only look at one of them: the creation of users (if you'd like to see the other examples checkout the sample application on GitHub).
This route will accept an incoming POST request, convert the body into JSON, perform some minimal validation, then pass the data to CouchDB (along with auth credentials). It'll then relay information to the client based on whether or not the operation succeeds.
const AUTH = `Basic ${btoa('osgood_admin:hunter12')}`;
export default async (request) => {
try {
var user = await request.json();
} catch (e) {
return json({"error": "CANNOT_PARSE_REQUEST"}, 400);
}
if (user.id || user._id) {
return json({"error": "CANNOT_OVERRIDE_ID"}, 400);
}
if (!user.username || typeof user.username !== 'string'
|| user.username.length < 3 || user.username.length > 20) {
return json({"error": "USERNAME_INVALID"}, 400);
}
const payload = await fetch(`http://localhost:5984/users`, {
method: 'POST',
headers: {
Authorization: AUTH,
'Content-Type': 'application/json',
},
body: JSON.stringify(user)
});
const obj = await payload.json();
if (obj.error) {
return json({"error": "UNABLE_TO_INSERT"}, 500);
}
return json({ok: true});
}
function json(obj, status = 200) {
const headers = new Headers({
'Content-Type': 'application/json'
});
const body = JSON.stringify(obj);
const response = new Response(body, { headers, status });
return response;
}
If you've ever worked with Service Workers, Lambda Functions, or Express.js controllers then this code might look familiar. The file exports a single default function which accepts request
and context
arguments. The request
argument is an instance of the Request object available in modern browsers. The context
argument has some additional niceties that we don't need for this particular example. The function itself can be an async
function or otherwise return a promise. If the promise rejects Osgood will respond to the client with a 500
error. If it resolves a string
or a simple object then Osgood will reply with a 200
and an appropriate Content Type. But, for fine grained control, a Response object can be returned which allows for manually setting the HTTP status code and other headers.
Running Osgood
To run Osgood, first download a release for your platform. Once that's done extract the osgood
binary somewhere, ideally in your $PATH
.
Then, download the six files for this project (app.js
, list.js
, create.js
, delete.js
, update.js
, view.js
). Finally, run this command:
$ osgood app.js
This will start the Osgood Application and route requests to the five Osgood Workers. Of course, the service won't be too useful without a CouchDB instance to talk to. The following commands will run CouchDB in a Docker container:
$ docker run \
-e COUCHDB_USER=osgood_admin \
-e COUCHDB_PASSWORD=hunter12 \
-p 5984:5984 \
--name osgood-couch \
-d couchdb
$ curl \
-X PUT \
http://localhost:5984/users
After that we're ready to interact with the application. The next command will send a POST request to the Osgood Application and create our first user:
$ curl \
-X POST \
http://localhost:8000/users \
-d '{"username": "osgood"}' \
-H "Content-Type: application/json"
More Information
Osgood is open source. It's written in Rust and runs JavaScript using the speedy V8 engine.
The sourcecode is hosted on GitHub and is available at IntrinsicLabs/osgood. Pull Requests welcome!
Top comments (0)