Переглянути джерело

feat: RBAC authentication contract and role library

Matt Condon 7 роки тому
батько
коміт
e931c1cbfc

+ 61 - 0
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");
+    }
+}

+ 101 - 0
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);
+
+    //     _;
+    // }
+}

+ 55 - 0
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];
+    }
+}

+ 77 - 0
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 })
+      )
+    })
+  })
+
+})

+ 68 - 0
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");
+    }
+}