paint-brush
Role Based Access Control for the Ethereum Blockchainby@alcueca
3,361 reads
3,361 reads

Role Based Access Control for the Ethereum Blockchain

by Alberto Cuesta Cañada May 6th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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.

Company Mentioned

Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Role Based Access Control for the Ethereum Blockchain
Alberto Cuesta Cañada  HackerNoon profile picture

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, who 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:

  • The design requirements for our access control system;
  • the implementation as a smart contract;
  • the test cases we considered;
  • the gas usage of state changing methods
  • 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.
  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. The bearers of each role are stored in an array inside the Role struct, instead of a mapping as in many contracts that store users. Since I expect to have many roles with few users this should be efficient enought and this way I can retrieve a list of all bearers for security audits.
  4. 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.

The use of arrays instead of mappings makes the code more verbose, unfortunately. More compact or gas efficient implementations surely exist, but this one passes all my tests and I prefer to code for clarity until optimizations are strongly demanded.

Implementation

/**
* @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);

    /**
     * @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;
        address[] bearers;
    }

    /**
     * @notice All roles ever created.
     */
    Role[] public roles;

    /**
     * @notice The contract constructor, empty as of now.
     */
    constructor() public {
    }

    /**
     * @notice Create a new role.
     * @dev If the _admin parameter is the id of the newly created
     * role msg.sender is added to it automatically.
     * @param _roleDescription The description of the role being 
     * 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,
                bearers: new address[](0)
            })
        ) - 1;
        emit RoleCreated(role);
        if (_admin == role) {
            roles[role].bearers.push(msg.sender);
            emit BearerAdded(msg.sender, role);
        }
        return role;
    }

    /**
     * @notice Verify whether an address 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)
    {
        if (_role >= roles.length ) return false;
            address[] memory _bearers = roles[_role].bearers;
            for (uint256 i = 0; i < _bearers.length; i++){
                if (_bearers[i] == _account) return true;
            }
        return false;
    }

    /**
     * @notice Add a bearer to a role
     * @param _account The address 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 not authorized to add bearers."
        );
        if (hasRole(_account, _role) == false){
            roles[_role].bearers.push(_account);
            emit BearerAdded(_account, _role);
        }
    }

    /**
     * @notice Remove a bearer from a role
     * @param _account The address 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 not authorized to remove bearers."
        );
        address[] memory _bearers = roles[_role].bearers;
        for (uint256 i = 0; i < _bearers.length; i++){
            if (_bearers[i] == _account){
                _bearers[i] = _bearers[_bearers.length - 1];
                roles[_role].bearers.pop();
                emit BearerRemoved(_account, _role);
            }
        }
    }
}

Testing

I always 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. In response to previous feedback, I’m now including as well a gas usage report using eth-gas-reporter.

Contract: RBAC
RBAC
✓ addRootRole 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 exits if the bearer belongs to the role.
✓ addBearer adds a bearer to a role.
✓ removeBearer throws on non existing roles.
✓ removeBearer throws on non authorized users.
✓ removeBearer exits if the bearer doesn't belong to the role.
✓ removeBearer removes a bearer from a role.

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.

<a href="https://medium.com/media/3c851dac986ab6dbb2d1aaa91205a8eb/href">https://medium.com/media/3c851dac986ab6dbb2d1aaa91205a8eb/href</a>