Skip to content

Airlock

julianrkung edited this page May 5, 2021 · 3 revisions

Overview

Airlock is the platform solution developed by the Blueprint Integration team that acts as an intermediate server between Airtable and a web-client. Airlock was developed to handle issues of authentication and access control that Airtable does not come out-of-the-box with. The Airlock server mounts on top of our backend express server (code here), and we use the Airlock client (code here) to interact with it.

For in-depth documentation on Airlock, follow the link under Resources at the bottom of the page

Use Case

For authentication, we make use of Airlock's base.register, base.login and base.logout calls in airlock.ts to handle login, sign up, and log out. Under the hood, Airlock handles authentication, hashing of passwords, and issuing of access tokens to browsers automatically for us, which is why code for these 3 things do not exist explicitly in the codebase.

Access control is used so that a user is only able to see as much data as he or she have the permission to do. This is enforced as a set of rules that we define in a file in Airlock (see the Access Resolvers section below for more information).

Understanding the code

airlock is instantiated in airtable.js. At first glance, this might not look different from a vanilla airtable, and that is by design. But look closely at Airtable.configure(...), and you will see that the endpoint specified is the node server (hosting Airlock), and the apiKey being used is the raw string 'airlock'. This tells us that the Airtable API calls in this web client are making calls to Airlock under the hood. The web-client does not use (and should NOT contain) an Airtable API Key.

Airlock is instantiated server-side in meepanyar-node

new Airlock({
  // port: airlockPort,
  server: app, /* the rest of our node server */
  airtableApiKey: [apiKey], /* Airtable API Key */ 
  airtableBaseId: process.env.AIRTABLE_BASE_ID,
  airtableUserTableName: 'Users', /* The table containing the users of the app */ 
  airtableUsernameColumn: 'Username', /* The column name containing the username of the app */ 
  airtablePasswordColumn: 'Password', /* The column name containing the password of the app */ 
  allowedOrigins: [
    PRODUCTION_WEB_URL,
    ...DEVELOPMENT_WEB_URLS
  ]  /* a list of origins allowed to interact with Airlock. This  must be explicitly specified due to CORS */
});

Public / Private Key Pair

In order to run Airlock, we need a public/private key pair for the server. These should only be generated once and should never be committed to git. If they are ever regenerated, all existing sessions for a user will be invalidated and all users will need to log in again. You will also need these keys locally in order to test airlock, these should be shared with developers through secure means.

Generating Keys

In production:

  • The contents of jwt.key should entirely be pasted into a PRIVATE_KEY env variable on the server. Similarly, jwt.key.pub should be pasted into the PUBLIC_KEY env variable.

In development / locally:

  • Locally, these files should be kept in the otherwise empty config/ folder. Even if this folder is empty, it must exist for Airlock to function.
ssh-keygen -t rsa -b 4096 -m PEM -f jwt.key
openssl rsa -in jwt.key -pubout -outform PEM -out jwt.key.pub

Dealing with CORs

Because our frontend and backend are on different origins, our backend server (and the client) must allow Cross-Origin requests.

In production:

  • The environment variable PRODUCTION_WEB_URL should point to the production frontend website. As of 5/4/2021, that production frontend website is https://meepanyar.netlify.app/

In development / locally:

  • The variable DEVELOPMENT_WEB_URLS define the allowed localhost origins. By default, only 'http://localhost:3000' and 'http://localhost:5000' development URLs are allowed. The former is the default URL primarily used when developing the frontend and is the URL that npm run start runs on. The latter is the URL used when one builds and runs a production environment locally by running npm run build and serve -s build (you might need to install serve via npm install -g serve). The production environment is usually used when developing and testing offline functionality.

Access Resolvers

In the meepanyar-node repo, the folder resolvers/ contains a file index.js. This is where the rules for access controls are written.

index.js exports a map of key value pairs where the key is the table name and the value is either an object or a function that returns a boolean value.

The value is an object when we want to specify different read and write permissions, in which case we would define another key-value pair with the key being read, and write and the value being the function that returns a boolean value.

This 'function that returns a boolean value' that keeps being mentioned is essentially the rule that is used to evaluate whether a particular row should be return. A row (record) and the authenticated user is passed into this function, and Airlock that evaluates the record based on the authenticated user to return either true (the current authenticated user HAS permission to access this role) or not (he or she does not). Function calls to Airtable through Airlock therefore return a filtered subset of rows, only the filtered subset that a particular user has permission to see.

Interestingly, access resolver functions (the 'function that returns a boolean value') can also return objects.

  • In the event of a read:
    • the object returned by the access resolver function is returned in place of the record that it was called on
  • In the event of a write:
    • the object returned by the access resolve function is attempted to be persisted to Airtable

I highly recommend watching the "Video demo of Airlock" listed in the Resources section as the demo makes it clear. The demo of access resolvers and all of its features occur towards the latter half of the video.

Walking through 2 Access Resolver Use Cases

  • We use access resolvers liberally in the project. There are 2 main uses I want to highlight.
  • Tables.Sites
    • The frontend refreshes data using a single getAllSites call in src/lib/redux/refreshData.ts. Each Site record is passed through the read access resolver function for the Sites table where:
      • If the function returns false (in the case that the User isn't linked to the Site record), the Site record is not sent to the User
      • If the function passes the false case, then it makes calls to grab all the Customers, Meter Readings, Payments, Inventory of the site and return them inside of the object containing the Site record. This way, all of a Site's information is coupled with the actual Site record, and is later parsed by the frontend in the later half of refreshData.ts
  • Tables.PurchaseRequests
    • When a Purchase Request is created or updated, it will pass through the write resolver function for Tables.PurchaseRequests.
      • The write resolver logic returns false if the User attempting to review the Purchase Request is not an admin
      • The write resolver logic also uploads the receipt's dataURI to Azure blob storage in order to get a public photo URL for Airtable. This is necessary because Airtable only accepts images in the form of public URLs.

Resources