Skip to main content
Version: master

ADR 014: Improve the permission system

Changelog

  • May 19th, 2022: First draft;

Status

ACCEPTED Implemented

Abstract

This ADR introduces a new way of registering and managing permissions within the x/subspaces module all the other modules that depend on it.

Context

Currently, Desmos implements a permission system that is very similar to the one used within Linux-based systems: each permission is represented by an uint value treated reading its bits individually. The combination of a permission is thus obtained using the | operator, and to check if a specific permission is set it's sufficient to check the value of the associated bit. Although this works, it has some limitations to it:

  • clients need to know the value of each permission, and how to properly combine them to form the final permission value they want to set;
  • permission values that are given from outside (e.g. a client setting a permission for a user) need to be sanitized in order to grant forward-compatibility;
  • all permissions need to be put inside the x/subspaces module, so that a proper validity mask can be computed and used to sanitize permissions values.

Decision

To allow for an easier way of managing permissions, we will change the entire permission system from being based on a bitwise system to be based on a more human-readable system. Instead of representing each permission value as a uint and then reading individual bits, we will represent each permission using a string value that can easily tell what the permission is about. Here are some examples:

package types

type Permission string

const (
PermissionManageSubspace Permission = "PERMISSION_MANAGE_SUBSPACE"
PermissionEverything Permission = "PERMISSION_EVERYTHING"
)

In order to properly support any kind of permission from different modules, we will implement the following method within the x/subspaces module:

var (
// registeredPermissions represents the list of permissions that are registered and should be considered valid
registeredPermissions []Permission
)

// RegisterPermission allows to register the permission with the given name and returns its value
func RegisterPermission(permissionName string) Permission {
permission := Permission(strings.ToUpper(strings.ReplaceAll(permissionName, " ", "_")))
for _, registeredPermission := range registeredPermissions {
if registeredPermission == permission {
panic(fmt.Errorf("permission %s has already been registered", permission))
}
}

registeredPermissions = append(registeredPermissions, permission)
return permission
}

This will allow all modules to register whatever permission they want, and then use them freely while performing checks:

// Define the permissions for the posts module
var (
PermissionCreatePost = subspacestypes.RegisterPermission("create post")
PermissionEditPost = subspacestypes.RegisterPermission("edit post")
)

// Check such permissions
keeper.SubspacesKeeper.HasPermissions(ctx, subspaceID, user, PermissionCreatePost, PermisisonEditPost)

When setting a user or a group's permissions, we will then require the users to provide them as a string array, and then we simply check whether the given permissions are registered inside the supported ones or not. If they are, we store the entire array on the store.

Types

UserGroup

We will change the UserGroup type to support this new permission system by replacing the permissions field from being a uint32 to be a repeated string:

syntax = "proto3";

// UserGroup represents a group of users
message UserGroup {
// ID of the subspace inside which this group exists
uint64 subspace_id = 1;

// Unique id that identifies the group
uint32 id = 2;

// Human-readable name of the user group
string name = 3;

// Optional description of this group
string description = 4;

// Permissions that will be granted to all the users part of this group
repeated string permissions = 5;
}

UserPermission

Instead of storing user permissions as follows:

UserPermissionPrefix | SubspaceID | User | -> uint32 

We will define a new UserPermission object:

syntax = "proto3";

// UserPermission contains the details of a single user permissions withing a subspace
message UserPermission {
// Address of the user
bytes user = 1;

// Permissions set to the user
repeated string permissions = 2;
}

then the permissions of a user will be stored as follows:

UserPermissionPrefix | SubspaceID | User | -> ProtcolBuffer(UserPermission)

Msg Service

We will change the Msg service of the x/subspaces module to properly allow setting permissions:

syntax = "proto3";

// MsgSetUserGroupPermissions represents the message used to set the permissions of a user group
message MsgSetUserGroupPermissions {
uint64 subspace_id = 1;
uint32 group_id = 2;
repeated string permissions = 3;
string signer = 4;
}

// MsgSetUserPermissions represents the message used to set the permissions of a specific user
message MsgSetUserPermissions {
uint64 subspace_id = 1;
string user = 2;
repeated string permissions = 3;
string signer = 4;
}

Query Service

We will change the Query service of the x/subspaces module to properly returns new permission values:

syntax = "proto3";

// QueryUserPermissionsRequest is the response type for the Query/UserPermissions method
message QueryUserPermissionsResponse {
repeated string permissions = 1;
repeated PermissionDetail details = 2;
}

Consequences

Backwards Compatibility

The above detailed solution is not backward compatible. For this reason, we will need to write a migration code that reads all the currently set permissions and performs the following operations:

  • split the permission value into individual permissions;
  • map each individual permission to the new Permission type;
  • replace the current store values with the new Permission values.

This will need to be performed for all user groups as well as all individual user permissions.

Positive

  • Allows to define permission in all modules
  • No need to sanitize the values received from clients
  • Easier permission setting from clients (values are now human-readable)

Negative

  • Storing string instead of uint takes up more storage space

Neutral

Further Discussions

In the future, we might even allow developers to register custom permissions within their own subspaces, and then request those permissions during the execution of different messages. This could be done by allowing them to specify a MsgTypeURL -> []Permission entry for each kind of message type, so that when a message with MsgTypeURL gets executed, the user needs to have the specified permission in order to successfully perform the request.

Test Cases

  • Migrating from old permissions to new permissions work properly and no data is lost

References