From e931c1cbfcf92fa34b557c1d88c4f7218861e587 Mon Sep 17 00:00:00 2001 From: Matt Condon Date: Fri, 24 Nov 2017 12:56:03 +0200 Subject: [PATCH] feat: RBAC authentication contract and role library --- contracts/examples/RBACExample.sol | 61 +++++++++++++++++ contracts/ownership/rbac/RBAC.sol | 101 +++++++++++++++++++++++++++++ contracts/ownership/rbac/Roles.sol | 55 ++++++++++++++++ test/RBAC.js | 77 ++++++++++++++++++++++ test/helpers/RBACMock.sol | 68 +++++++++++++++++++ 5 files changed, 362 insertions(+) create mode 100644 contracts/examples/RBACExample.sol create mode 100644 contracts/ownership/rbac/RBAC.sol create mode 100644 contracts/ownership/rbac/Roles.sol create mode 100644 test/RBAC.js create mode 100644 test/helpers/RBACMock.sol diff --git a/contracts/examples/RBACExample.sol b/contracts/examples/RBACExample.sol new file mode 100644 index 000000000..ddc765873 --- /dev/null +++ b/contracts/examples/RBACExample.sol @@ -0,0 +1,61 @@ +pragma solidity ^0.4.8; + +import '../ownership/rbac/RBAC.sol'; + + +contract RBACExample is RBAC { + + modifier onlyOwnerOrAdvisor() + { + require( + hasRole(msg.sender, "owner") || + hasRole(msg.sender, "advisor") + ); + _; + } + + function RBACExample(address[] _advisors) + public + { + addRole(msg.sender, "owner"); + addRole(msg.sender, "advisor"); + + for (uint256 i = 0; i < _advisors.length; i++) { + addRole(_advisors[i], "advisor"); + } + } + + function onlyOwnersCanDoThis() + onlyRole("owner") + view + external + { + } + + function onlyAdvisorsCanDoThis() + onlyRole("advisor") + view + external + { + } + + function eitherOwnerOrAdvisorCanDoThis() + onlyOwnerOrAdvisor + view + external + { + } + + // owners can remove advisor's role + function removeAdvisor(address _addr) + onlyRole("owner") + public + { + // revert if the user isn't an advisor + // (perhaps you want to soft-fail here instead?) + checkRole(_addr, "advisor"); + + // remove the advisor's role + removeRole(_addr, "advisor"); + } +} diff --git a/contracts/ownership/rbac/RBAC.sol b/contracts/ownership/rbac/RBAC.sol new file mode 100644 index 000000000..39c717c3b --- /dev/null +++ b/contracts/ownership/rbac/RBAC.sol @@ -0,0 +1,101 @@ +pragma solidity ^0.4.18; + +import './Roles.sol'; + + +/** + * @title RBAC (Role-Based Access Control) + * @author Matt Condon (@Shrugs) + * @dev Stores and provides setters and getters for roles and addresses. + * Supports unlimited numbers of roles and addresses. + * See //contracts/examples/RBACExample.sol for an example of usage. + * This RBAC method uses strings to key roles. It may be beneficial + * for you to write your own implementation of this interface using Enums or similar. + */ +contract RBAC { + using Roles for Roles.Role; + + mapping (string => Roles.Role) internal roles; + + /** + * @dev add a role to an address + * @param addr address + * @param roleName the name of the role + */ + function addRole(address addr, string roleName) + internal + { + roles[roleName].add(addr); + } + + /** + * @dev remove a role from an address + * @param addr address + * @param roleName the name of the role + */ + function removeRole(address addr, string roleName) + internal + { + roles[roleName].remove(addr); + } + + /** + * @dev reverts if addr does not have role + * @param addr address + * @param roleName the name of the role + * // reverts + */ + function checkRole(address addr, string roleName) + view + internal + { + roles[roleName].check(addr); + } + + /** + * @dev determine if addr has role + * @param addr address + * @param roleName the name of the role + * @return bool + */ + function hasRole(address addr, string roleName) + view + internal + returns (bool) + { + return roles[roleName].has(addr); + } + + /** + * @dev modifier to scope access to a single role (uses msg.sender as addr) + * @param roleName the name of the role + * // reverts + */ + modifier onlyRole(string roleName) + { + checkRole(msg.sender, roleName); + _; + } + + /** + * @dev modifier to scope access to a set of roles (uses msg.sender as addr) + * @param roleNames the names of the roles to scope access to + * // reverts + * + * @TODO - when solidity supports dynamic arrays as arguments, provide this + * see: https://github.com/ethereum/solidity/issues/2467 + */ + // modifier onlyRoles(string[] roleNames) { + // bool hasAnyRole = false; + // for (uint8 i = 0; i < roleNames.length; i++) { + // if (hasRole(msg.sender, roleNames[i])) { + // hasAnyRole = true; + // break; + // } + // } + + // require(hasAnyRole); + + // _; + // } +} diff --git a/contracts/ownership/rbac/Roles.sol b/contracts/ownership/rbac/Roles.sol new file mode 100644 index 000000000..3ef7e2716 --- /dev/null +++ b/contracts/ownership/rbac/Roles.sol @@ -0,0 +1,55 @@ +pragma solidity ^0.4.18; + + +/** + * @title Roles + * @author Francisco Giordano (@frangio) + * @dev Library for managing addresses assigned to a Role. + * See RBAC.sol for example usage. + */ +library Roles { + struct Role { + mapping (address => bool) bearer; + } + + /** + * @dev give an address access to this role + */ + function add(Role storage role, address addr) + internal + { + role.bearer[addr] = true; + } + + /** + * @dev remove an address' access to this role + */ + function remove(Role storage role, address addr) + internal + { + role.bearer[addr] = false; + } + + /** + * @dev check if an address has this role + * // reverts + */ + function check(Role storage role, address addr) + view + internal + { + require(has(role, addr)); + } + + /** + * @dev check if an address has this role + * @return bool + */ + function has(Role storage role, address addr) + view + internal + returns (bool) + { + return role.bearer[addr]; + } +} diff --git a/test/RBAC.js b/test/RBAC.js new file mode 100644 index 000000000..1846f5698 --- /dev/null +++ b/test/RBAC.js @@ -0,0 +1,77 @@ +const RBACMock = artifacts.require('./helpers/RBACMock.sol') + +import expectThrow from './helpers/expectThrow' + +require('chai') + .use(require('chai-as-promised')) + .should() + +contract('RBAC', function(accounts) { + let mock + + const [ + owner, + anyone, + ...advisors + ] = accounts + + before(async () => { + mock = await RBACMock.new(advisors, { from: owner }) + }) + + context('in normal conditions', () => { + it('allows owner to call #onlyOwnersCanDoThis', async () => { + await mock.onlyOwnersCanDoThis({ from: owner }) + .should.be.fulfilled + }) + it('allows owner to call #onlyAdvisorsCanDoThis', async () => { + await mock.onlyAdvisorsCanDoThis({ from: owner }) + .should.be.fulfilled + }) + it('allows advisors to call #onlyAdvisorsCanDoThis', async () => { + await mock.onlyAdvisorsCanDoThis({ from: advisors[0] }) + .should.be.fulfilled + }) + it('allows owner to call #eitherOwnerOrAdvisorCanDoThis', async () => { + await mock.eitherOwnerOrAdvisorCanDoThis({ from: owner }) + .should.be.fulfilled + }) + it('allows advisors to call #eitherOwnerOrAdvisorCanDoThis', async () => { + await mock.eitherOwnerOrAdvisorCanDoThis({ from: advisors[0] }) + .should.be.fulfilled + }) + it('does not allow owners to call #nobodyCanDoThis', async () => { + expectThrow( + mock.nobodyCanDoThis({ from: owner }) + ) + }) + it('does not allow advisors to call #nobodyCanDoThis', async () => { + expectThrow( + mock.nobodyCanDoThis({ from: advisors[0] }) + ) + }) + it('does not allow anyone to call #nobodyCanDoThis', async () => { + expectThrow( + mock.nobodyCanDoThis({ from: anyone }) + ) + }) + it('allows an owner to remove an advisor\'s role', async () => { + await mock.removeAdvisor(advisors[0], { from: owner }) + .should.be.fulfilled + }) + }) + + context('in adversarial conditions', () => { + it('does not allow an advisor to remove another advisor', async () => { + expectThrow( + mock.removeAdvisor(advisors[1], { from: advisors[0] }) + ) + }) + it('does not allow "anyone" to remove an advisor', async () => { + expectThrow( + mock.removeAdvisor(advisors[0], { from: anyone }) + ) + }) + }) + +}) diff --git a/test/helpers/RBACMock.sol b/test/helpers/RBACMock.sol new file mode 100644 index 000000000..9cd8edead --- /dev/null +++ b/test/helpers/RBACMock.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.4.8; + +import '../../contracts/ownership/rbac/RBAC.sol'; + + +contract RBACMock is RBAC { + + modifier onlyOwnerOrAdvisor() + { + require( + hasRole(msg.sender, "owner") || + hasRole(msg.sender, "advisor") + ); + _; + } + + function RBACMock(address[] _advisors) + public + { + addRole(msg.sender, "owner"); + addRole(msg.sender, "advisor"); + + for (uint256 i = 0; i < _advisors.length; i++) { + addRole(_advisors[i], "advisor"); + } + } + + function onlyOwnersCanDoThis() + onlyRole("owner") + view + external + { + } + + function onlyAdvisorsCanDoThis() + onlyRole("advisor") + view + external + { + } + + function eitherOwnerOrAdvisorCanDoThis() + onlyOwnerOrAdvisor + view + external + { + } + + function nobodyCanDoThis() + onlyRole("unknown") + view + external + { + } + + // owners can remove advisor's role + function removeAdvisor(address _addr) + onlyRole("owner") + public + { + // revert if the user isn't an advisor + // (perhaps you want to soft-fail here instead?) + checkRole(_addr, "advisor"); + + // remove the advisor's role + removeRole(_addr, "advisor"); + } +}