Merge pull request #580 from Shrugs/feat/rbac
feat: RBAC authentication contract and role librarypull/600/head
commit
83b941c76c
@ -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…
Reference in new issue