Role Based Access Control for the Ethereum Blockchain

Written by alcueca | Published 2019/05/06
Tech Story Tags: solidity | blockchain | ethereum | how-to | blockchain-development | blockchain-architecture | access-control | latest-tech-stories

TLDR Role Based Access Control is a security need for software systems that are designed to be accessed by hundreds of users. This need is commonly implemented in enterprise software and operating systems, but not much has been done for the Ethereum blockchain. This article aims to show how you can build a Role Based access control system for the. Ethereum. It is not necessarily complex to implement, but as this article shows there are a number of trade-offs and tradeoffs that need to be taken into account. We also have made our code public, please feel free to reuse it in any way.via the TL;DR App

You decide who does what with your smart contracts
Historically, privacy was almost implicit, because it was hard to find and gather information. But in the digital world we need to have more explicit rules. — Bill Gates

Introduction

Role Based Access Control is a security need for software systems that are designed to be accessed by hundreds of users. While this need is commonly implemented in enterprise software and operating systems, not much has been done for the Ethereum Blockchain.
This article aims to show how we implemented Role Based Access Control in Solidity for the Ethereum Blockchain, and to teach you to build your own. We also have made our code public, please feel free to reuse it in any way or form.
When designing a Supply Chain as a Directed Acyclic Graph we realised that we needed to determine dynamically who could add information to each node in the graph. From a real world perspective if you own a manufacturing facility you probably want all operators in the assembly line to record with their own account that they have assembled a part.
OpenZeppelin, which I use as the gold standard in solidity development, have a Roles.sol contract that they use to implement roles such as Minter and Burner in their ERC721.sol contracts. Unfortunately these implementations don’t allow to create new roles at runtime, which you need if you want to control access to each separate token using separate roles.
This article aims to show how you can build a Role Based Access Control System for the Ethereum Blockchain.
I ended just coding a RBAC contract from scratch according to our requirements, before finding an discontinued version of the same idea from OpenZeppelin which had pretty much the very same methods. In the interest of reusability I refactored my code as much as I could to follow their nomenclature.
In the following sections I will describe:
  1. The design requirements for our access control system;
  2. the implementation as a smart contract;
  3. the test cases we considered;
  4. the gas usage of state changing methods
  5. and some finishing thoughts.
Let’s dig in.

Conceptual Design

My idea for an RBAC system was simple.
  1. Roles would be identified by a numeric identifier like groups in unix.
  2. Roles could be created dynamically with just a description.
  3. Each role would store the addresses of users.
  4. Each role would have a second role associated, which would be the only one allowed to add or remove users.
