Skip to content

feat: RBAC authentication contract and role library #580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions contracts/ownership/rbac/RBAC.sol
Original file line number Diff line number Diff line change
@@ -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);

// _;
// }
}
55 changes: 55 additions & 0 deletions contracts/ownership/rbac/Roles.sol
Original file line number Diff line number Diff line change
@@ -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];
}
}
98 changes: 98 additions & 0 deletions test/RBAC.test.js
Original file line number Diff line number Diff line change
@@ -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 })
);
});
});
});
16 changes: 16 additions & 0 deletions test/helpers/expectEvent.js
Original file line number Diff line number Diff line change
@@ -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,
};
69 changes: 69 additions & 0 deletions test/mocks/RBACMock.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}