Permission Management
Permission managers associate a set of permissions with a specific role type (see User Roles). User’s are individually granted the permissions associated with their granted role. The permissions defined in an app are created based on the app's included functionality, and they are grouped together by role based on the app’s required business logic.
Permissions are checked in smart contract functions whenever needed, such as calling a function, or doing a specific sub-action within the app. Regardless of what line the permission check happens on, the most important part to know is that a user's permissions for an action must be checked whenever that action is being performed. The actual checking of permissions does not happen implicitly, rather the smart contract programmer must invoke the permission checking function call for each action.
To use the BlockApps Solidity Library base Permission Manager, define a custom App Permission Manager that inherits from the base PermissionManager
class. This allows it to access to the core permission management functionality built into the contract.
A permission manager needs a set of permissions, aka the actions that available to be done in the app, and a set of roles or group names that users in the app belong to. These must be defined as enums
with a NULL
and MAX
value for the Role
enum.
Building a Role-Based Permission Manager
Below is an example for a basic app that defines a Create, Read, and Update action on an asset, and two types of users, an Admin and a User.
Example:
contract Permission {
enum PermissionEnum {
CREATE,
READ,
UPDATE
}
}
contract Role {
enum RoleEnum {
NULL,
ADMIN,
USER,
MAX
}
}
contract AppPermissionManager is PermissionManager, Role, Permission {
PermissionManager private basePM;
address public parentContract;
constructor(address _admin, address _master) public {
parentContract = msg.sender;
basePM = new PermissionManager(address(this), _master);
grantRole('Dapp Deployer', tx.origin, RoleEnum.ADMIN); // give the dapp uploader an admin role
}
}
Next, we need to define which roles get what permissions. So for this example, lets say an Admin has all permissions, and a User only has Create and Update Permissions.
In order to do this, we first need to understand how permissions are represented in the Base PermissionManager. They are represented as integer bitmaps, where a 1
at a given position indicates that the user does have that permission. An action (permission) is associated with a specific position in the bitmap based on its int
value casted from its enum
. So the CREATE
permission/action corresponds with the bit in the zeroth position, and the UPDATE
permission corresponds with the bit in the second position.
Now that we know that we know how to assign a role an individual permission, we can construct the bitmap for a role's entire permissions. It is typically best practice to separate the contract that maps a Role to its Permissions, and the actual permission manager.
Constructing the role's bitmap is as easy as OR'ing the allowed permissions together, shifting a 1
by Permission
bits.
contract RolePermissions is Role, Permission, RestStatus {
// data structure to associate a Role with a bitmap of permissions
// A roles permissions are located at rolePermissions[uint(role)]
uint[] rolePermissions;
constructor() public {
rolePermissions.length = uint(RoleEnum.MAX);
// setup empty role values
rolePermissions[uint(RoleEnum.NULL)] = 0;
rolePermissions[uint(RoleEnum.MAX)] = 0;
// cast each role enum value to an int so it can be indexed
// OR all values together to make a map where there are 1s in every position
// that this role has permissions
rolePermissions[uint(RoleEnum.USER)] =
// bit shift 1 by the int value of the permission
(1 << uint(Permission.CREATE)) |
(1 << uint(Permission.UPDATE)) ;
rolePermissions[uint(RoleEnum.ADMIN)] =
(1 << uint(Permission.CREATE)) |
(1 << uint(Permission.READ)) |
(1 << uint(Permission.UPDATE)) ;
}
function getRolePermissions(Role _role) public view returns (uint) {
// Get permissions (bitmap) for the role
return rolePermissions[uint(_role)];
}
}
Next in our AppPermissionManager
we need to actually grant a user a role's permissions. This is as simple as creating a mapping between a user's address, and their role's permission bitmap.
contract AppPermissionManager ... {
function grantRole(string _id, address _address, RoleEnum _role) public returns (uint, uint) {
// if you are not the dapp (in constructor),
// then this action is forbidden
// some implementations also may need to self-reference to check if the
// caller has permissions to grant roles
if (msg.sender != parentContract) {
return (RestStatus.FORBIDDEN, 0);
}
// Get permission bitmap for a role
uint permissions = getRolePermissions(_role);
// Grant role to a user using the basePM
return basePM.grant(_id, _address, permissions);
}
}
Finally, the AppPermissionManager must implement some way of checking for a user's permissions. This is done by calling the Base PermissionManager
's check
function, providing a user address and an integer representation of a Permission enum
value. In apps, we typically wrap the call to check
by a named function corresponding with the action, so it easier use.
contract AppPermissionManager is ... {
function canCreate(address _address) returns (bool) {
uint p = 1 << uint(Permission.CREATE);
return basePM.check(_address, p) == RestStatus.OK;
}
function canRead(address _address) returns (bool) {
uint p = 1 << uint(Permission.READ);
return basePM.check(_address, p) == RestStatus.OK;
}
function canUpdate(address _address) returns (bool) {
uint p = 1 << uint(Permission.UPDATE);
return basePM.check(_address, p) == RestStatus.OK;
}
}
Integrating a Permission Manager
To use the AppPermissionManager
in your app, simply construct an instance of one in your Dapp's primary contract, and call its can<DoAction>
functions from the function's caller.
Dapp:
contract Dapp {
AppPermissionManager permissionManager;
constructor() {
permissionManager = new AppPermissionManager(tx.origin, tx.origin);
}
}
Asset:
contract Asset {
PermissionManager pm;
constructor() {
...
Dapp tempDapp = Dapp(account(address(100),“parent”));
pm = PermissionManager(account(tempDapp.permissionManager(), “parent”));
// check the permissions of the function caller
require(pm.canCreate(tx.origin));
...
}
function updateAsset(...) {
// check the permissions of the function caller
require(pm.canUpdate(tx.origin));
// update Asset's fields...
}
}
Your application's middleware might also need to check permissions of a user making an API call, so apps typically define function calls to each permission checking function:
// permissionManager.js
const can = async (admin, contract, methodArgs, options) => {
const { method, address } = methodArgs
const args = { address }
const callArgs = { contract, method, args: util.usc(args) }
const [isPermitted] = await rest.call(admin, callArgs, options)
return isPermitted
}
const canCreate = async (admin, contract, args, options) => can(admin, contract, { ...args, method: ‘canCreate’ }, options)
These function can then be used anywhere in the app to check if a user has permission to do an action, before the action is actually sent to STRATO. This is most typically done before private shard/asset shard creation.
These permission checking functions are bound to the primary dapp object in dapp.js
.
See more about function binding in the Asset Framework Documentation
Other Permission Management Methods
Aside from role-based permission management, Dapps can also implement more fine-tuned permissioning on Asset functions through a number of methods based on user identity in relation to data stored in the Asset. Below are some of the most commonly used methods. Of course, these methods may be used in conjunction with a role-based permission manager.
Asset Owner
This method controls the access of a function to only the currently assigned owner
of an asset. The owner of an asset is an address state variable in the Asset Contract. This is initially assigned to Asset Creator. This control method is already implemented in the transferOwnership
method of any Asset generated in the Asset Framework.
contract Asset {
address owner;
constructor() {
owner = tx.origin;
}
function transferOwnership(address _newOwner) returns (uint) {
if (tx.origin != owner) {
return 403;
}
...
}
}
Organization Membership
This is a less strict version of the method above, that simply requires that a function caller must be in the same organization as the owner
. This allows any user of the same organization to perform an operation. Useful for when asset control is more based on the organization than a specific user. Combine this with a role-based permission manager to ensure a function caller is both part of a specific organization and has a specific role in that organization.
contract Asset {
address owner;
string ownerOrg;
constructor() {
owner = tx.origin;
ownerOrg = getUserCert(tx.origin)["organization"];
}
function transferOwnership(address _newOwner) returns (uint) {
if (getUserCert(tx.origin)["organization"] != ownerOrg) {
return 403;
}
...
}
}
Party/Counterparty
Often Assets will need to be assigned to be managed by another organization as it moves along in its lifecycle. However we also still want the owner of the asset to manage it as well.
contract Asset {
uint public data;
address owner;
string counterParty;
constructor() {
owner = tx.origin;
ownerOrg = getUserCert(tx.origin)["organization"];
}
function assignCounterParty(string _counterParty) returns (uint) {
if (tx.origin != owner) {
return 403;
}
counterParty = _counterParty;
return 200;
}
function updateByCounterParty(uint _data) returns (uint) {
if (getUserCert(tx.origin)["organization"] != counterParty) {
return 403;
}
data = _data;
return 200;
}
}
Advanced
This section describes how the Base Permission Manager Contract works in more detail.
Enums are a way to translate human-identifiable concepts like permission/action names or roles into more machine-readable values, which are int
s. The base PM operates purely on int
s, and expects users of the contract to implement a level of abstraction (the app's own Permission Manager) on top of it that translates the application specific enum
s into int
s. This allows the PM to abstract itself away from specific business logic, such as permission and role names, and make it composable with any other application.
This is possible since enum
s can be directly casted as int
s in Solidity. (Enums are basically wrappers around int
s for convenience of naming.)
In the next sections, it describes permissions and roles as int
s for ease of explanation, however know that these can be substituted for Solidity enum values defined in your app for same entities.
Defining Permissions
The base PM stores a role's permissions as an integer bitmap. In the Solidity, this bitmap is stored as an int
. Therefore for every role defined in the app, there is single corresponding bitmap that defines every permission this specific role has. A 1
at a given bit position k
for role j
indicates that role j
has permission k
. Otherwise the bit is 0
, indicating role j
does not have permission k
.
Every action that can be done in the app (i.e. create assets, update, transfer, etc.) must initially be defined in a app-specific enum
. This is typically referred to as the "Permission Enums" or just "the permissions". By representing them as enum
values, they can predictably be casted to int
s across any contract. A permission's int
value is essentially what uniquely defines it - it is the bit position k
where a role's assigned permissions are stored for this particular permission's actions. So if a permission ("update" for example) has an integer value of 3
, a role would store a 1
in the 3rd bit position of its bitmap. Similarly
Therefore, to define a set of permissions to a given role, we must determine the int
value where there are 1
s in every position required, and 0
s every where else. First a mask for each integer value has to be calculated for a given permission where there is a 1
in only the k
th position. This is done by left bitshifting 1
by k
:
int p = (uint(Permission.PERMISSION_NAME) << 1);
Then each individual permission integer value is bitwise OR'd together to allow each 1
to retain its place in the bitmap
int rolePermissionsBitmap = (uint(Permission.PERMISSION_NAME) << 1)
| (uint(Permission.OTHER_PERMISSION) << 1);
| (uint(Permission.ANOTHER_PERMISSION) << 1);
This must be done for every role.
Once the bitmap for each role is defined, they must stored in association with the role it represents. This is done simply by using a Solidity mapping(int => int)
, where the index is a role's int
value, and the value is the permission's bitmap.
function makePermissions() {
...
rolePermissions[int(role)] = rolePermissionsBitmap;
}
Assigning Permisisons
Permissions are assigned on a per-user-account basis, rather than checking a user's role in a seperate user role manager, and looking up that role's permissions. This removes an extra layer of indirection and an extra dependency.
So the PM stores a mapping(address => int)
, where the index is specific user's address, and the value is tje permission bitmap corresponding to the role which they were assigned.
Since the bitmaps for each role are already calculated, they can simply be looked up in the previously mentioned role-to-bitmap mapping
.
function assignPermission(address _user, Role _r) {
userPermissions[_user] = rolePermissions[int(_r)];
}
Fetching Permissions
Next we demonstrate how a particular user's permissions are fetched, and subsequently determined to have a permission.
In an external contract when the corresponding action is taking place, a call to check the function caller's permissions must be done. This call to the Permission Manager must return a bool
value so that the app can properly exit the execution of the function. So the Permission Manager must expose a function like can<Permission>(address _user);
where <Permission>
is some Permission name. The function internally hardcodes what permission will be checked, so the external contract does not need to directly pass in a Permission enum
value.
Any function of this nature must first fetch a user's stored permissions:
function canUpdate() returns (bool) {
Permission p = int(Permissions.UPDATE);
int permissions = userPermissions[_user];
...
}
Next, for some permission k
provided to the function, it must be determined if the returned permission bitmap has a a 1
in the k
th position. First, we calculate the integer mask where there is a 1
only in the k
th position:
int mask = (int(Permissions.UPDATE) << 1);
The mask is then used by performing a bitwise AND between it and the permission's bitmap. This will extract only the value of the bit in the k
th position:
int result = permissions & mask;
To determine if the user has permissions for this action, we determine if the result
is greater than 1
, since if a user does have permissions, the result will equal mask
, and if not, it will equal 0
.
function canUpdate() returns (bool) {
Permission mask = int(Permissions.UPDATE) << 1;
int permissions = userPermissions[_user];
return (permissions & mask) >= 1;
}
As an example, if we have a set of permissions:
contract Permissions {
enum PermissionsEnum {
CREATE,
READ,
UPDATE
}
}
And a user is assigned permissions CREATE
and UPDATE
, they would have a permission bitmap value of 5
.
If we were to call canUpdate
for this user, the following calculation is done:
Calculate Mask:
2 << 1
2 << 0001
Mask result:
0100
Perform AND with mask
As previously mentioned, we already know the user's permissions are 5
, so the calculation is as follows:
5 & 0100
Represent numbers as binary:
0101 & 0100
Perform Bitwise AND:
0101 &
0100
------
0100
Compare values:
0100 >= 0001 == true