-
Notifications
You must be signed in to change notification settings - Fork 0
Airlock
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
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).
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 */
});
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.
In production:
- The contents of
jwt.key
should entirely be pasted into aPRIVATE_KEY
env variable on the server. Similarly,jwt.key.pub
should be pasted into thePUBLIC_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
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 ishttps://meepanyar.netlify.app/
In development / locally:
- The variable
DEVELOPMENT_WEB_URLS
define the allowedlocalhost
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 thatnpm run start
runs on. The latter is the URL used when one builds and runs a production environment locally by runningnpm run build
andserve -s build
(you might need to installserve
vianpm install -g serve
). The production environment is usually used when developing and testing offline functionality.
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.
- 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 theread
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 theCustomers
,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 ofrefreshData.ts
- If the function returns
- The frontend refreshes data using a single
-
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 returnsfalse
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.
- The
- When a Purchase Request is created or updated, it will pass through the
- In-depth documentation of Airlock: https://www.notion.so/calblueprint/Airlock-9b9f98ecbe4e40b386d969c7a0dde575
- Video demo of Airlock: https://share.descript.com/view/mu2V9m33Qa4
- Creating a New User and Assigning them a Site
- Adding or Updating or Deleting an Airtable Column or Table
- Testing Translations in a Production Environment