If you are using the Roles.sol and RBAC.sol contracts from OpenZeppelin you need to be aware that Roles.sol only implements operations that take effect inside a role, while operations that happen outside the role are implemented in RBAC.sol or the access/roles/*Role.sol contracts, including the data structure to store roles as you create them.
In my implementation I took a number of decisions tailored to our use case:
  1. I include a description string inside the Role struct, with the structs themselves stored in an array. The position of each Role struct in the array is used as an identifier. There is a temptation to use a mapping to store the Roles, but I find it unnecessary here.
  2. Each role receives on instantiation the identifier of another role which we designate as its admin role and which cannot be modified after instantiation. This admin role is the only one that can add and remove bearers to this role.
  3. For security and consistency reasons you can remove a Bearer from a Role, but there is no method to remove a Role completely from the system.
In my implementation I took a number of implementation decisions tailored to our use case.
* An earlier version of this article used arrays to store the bearers in each role. That was changed into a mapping as a suggestion from Nico Vergauwen. The code is more efficient and clearer this way, even if auditing is more cumbersome.

Implementation

pragma solidity ^0.5.0;
/**
* @title RBAC
* @author Alberto Cuesta Canada
* @notice Implements runtime configurable Role Based Access Control.
*/
contract RBAC {
  event RoleCreated(uint256 role);
  event BearerAdded(address account, uint256 role);
  event BearerRemoved(address account, uint256 role);
  uint256 constant NO_ROLE = 0;
  /**
   * @notice A role, which will be used to group users.
   * @dev The role id is its position in the roles array.
   * @param description A description for the role.
   * @param admin The only role that can add or remove bearers from
   * this role. To have the role bearers to be also the role admins 
   * you should pass roles.length as the admin role.
   * @param bearers Addresses belonging to this role.
   */
  struct Role {
    string description;
    uint256 admin;
    mapping (address => bool) bearers;
  }
  /**
   * @notice All roles ever created.
   */
  Role[] public roles;
  /**
   * @notice The contract constructor, empty as of now.
   */
  constructor() public {
    addRootRole("NO_ROLE");
  }
  /**
   * @notice Create a new role that has itself as an admin. 
   * msg.sender is added as a bearer.
   * @param _roleDescription The description of the role created.
   * @return The role id.
   */
  function addRootRole(string memory _roleDescription)
    public
    returns(uint256)
  {
    uint256 role = addRole(_roleDescription, roles.length);
    roles[role].bearers[msg.sender] = true;
    emit BearerAdded(msg.sender, role);
  }
  /**
   * @notice Create a new role.
   * @param _roleDescription The description of the role created.
   * @param _admin The role that is allowed to add and remove
   * bearers from the role being created.
   * @return The role id.
   */
  function addRole(string memory _roleDescription, uint256 _admin)
    public
    returns(uint256)
  {
    require(_admin <= roles.length, "Admin role doesn't exist.");
    uint256 role = roles.push(
      Role({
        description: _roleDescription,
        admin: _admin
      })
    ) - 1;
    emit RoleCreated(role);
    return role;
  }
  /**
   * @notice Retrieve the number of roles in the contract.
   * @dev The zero position in the roles array is reserved for
   * NO_ROLE and doesn't count towards this total.
   */
  function totalRoles()
    public
    view
    returns(uint256)
  {
    return roles.length - 1;
  }
  /**
   * @notice Verify whether an account is a bearer of a role
   * @param _account The account to verify.
   * @param _role The role to look into.
   * @return Whether the account is a bearer of the role.
   */
  function hasRole(address _account, uint256 _role)
    public
    view
    returns(bool)
  {
    return _role < roles.length && roles[_role].bearers[_account];
  }
  /**
   * @notice A method to add a bearer to a role
   * @param _account The account to add as a bearer.
   * @param _role The role to add the bearer to.
   */
  function addBearer(address _account, uint256 _role)
    public
  {
    require(
      _role < roles.length,
      "Role doesn't exist."
    );
    require(
      hasRole(msg.sender, roles[_role].admin),
      "User can't add bearers."
    );
    require(
      !hasRole(_account, _role),
      "Account is bearer of role."
    );
    roles[_role].bearers[_account] = true;
    emit BearerAdded(_account, _role);
  }
  /**
   * @notice A method to remove a bearer from a role
   * @param _account The account to remove as a bearer.
   * @param _role The role to remove the bearer from.
   */
  function removeBearer(address _account, uint256 _role)
    public
  {
    require(
      _role < roles.length,
      "Role doesn't exist."
    );
    require(
      hasRole(msg.sender, roles[_role].admin),
      "User can't remove bearers."
    );
    require(
      hasRole(_account, _role),
      "Account is not bearer of role."
    );
    delete roles[_role].bearers[_account];
    emit BearerRemoved(_account, _role);
  }
}

Testing

I like to describe the tests passed when making a contract public, both to show the edge cases and to provide some confidence as to the reliability of the code.
Contract: RBAC
RBACaddRootRole creates a role.
✓ hasRole returns false for non existing roles.
✓ hasRole returns false for non existing bearerships.
addRootRole adds msg.sender as bearer.
addRole doesn’t add msg.sender with admin role.
✓ addBearer reverts on non existing roles.
✓ addBearer reverts on non authorized users.
✓ addBearer reverts if the bearer belongs to the role.
✓ addBearer adds a bearer to a role.
✓ removeBearer reverts on non existing roles.
✓ removeBearer reverts on non authorized users.
✓ removeBearer reverts if the bearer doesn't belong to the role.
✓ removeBearer removes a bearer from a role.
In response to previous feedback, I’m now including as well a gas usage report using eth-gas-reporter.

Conclusion

This article describes an implementation for a Smart Contract Role Based Access Control System, which has the following properties:
  1. Allows new roles to be created at runtime.
  2. Includes a concept of role administrator, which is allowed to add and remove members to the role.
  3. Allows to easily determine all existing roles and their bearers.
Role Based Access Control is not necessarily complex to implement, but as this article shows there are a number of trade-offs and design decisions that need to be taken into account, which will be closely related to your users and the actions that they are allowed to perform. While I would be very pleased if you decide to reuse this implementation of an RBAC system, I encourage you to look for and consider other options.
For a practical application of this RBAC contract, stay tuned for the next article in our Supply Chain series.

Written by alcueca | Hello. I design and build blockchain solutions. I like to make the complex simple.
Published by HackerNoon on 2019/05/06