Modern complex projects often require sophisticated access-control functionality, like Identity and Access Management (IAM ) systems. Sometimes, however there are cases where simple solutions might just cut it. Let’s cover one of these cases.

Say we’re working on a system for publishing articles, allowing discussion below each article, with the following requirements:

  • Moderators can edit all articles and comments
  • Authors can create articles and edit only those created by themselves
  • Users can read articles and post comments about them
  • Any user-type can edit and delete their own profile

We could define different user-levels as a bit-mask and store that alongside user’s information (e.g. users table in the database):

const (
	user = 1 << iota    // 0001
	author              // 0010
	moderator           // 0100
	self                // 1000
)

Each of the actions listed above can now be expressed as a list of user types allowed to perform them, by using the bitwise OR operator, omitting actions allowed by everybody even without an account:

const (
	CreateArticle = author
	UpdateArticle = self | moderator
	DeleteArticle = self | moderator

	CreateComment = user | author | moderator
	UpdateComment = moderator
	DeleteComment = moderator

	UpdateUser = self
	DeleteUser = self
)

Each action has only the bits of every user type set to 1. For instance CreateArticle is 2 (0010), UpdateArticle is 12 (1100) and CreateComment is 7 (0111). We could now create a function which determines whether given action is allowed by simply checking if they share at least one bit set to 1, for which we’re going to utilize the bitwise AND operator.

self is a bit tricky here, because it must be populated differently per-entity. So we should make sure that both users, articles and comments have ID:

type User struct {
	ID   ID
	Role int
	Name string
	// ...
}

type Article struct {
	ID     ID
	Author ID
	Name   string
	// ...
}

type Comment struct {
	ID      ID
	Author  ID
	Article ID
	// ...
}

The ID type here can be anything comparable (e.g. int, string, UUID). Now the function in question is painfully obvious:

func Allowed(u User, action int, target ID) bool {
	if user.ID == target {
		u.Role |= self
	}

	return u.Role&action != 0
}

target is the ID of whatever is being acted upon or left with the zero value for that type when not applicable (e.g. on any Create… action). In order to check whether a user (u) can update given article (a) we call Allowed(u, UpdateArticle, a.Author) and handle (e.g. return error) when the result is false.

However…

You’ve probably already figured out that this mimics the way Unix operating systems encode filesystem access. I could think of a few pros to this approach:

  • The check whether given action is allowed is trivial
  • Adding new user type is a single line of code. Each new user adds exactly one bit to the mask, so until we go above 64, everything’s fine
  • Permissions are obvious even to non-technical people
  • Same is applicable to pretty much any programming language I can think of

However, this does not come without gotchas. Take that self which is not really a user type, but a made-up concept to allow everybody to edit some of their own creations. We had to add it, because it’s not a desired capability for all entities. Comments for instance can NOT be edited, once posted. Not only that, we had to add a new argument and logic to our little pure function.

One could think of a few new requirements that would seem simple, yet require more arguments, more checks and eventually a complete overhaul of this whole idea. Technically speaking, even the Unix permissions turned out to be not enough and modern Unix-like operating systems implement more complex ACLs stored in the extended file attributes on top of it.