/ Engineering

Here's how we added RBAC to our web app

When Simple OKR started out, it was an app for a single user. There were no teams or multi-user support. Multi-user support was added a bit later after the launch. This feature allowed to onboard teams and companies onto the product. Even then, it was a very bare bones implementation, and we had no dedicated user profile pages or access controls. Recently we introduced user roles to Simple OKR, and I wanted to share with you how it was implemented. It's not a perfect solution and will most likely change in the future, but here it is.

Simple OKR is written in Go. It is written as a monolith and follows the Model-View-Controller pattern. A typical request processing flow looks something like this:

  1. HTTP request is made by a browser.
  2. The request passes several middlewares before it gets routed to the handler.
  3. Authentication middleware updates the context of http.Request with auth.User.
  4. The handler evaluates any number of access policies and proceeds on success.
  5. The view is rendered and any additional view policies are evaluated at render time.

Such request processing is fairly common and can be found in popular web frameworks like Django.

Every user in the system has a role attached to them. It's simply stored as a separate column in the database. We have 3 user roles at the moment.

Role Description
Owner Organization owner. User account that created the organization.
Admin Admin privileges. Can perform administrative tasks such as manage subscriptions and users.
User Regular user. Access is restricted to certain pages.

Owner role is more of a formality, but from the user's perspective it's treated the same way as Admin.

OK, so we have user roles. The user and role gets loaded when the request is processed by the authentication middleware. When we reach the request handler, we evaluate one or more access policies. All policies follow the same interface which looks like this:

type AccessPolicy interface {
    HasAccess() bool
}

And here's one of the concrete implementations that requires Owner role to proceed:

type OwnerAccessPolicy struct {
    User auth.User
}

func (p OwnerAccessPolicy) HasAccess() bool {
    if !(ActiveUserRequiredPolicy{User: p.User}).HasAccess() {
        return false
    }

    if p.User.Role() == auth.RoleOwner {
        return true
    }

    return false
}

Access policies are implemented in code and are very simple. We use the same AccessPolicy interface to restrict access to various pages and handle pages that require user login. Access policies are evaluated at the beginning of the handler function and can be targeted at specific HTTP request methods (e.g. maybe you can view the resource, but you can't make any modifications to it).

There's one more place where we evalute access policies -- and that's views. We have custom template functions for checking whether the user has particular role, e.g. is_admin. Each of these functions takes a user instance that's always part of the render context. These template functions evaluate the same set of access policies as handlers. They are really useful if we want to display certain things only to admins or owners. For example, link to user management may not be available to regular user but is shown to admins.

This is how we implemented role based access control in our web app. I hope you found something useful in this post. If you're using OKR at your company give Simple OKR a show as we offer 14-day free trial.

Here's how we added RBAC to our web app
Share this