Merge pull request #580 from Shrugs/feat/rbac

feat: RBAC authentication contract and role library
pull/600/head
Francisco Giordano 7 years ago committed by GitHub
commit 83b941c76c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 157
      contracts/ownership/rbac/RBAC.sol
  2. 55
      contracts/ownership/rbac/Roles.sol
  3. 98
      test/RBAC.test.js
  4. 16
      test/helpers/expectEvent.js
  5. 69
      test/mocks/RBACMock.sol

@ -0,0 +1,157 @@
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.
* It's also recommended that you define constants in the contract, like ROLE_ADMIN below,
* to avoid typos.
*/
contract RBAC {
using Roles for Roles.Role;
mapping (string => Roles.Role) private roles;
event RoleAdded(address addr, string roleName);
event RoleRemoved(address addr, string roleName);
/**
* A constant role name for indicating admins.
*/
string public constant ROLE_ADMIN = "admin";
/**
* @dev constructor. Sets msg.sender as admin by default
*/
function RBAC()
public
{
addRole(msg.sender, ROLE_ADMIN);
}
/**
* @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);
RoleAdded(addr, roleName);
}
/**
* @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);
RoleRemoved(addr, roleName);
}
/**
* @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
public
{
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
public
returns (bool)
{
return roles[roleName].has(addr);
}
/**
* @dev add a role to an address
* @param addr address
* @param roleName the name of the role
*/
function adminAddRole(address addr, string roleName)
onlyAdmin
public
{
addRole(addr, roleName);
}
/**
* @dev remove a role from an address
* @param addr address
* @param roleName the name of the role
*/
function adminRemoveRole(address addr, string roleName)
onlyAdmin
public
{
removeRole(addr, roleName);
}
/**
* @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 admins
* // reverts
*/
modifier onlyAdmin()
{
checkRole(msg.sender, ROLE_ADMIN);
_;
}
/**
* @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 to modifiers, 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);
// _;
// }
}

@ -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];
}
}

@ -0,0 +1,98 @@
import expectThrow from './helpers/expectThrow';
import expectEvent from './helpers/expectEvent';
const RBACMock = artifacts.require('./mocks/RBACMock.sol');
require('chai')
.use(require('chai-as-promised'))
.should();
const ROLE_ADVISOR = 'advisor';
contract('RBAC', function (accounts) {
let mock;
const [
admin,
anyone,
futureAdvisor,
...advisors
] = accounts;
before(async () => {
mock = await RBACMock.new(advisors, { from: admin });
});
context('in normal conditions', () => {
it('allows admin to call #onlyAdminsCanDoThis', async () => {
await mock.onlyAdminsCanDoThis({ from: admin })
.should.be.fulfilled;
});
it('allows admin to call #onlyAdvisorsCanDoThis', async () => {
await mock.onlyAdvisorsCanDoThis({ from: admin })
.should.be.fulfilled;
});
it('allows advisors to call #onlyAdvisorsCanDoThis', async () => {
await mock.onlyAdvisorsCanDoThis({ from: advisors[0] })
.should.be.fulfilled;
});
it('allows admin to call #eitherAdminOrAdvisorCanDoThis', async () => {
await mock.eitherAdminOrAdvisorCanDoThis({ from: admin })
.should.be.fulfilled;
});
it('allows advisors to call #eitherAdminOrAdvisorCanDoThis', async () => {
await mock.eitherAdminOrAdvisorCanDoThis({ from: advisors[0] })
.should.be.fulfilled;
});
it('does not allow admins to call #nobodyCanDoThis', async () => {
expectThrow(
mock.nobodyCanDoThis({ from: admin })
);
});
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 admin to remove an advisor\'s role', async () => {
await mock.removeAdvisor(advisors[0], { from: admin })
.should.be.fulfilled;
});
it('allows admins to #adminRemoveRole', async () => {
await mock.adminRemoveRole(advisors[3], ROLE_ADVISOR, { from: admin })
.should.be.fulfilled;
});
it('announces a RoleAdded event on addRole', async () => {
expectEvent.inTransaction(
mock.adminAddRole(futureAdvisor, ROLE_ADVISOR, { from: admin }),
'RoleAdded'
);
});
it('announces a RoleRemoved event on removeRole', async () => {
expectEvent.inTransaction(
mock.adminRemoveRole(futureAdvisor, ROLE_ADVISOR, { from: admin }),
'RoleRemoved'
);
});
});
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 })
);
});
});
});

@ -0,0 +1,16 @@
const assert = require('chai').assert;
const inLogs = async (logs, eventName) => {
const event = logs.find(e => e.event === eventName);
assert.exists(event);
};
const inTransaction = async (tx, eventName) => {
const { logs } = await tx;
return inLogs(logs, eventName);
};
module.exports = {
inLogs,
inTransaction,
};

@ -0,0 +1,69 @@
pragma solidity ^0.4.8;
import '../../contracts/ownership/rbac/RBAC.sol';
contract RBACMock is RBAC {
string constant ROLE_ADVISOR = "advisor";
modifier onlyAdminOrAdvisor()
{
require(
hasRole(msg.sender, ROLE_ADMIN) ||
hasRole(msg.sender, ROLE_ADVISOR)
);
_;
}
function RBACMock(address[] _advisors)
public
{
addRole(msg.sender, ROLE_ADVISOR);
for (uint256 i = 0; i < _advisors.length; i++) {
addRole(_advisors[i], ROLE_ADVISOR);
}
}
function onlyAdminsCanDoThis()
onlyAdmin
view
external
{
}
function onlyAdvisorsCanDoThis()
onlyRole(ROLE_ADVISOR)
view
external
{
}
function eitherAdminOrAdvisorCanDoThis()
onlyAdminOrAdvisor
view
external
{
}
function nobodyCanDoThis()
onlyRole("unknown")
view
external
{
}
// admins can remove advisor's role
function removeAdvisor(address _addr)
onlyAdmin
public
{
// revert if the user isn't an advisor
// (perhaps you want to soft-fail here instead?)
checkRole(_addr, ROLE_ADVISOR);
// remove the advisor's role
removeRole(_addr, ROLE_ADVISOR);
}
}
Loading…
Cancel
Save