简介
基于角色的访问控制是软件系统的安全需求,旨在为数百个用户提供访问。虽然这种需求通常在企业软件和操作系统中实现,但对以太坊区块链的处理并不多。
当将供应链设计为有向无环图时,我们意识到需要动态地确定谁可以向图中的每个节点添加信息。从现实世界的角度来看,如果您拥有一家制造工厂,您可能希望装配线上的所有操作员都能够用他们自己的帐户记录他们已经组装了一个零件。
OpenZeppelin是我在Solidity开发中使用的金标准,它有一个roles.sol合同,用于在erc721.sol合同中实现诸如minter和burner等角色。不幸的是,这些实现不允许在运行时创建新角色,如果您想使用单独的角色控制对每个单独令牌的访问,则需要创建新角色。
本文旨在展示如何为以太坊区块链构建基于角色的访问控制系统。根据我们的要求从头开始编写RBAC合同,然后从OpenZeppelin中找到了相同想法的版本,它具有几乎相同的方法。为了可重用性,我尽可能地重构我的代码以遵循它们的命名法。
在以下各节中,我将介绍:
1. 我们进入访问系统的设计要求;
2. 智能合约的实施;
3. 测试案例;
4. 状态变换法的gas利用;
5. 还有一些完善的想法。
概念设计
我对RBAC系统的想法很简单。
1. 角色将由数字标识符标识,如Unix中的组。
2. 角色可以动态创建。
3. 每个角色存储用户的地址。
4. 每个角色都会有一个关联的第二个角色,这是唯一允许添加或删除用户的角色。
如果您是使用OpenZeppelin中的Roles.sol和RBAC.sol合同,则需要注意Roles.sol仅实现在角色内生效的操作,而在角色外部发生的操作在RBAC.sol或访问中实现/roles/*Role.sol收缩,包括在创建角色时存储角色的数据结构。
在我的实现中,我根据我们的用例做了一些决策:
· 角色结构中包含一个描述字符串,结构本身存储在一个数组中。数组中每个角色结构的位置用作标识符。有一种使用映射来存储角色,但我发现这里没有必要。
· 每个角色在实例化时接收我们指定为其管理角色的另一个角色的标识符,并且在实例化之后不能修改该角色。此管理员角色是唯一可以为此角色添加和删除承载者的角色。
出于安全性和一致性的原因,您可以从角色中删除承载,但没有方法可以从系统中完全删除角色。
pragma solidity ^0.5.0;
/**
* @title RBAC
* @author Alberto Cuesta Canada
* @notice Implements runtime configurable Role Based Access Control.
*/
contract RBAC {
event RoleCreated(uint256 role);
event BearerAdded(address account, uint256 role);
event BearerRemoved(address account, uint256 role);
uint256 constant NO_ROLE = 0;
/**
* @notice A role, which will be used to group users.
* @dev The role id is its position in the roles array.
* @param description A description for the role.
* @param admin The only role that can add or remove bearers from
* this role. To have the role bearers to be also the role admins
* you should pass roles.length as the admin role.
* @param bearers Addresses belonging to this role.
*/
struct Role {
string description;
uint256 admin;
mapping (address =》 bool) bearers;
}
/**
* @notice All roles ever created.
*/
Role[] public roles;
/**
* @notice The contract constructor, empty as of now.
*/
constructor() public {
addRootRole(“NO_ROLE”);
}
/**
* @notice Create a new role that has itself as an admin.
* msg.sender is added as a bearer.
* @param _roleDescription The description of the role created.
* @return The role id.
*/
function addRootRole(string memory _roleDescription)
public
returns(uint256)
{
uint256 role = addRole(_roleDescription, roles.length);
roles[role].bearers[msg.sender] = true;
emit BearerAdded(msg.sender, role);
}
/**
* @notice Create a new role.
* @param _roleDescription The description of the role created.
* @param _admin The role that is allowed to add and remove
* bearers from the role being created.
* @return The role id.
*/
function addRole(string memory _roleDescription, uint256 _admin)
public
returns(uint256)
{
require(_admin 《= roles.length, “Admin role doesn‘t exist.”);
uint256 role = roles.push(
Role({
description: _roleDescription,
admin: _admin
})
) - 1;
emit RoleCreated(role);
return role;
}
/**
* @notice Retrieve the number of roles in the contract.
* @dev The zero position in the roles array is reserved for
* NO_ROLE and doesn’t count towards this total.
*/
function totalRoles()
public
view
returns(uint256)
{
return roles.length - 1;
}
/**
* @notice Verify whether an account is a bearer of a role
* @param _account The account to verify.
* @param _role The role to look into.
* @return Whether the account is a bearer of the role.
*/
function hasRole(address _account, uint256 _role)
public
view
returns(bool)
{
return _role 《 roles.length && roles[_role].bearers[_account];
}
/**
* @notice A method to add a bearer to a role
* @param _account The account to add as a bearer.
* @param _role The role to add the bearer to.
*/
function addBearer(address _account, uint256 _role)
public
{
require(
_role 《 roles.length,
“Role doesn‘t exist.”
);
require(
hasRole(msg.sender, roles[_role].admin),
“User can’t add bearers.”
);
require(
!hasRole(_account, _role),
“Account is bearer of role.”
);
roles[_role].bearers[_account] = true;
emit BearerAdded(_account, _role);
}
/**
* @notice A method to remove a bearer from a role
* @param _account The account to remove as a bearer.
* @param _role The role to remove the bearer from.
*/
function removeBearer(address _account, uint256 _role)
public
{
require(
_role 《 roles.length,
“Role doesn‘t exist.”
);
require(
hasRole(msg.sender, roles[_role].admin),
“User can’t remove bearers.”
);
require(
hasRole(_account, _role),
“Account is not bearer of role.”
);
delete roles[_role].bearers[_account];
emit BearerRemoved(_account, _role);
}
}
测试
我喜欢公开测试智能合约,既展示了操作案例,又能对代码的可靠性提供了一些信心。
Contract: RBAC
RBAC
✓ addRootRole creates a role.
✓ hasRole returns false for non existing roles.
✓ hasRole returns false for non existing bearerships.
✓ addRootRole adds msg.sender as bearer.
✓ addRole doesn’t add msg.sender with admin role.
✓ addBearer reverts on non existing roles.
✓ addBearer reverts on non authorized users.
✓ addBearer reverts if the bearer belongs to the role.
✓ addBearer adds a bearer to a role.
✓ removeBearer reverts on non existing roles.
✓ removeBearer reverts on non authorized users.
✓ removeBearer reverts if the bearer doesn‘t belong to the role.
✓ removeBearer removes a bearer from a role.
为了回应之前的反馈,我现在还使用eth-gas-reporter的gas使用报告。
结论
本文描述了一个基于智能合约角色的访问控制系统的实现,它具有以下属性:
1. 允许在系统运行时创建新角色。
2. 包括角色管理员的概念,允许添加和删除角色的成员。
3. 允许轻松确定所有现有角色及其承载。
4. 基于角色的访问控制实现起来并不一定复杂,但正如本文所示,需要考虑许多权衡和设计决策,这些决策与您的用户及其允许的操作密切相关 去设计。如果您决定复用RBAC系统的这种实现,我会很高兴,但我也鼓励您寻找并考虑其他选择。
评论
查看更多