Skip to content

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 ints. The base PM operates purely on ints, 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 enums into ints. 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 enums can be directly casted as ints in Solidity. (Enums are basically wrappers around ints for convenience of naming.)

In the next sections, it describes permissions and roles as ints 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 ints 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 1s in every position required, and 0s 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 kth 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 kth position. First, we calculate the integer mask where there is a 1 only in the kth 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 kth 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
Represent numbers in binary:

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