DEV Community

Cover image for A dive into ABAC systems in Node.js applications with Fauna
IsaacThaJunior
IsaacThaJunior

Posted on

A dive into ABAC systems in Node.js applications with Fauna

Authorization is a significant component when building web applications, as developers are required both to know and recognize the identity of their users, grant them access and then restrict access to unregistered or unauthorized users. Put simply, It is a process of user recognition. In this tutorial, we are going to talk about ABAC, ABAC systems and ABAC in Node.js applications with Fauna.

What is ABAC authentication?

Attribute-Based Access Control or ABAC is a system that defines or controls access to what users can create or delete. It is an authorization model that grants access to users through attributes rather than roles.
According to the documentation,

Attribute-based access control (ABAC) is a flexible, fine-grained strategy for managing identity-based operations within Fauna.

Image

ABAC controls detailed level user access, which is established based on attributes different from its counterpart. The role-based access control (RBAC) controls broad access, which is established based on roles.

One of the benefits of ABAC is its user-intuitive nature that hides technical permission sets behind easy-to-understand user profiles that anyone with authority can update. It knows that the user will have the access they need as long as their attributes are up to date.

Another benefit is that ABAC enables dynamic and context-specific access to resources adapted to different access control policies.

Usefulness of ABAC in modern software development

ABAC provides fine-grained, elegant and contextual access control that is focused on delivering functional, transactional and data access controls. It's easier to maintain architecture allows for the swift and streamlined onboarding of new APIs.

One use case for ABAC is in the use of Completely Automated public Turing Test To Tell computers and Humans Apart or Captcha as is commonly known. Captchas are used to control access depending on whether the party seeking access is a machine or human.

Another use case is a newly re-assigned finance executive is only able to access information that is directly to their new project and not their previous one

ABAC in NodeJS systems

ABAC is an extension of RBAC that tries to solve problems in a specific situation. It uses attributes as building blocks in NodeJS systems to define access control rules and access requests. In systems where attributes separate access, ABAC provides a framework to solve role explosion issues by defining rights based on various properties of various users.

How ABAC systems work in NodeJS applications.

ABAC works by examining attributes to make decisions concerning access and this attributes can be broken up into four categories:

  • Subject attributes: This can be a human user or any device that issues access requests to perform operations on objects.

  • Object attributes: This is the resource or anything upon which the subject can operate.

  • Action: The execution of a function at the request of the subject on the object or what exactly will the subject do with the resource. It describes the action being attempted. This includes read, write, edit, delete, copy, execute, and modify.

  • Environmental attributes: The operational or situational context in which the access request occurs. The environmental attribute is independent of subject and object attributes.

Using Fauna for ABAC systems with NodeJS

Fauna provides some features such as privileges, membership, predicate functions, overlapping roles in a document and even through delegation. In this section we will look at these features and how they can be used to provide access to our Fauna documents. We will use Node.js to build an example application that leverages Fauna’s ABAC feature.

Privileges

A privilege is used to specify a resource in a Fauna database (e.g. a document, key, index, etc.) Privileges are specific actions that can be executed on particular resources and can be assessed through a set of predefined actions and a predicate function. With privileges, developers can add specific access to a group of predefined actions to grant access. Actions in Fauna can vary depending on the use case, this can be core schemas for a collection, index, function or even a key. We can add access to this in the create and delete schemas. The documents use case and access in this can be added in the read, history_read and history_write.

Membership

Membership defines a set of collections whose documents should have the role's privileges. A predicate function can be provided when determining the membership.

Predicate Functions

A predicate function is an FQL, read-only function that returns a boolean value to show or indicate whether access or action is allowed or not. The purpose of a predicate function is to read attributes of resources and create access policies on them when some conditions have been met. If the condition returns false, then the condition is not met.

Overlapping roles

This is a situation where a document has two or more roles for performing a specific action and Fauna tries to optimize for common access patterns so as to avoid evaluating roles unnecessarily.

Getting started with NodeJS project

This section will discuss how to use Fauna on a Nodejs project, but first, we will install Fauna on our local machine. To do this, we will install the Fauna shell using the command below:

npm install -g fauna-shell
Enter fullscreen mode Exit fullscreen mode

The command above will install the Fauna shell on your local machine. After we have installed the fauna shell, we then log in with our fauna credentials. If you don’t have an account with Fauna, you can create one here.

To create a database using the Fauna shell, we will use the command below, you can change the database name however you see fit.


fauna create-database abac-database
Enter fullscreen mode Exit fullscreen mode

The above command will create a database named abac-database, to start the database on your terminal, run the command below:


fauna shell abac-database
Enter fullscreen mode Exit fullscreen mode

Next we need to get our secret key from Fauna which we will use for our application by running the command below


CreateKey({ role: "server" })

//  Returns something like this
//  {
//   ref: Ref(Keys(), "278091949991264787"),
//   ts: 1601468000353000,
//    role: 'server',
//    secret: 'fnAD2_sntiACE_xHweiTXMNvy7Z4vJ2OkA7yZAd1',  copy this
//    hashed_secret: '$2a$05$AjuS2MrHwgBCUKepWp/KLOniI4hinzLbUqIHf1PZsOlu3qbSncgr.'
//  }
Enter fullscreen mode Exit fullscreen mode

After you’ve started the database, you’d need to create a collection for contacts, address and phone numbers below:


