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
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:
Let’s dig in.
My idea for an RBAC system was simple.
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:
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.
/**
* @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);
}
}
}
}
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.
This article describes an implementation for a Smart Contract Role Based Access Control System, which has the following properties:
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>