CreateCollection({ name: "contacts" }),
CreateCollection({ name: "address" }),
CreateCollection({ name: phone_number })
Enter fullscreen mode Exit fullscreen mode

Next, create indexes for our collection by copying the code block below:


CreateIndex({ 
  name: "user_by_name",
  source: Collection("contacts"),
  terms: [{ field: ["data", "name"] }],
})

CreateIndex({ 
  name: "user_by_number",
  source: Collection("phone_number"),
  terms: [{ field: ["data", "phone_number"] }],
})
Enter fullscreen mode Exit fullscreen mode

You can then add data to our collections below


Map([
  ["Isaac Okoro", 34900],
  ["Kay", 49061],
  ["Bright", 29083]
], Lambda("data", Let(
    {
      user: Create(Collection("contacts"), {
        data: { name: Select(0, Var("data")) },
        credentials: { address: "east west road" }
      }),
        salary: Select(1, Var("data"))
      },
      Create(Collection("phone_number"), { data: {
        user: Select("ref", Var("contacts")),
        phone_number: Var("phone_number")
      }})
 )))     
Enter fullscreen mode Exit fullscreen mode

In the code above, we created contacts and phone_number collections, and inside it we created a user that references a key, the contacts collection also stores the user’s credentials and address. In our next section, we will look at using our prepared database in a Node.js application.

Using ABAC system in Contacts App

First, we will build a Node.js contacts application. To do that, we will run the command below in our Terminal to scaffold a Node.js application :


npm init
Enter fullscreen mode Exit fullscreen mode

You will then asked some question, answer them and then a package.json file will be created for you.

Then we will install our Fauna dependency which we will use in our project


npm i faunadb
Enter fullscreen mode Exit fullscreen mode

In the code above, we installed fauna’s NPM package, we will then go into our Fauna account and then get our secret key after which we will navigate to our application and connect our Fauna access key to our project’s env file.
Next, install the following dependencies which we will use:


npm install commander inquirer
Enter fullscreen mode Exit fullscreen mode
- `Commander` is a simple library for commands.
- `inquirer` is for complex input commands
Enter fullscreen mode Exit fullscreen mode

Now we have installed our dependencies, we then navigate to our index.js file and import those dependencies


const faunadb = require('faunadb');
const program = require('commander');
const { prompt } = require('inquirer');
Enter fullscreen mode Exit fullscreen mode

Next we create a new js file called prompts.js ( you can name yours differently) and then we create our prompts command and then export it


const newContactPrompts = [
  {name: 'firstName', message: 'First Name'},
  {name: 'lastName', message: 'Last Name'},
  {name: 'phoneNumber', message: 'Phone Number'}
]
module.exports = {newContactPrompts}
Enter fullscreen mode Exit fullscreen mode

Next we create a file where we put the logic for the app


const fs = require('fs')
const path = require('path')
// this path needs to be relative to work with fs
const contactsLocation = path.join(__dirname, 'contacts.json')
/**
 * should read the contacts at the
 * @contactsLocation path and convert
 * it to a js object
 */
const getContacts = () => {
  const contacts = fs.readFileSync(contactsLocation)
    .toString()
  return JSON.parse(contacts)
}
/**
 * takes a contacts object, converts it to JSON
 * and saves it at the @contactsLocation path
 * @param {Object} contacts contacts object
 */
const saveContacts = (contacts) => {
  fs.writeFileSync(contactsLocation, JSON.stringify(contacts, null, 2))
}
module.exports = { 
  contactsLocation,
  getContacts,
  saveContacts
}
Enter fullscreen mode Exit fullscreen mode

After we have done that, we then import our newly created files to be used in our index.js


const faunadb = require('faunadb');
const program = require('commander')
const { prompt } = require('inquirer')
const {newContactPrompts} = require('./prompts')
const {getContacts, saveContacts} = require('./utils')
Enter fullscreen mode Exit fullscreen mode

In the code block below, we defined the contacts CLI commands


program
  .version('0.0.1')
  .description('Address book CLI program')
program
  .command('new')
  .alias('n')
  .description('add a new contact')
  .action(() => {
    prompt(newContactPrompts)
      .then(({firstName, lastName, phoneNumber}) => {
        const key = firstName + ' ' + lastName
        const contacts = getContacts()
        contacts[key] = {firstName, lastName, phoneNumber}
        saveContacts(contacts)
      })
  })
program
  .command('list')
  .alias('l')
  .description('list all contacts')
  .action(() => {
    const contacts = getContacts()
    prompt([
      {
        type: 'list',
        name: 'selected',
        message: 'Select a contact',
        choices: Object.keys(contacts)
      }
    ])
      .then(({selected}) => {
        const contact = contacts[selected]
        console.log(JSON.stringify(contact, null, 2))
      })
  })
program.parse(process.argv)
Enter fullscreen mode Exit fullscreen mode

When you’ve done this, run the command below in your terminal to add a new contact to your Fauna database


node index.js new 
Enter fullscreen mode Exit fullscreen mode

The image below should be the same as yours if done correctly:
Image

Conclusion

In this tutorial, we learned how to create a comprehensive authorization model and use it with Fauna's API and built-in ABAC features. We also built a contacts application and enabled ABAC by giving different permissions to users and admin to it.

Currently, access can be given to any Fauna database, but the Fauna Query Language implements it.

Resources

The Fauna Docs
GitHub Repo

Discussion (0)