Building RBAC in Go
Mar 24th, 2022

Roie Schwaber-Cohen

Ronen Hilewicz
Authorization |
Integration

Introduction
Role-Based Access Control (RBAC) is an access control pattern that governs the way users access applications based on the roles they are assigned. Roles are essentially groupings of permissions to perform operations on particular resources. Instead of assigning numerous permissions to each user, RBAC allows users to be assigned a role that grants them access to a set of resources. For example, a role could be something like evilGenius
, or a sidekick
. A sidekick
like Morty Smith for example could have the permission to gather mega seeds
, and an evilGenius
like Rick would be able to create
a portal gun
.

We’ll first demonstrate how to implement an RBAC pattern in a Golang application without the use of any open-source libraries, and then review the usage of the casbin
open-source library as well as the Aserto Golang SDK.
Prerequisites
In order to run the examples in this tutorial, you’ll need to have Go installed on your machine.
Setup
The code examples in this tutorial can be found in this repository. To run each of them, navigate to the corresponding directory and run:
go run .
Shared dependencies
Our vanilla and casbin
examples share the user.json
file which contains the users in our system and their roles.
[
{
"id": "beth@the-smiths.com",
"roles": ["clone"]
},
{
"id": "morty@the-citadel.com",
"roles": ["sidekick"]
},
{
"id": "rick@the-citadel.com",
"roles": ["evilGenius", "squanch"]
}
]
In addition, we have three shared dependencies found in the pkg
directory in the go-rbac
repo. Let’s look at those first:
authz
In this package, we define an interface for an Authorizer
:
type Authorizer interface {
HasPermission(userID, action, asset string) bool
}
An Authorizer
must implement a single function, that given a userID
, an action
and an asset
, returns a bool
indicating whether the user has permission to perform the action on the asset.
The actionFromMethod
function is used to map HTTP methods to actions:
func actionFromMethod(httpMethod string) string {
switch httpMethod {
case "GET":
return "gather"
case "POST":
return "consume"
case "DELETE":
return "destroy"
default:
return ""
}
}
Next, we define the middleware function which applies the authorizer on the HTTP call made to the server. The middleware takes an Authorizer
as an argument and returns a http.Handler
function that resolves the action the user is trying to perform, based on the HTTP method called. The function actionFromMethod
is used to resolve the action based on the HTTP method (as described above).
Then, the middleware resolves the asset from the payload of the request and then calls the authorizer to determine whether the user has permission to perform that action on the asset or not. If the user does not have permissions or if no user information is sent with the request, the middleware returns a 403 Forbidden response. Otherwise, it calls the next handler in the chain.
func Middleware(a Authorizer) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, _, ok := r.BasicAuth()
// This is where the password would normally be verified
asset := mux.Vars(r)["asset"]
action := actionFromMethod(r.Method)
if !ok || !a.HasPermission(username, action, asset) {
log.Printf("User '%s' is not allowed to '%s' resource '%s'", username, action, asset)
w.WriteHeader(http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
server
In the server
package, we define the Start
function that simply starts an HTTP server that listens on port 8080.
func Start(handler http.Handler) {
fmt.Println("Staring server on 0.0.0.0:8080")
srv := http.Server{
Handler: handler,
Addr: "0.0.0.0:8080",
}
log.Fatal(srv.ListenAndServe())
}
users
The users
package contains helper functions that load a list of users defined in the users.json
file:
package users
import (
"encoding/json"
"github.com/aserto-demo/go-rbac/pkg/file"
)
type User struct {
ID string `json:"id"`
Roles []string `json:"roles"`
}
type Users map[string]User
func Load() (Users, error) {
jsonBytes, err := file.ReadBytes("../users.json")
if err != nil {
return nil, err
}
var userList []User
if err := json.Unmarshal(jsonBytes, &userList); err != nil {
return nil, err
}
users := Users{}
for _, user := range userList {
users[user.ID] = user
}
return users, nil
}
Vanilla Go RBAC
The first example is a simple RBAC implementation in Go, without the use of any libraries.
The roles.json
file contains all the roles in the system. Each role maps an action to the set of assets on which it can be performed.
{
"clone": {
"gather": ["megaSeeds", "timeCrystals"]
},
"sidekick": {
"gather": ["megaSeeds", "timeCrystals"],
"consume": ["megaSeeds", "timeCrystals"]
},
"evilGenius": {
"gather": ["megaSeeds", "timeCrystals"],
"consume": ["megaSeeds", "timeCrystals"],
"destroy": ["megaSeeds", "timeCrystals"]
}
}
Next, let’s start taking a look at the main.go
file:
package main
import (
"log"
"github.com/aserto-demo/go-rbac/pkg/authz"
"github.com/aserto-demo/go-rbac/pkg/server"
"github.com/aserto-demo/go-rbac/pkg/users"
"github.com/gorilla/mux"
)
Here we import the packages mentioned above, the gorilla/mux
- a request router and dispatcher for HTTP requests, and the log
package.
The authorizer
struct holds the users found in users.json
and the roles loaded from roles.json
:
type authorizer struct {
users users.Users
roles Roles
}
It satisfies the Authorizer
interface by implementing the HasPermission
function that iterates over the user’s roles and looks for one that allows the specified action on the asset being accessed.
func (a *authorizer) HasPermission(userID, action, asset string) bool {
user, ok := a.users[userID]
if !ok {
// Unknown userID
log.Print("Unknown user:", userID)
return false
}
for _, roleName := range user.Roles {
if role, ok := a.roles[roleName]; ok {
resources, ok := role[action]
if ok {
for _, resource := range resources {
if resource == asset {
return true
}
}
}
} else {
log.Printf("User '%s' has unknown role '%s'", userID, roleName)
}
}
return false
}
Finally, the program’s main function creates an HTTP router with authorization middleware that uses an authorizer initialized with the users and roles loaded from the corresponding files. It creates a router with a single handler that serves GET
, POST
, and DELETE
requests to the /api/{asset}
path:
func main() {
users, err := users.Load()
if err != nil {
log.Fatal("Failed to load users:", err)
}
roles, err := LoadRoles()
if err != nil {
log.Fatal("Failed to load roles:", err)
}
middleware := authz.Middleware(&authorizer{users: users, roles: roles})
router := mux.NewRouter()
router.HandleFunc("/api/{asset}", server.Handler).Methods("GET", "POST", "DELETE")
router.Use(middleware)
server.Start(router)
}
Click here to view the full vanilla Go RBAC implementation.
Casbin
Casbin is a powerful and efficient open-source access control library. It has SDKs in many languages, including Javascript, Golang, Rust, Python and more. It provides support for enforcing authorization based on various access control models: from a classic “subject-object-action” model, through RBAC and ABAC models to fully customizable models. It has support for many adapters for policy storage.
In Casbin, the access control model is encapsulated in a configuration file (src/rbac_model.conf
):
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
[policy_effect]
e = some(where (p.eft == allow))
Along with a policy/roles definition file (src/rbac_policy.conf
)
p, clone, megaSeeds, gather
p, clone, timeCrystals, gather
p, sidekick, megaSeeds, consume
p, sidekick, timeCrystals, consume
p, evilGenius, megaSeeds, destroy
p, evilGenius, timeCrystals, destroy
g, sidekick, clone
g, evilGenius, sidekick
- The
request_definition
section defines the request parameters. In this case, the request parameters are the minimally required parameters: subject (sub
), object (obj
) and action (act
). It defines the parameters’ names as well as the order that the policy matcher uses to match the request. - The
policy_definitions
section dictates the structure of the policy. In our example, the structure matches that of the request, containing the subject, object, and action parameters. In the policy/roles definition file, we can see that there are policies (on lines beginning withp
) for each role (clone
,sidekick
, andevilGenius
) - The
role_definition
section is specific for the RBAC model. In our example, the model indicates that an inheritance group (g) is comprised of two members. In the policy/roles definition file, we can see two role inheritance rules forsidekick
andevilGenius
, wheresidekick
inherits fromclone
andevilGenius
inherits fromsidekick
(which means theevilGenius
will also have theclone
permissions). - The
matchers
section defines the matching rules for policy and the request. In our example, the matcher is going to check whether each of the request parameters matches the policy parameters and that the roler.sub
is in the policy.
In this implementation, the dependencies we’ll use are mostly similar to the ones we’ve seen before, with the one difference being the casbin
package:
package main
import (
"log"
"github.com/aserto-demo/go-rbac/pkg/authz"
"github.com/aserto-demo/go-rbac/pkg/server"
"github.com/aserto-demo/go-rbac/pkg/users"
"github.com/casbin/casbin"
"github.com/gorilla/mux"
)
Our authorizer
struct now includes a casbin.Enforcer
field:
type authorizer struct {
users users.Users
enforcer *casbin.Enforcer
}
The implementation of the HasPermission
function uses the enforcer
to check the permissions. We iterate over each role assigned to the user, and get the decision from the enforcer for the iterated role as well as the action and asset passed to the function.
func (a *authorizer) HasPermission(userID, action, asset string) bool {
user, ok := a.users[userID]
if !ok {
// Unknown userID
log.Print("Unknown user:", userID)
return false
}
for _, role := range user.Roles {
if a.enforcer.Enforce(role, asset, action) {
return true
}
}
return false
}
Finally, we define the main
function of our program. We instantiate a new enforcer using the configuration and policy files we defined. We then load our users
file, initialize the router and middleware just like we did in the previous example.
func main() {
enforcer, err := casbin.NewEnforcerSafe("./rbac_model.conf", "./rbac_policy.csv")
if err != nil {
log.Fatal("Failed to create enforcer:", err)
}
users, err := users.Load()
if err != nil {
log.Fatal("Failed to load users:", err)
}
router := mux.NewRouter()
router.HandleFunc("/api/{asset}", server.Handler).Methods("GET", "POST", "DELETE")
router.Use(
authz.Middleware(&authorizer{users: users, enforcer: enforcer}),
)
server.Start(router)
}
Click here to view the full Casbin implementation.
goRBAC
The goRBAC
library is a lightweight role-based access control library for Golang. It is straightforward and easy to use, but isn’t as feature-rich as Casbin.
In this case, the role definition file looks like this:
{
"clone": ["gather-megaSeeds", "gather-timeCrystals"],
"sidekick": [
"gather-megaSeeds",
"gather-timeCrystals",
"consume-megaSeeds",
"consume-timeCrystals"
],
"evilGenius": [
"gather-megaSeeds",
"gather-timeCrystals",
"consume-megaSeeds",
"consume-timeCrystals",
"destroy-megaSeeds",
"destroy-timeCrystals"
]
}
As you can see, each role is assign with permissions which are the action name concatenated with the asset name.
The dependencies we use are mostly similar to the ones we’ve seen before, with the one difference being the gorbac
package.
The HasPermission
function implementation is as follows:
type authorizer struct {
users users.Users
rbac *gorbac.RBAC
permissions gorbac.Permissions
}
func (a *authorizer) HasPermission(userID, action, asset string) bool {
user, ok := a.users[userID]
if !ok {
// Unknown userID
log.Print("Unknown user:", userID)
return false
}
permission := action + "-" + asset
for _, role := range user.Roles {
if a.rbac.IsGranted(role, a.permissions[permission], nil) {
return true
}
}
return false
}
The authorizer struct now includes both the rbac
field and the permissions
field, with the required gorbac.RBAC
object and the gorbac.Permissions
object respectively.
In the HasPermission
function implementation, we create the permission by concatenating the action and the asset passed to the function. Then, we iterate over each of the user’s assigned roles and apply the rbac.IsGranted
function to check if the user has the permission.
The main
function is implemented as follows:
func main() {
// map[RoleId]PermissionIds
var roles map[string][]string
// Load roles information
if err := file.LoadJson("roles.json", &roles); err != nil {
log.Fatal(err)
}
rbac := gorbac.New()
permissions := make(gorbac.Permissions)
// Build roles and add them to goRBAC instance
for rid, pids := range roles {
role := gorbac.NewStdRole(rid)
for _, pid := range pids {
_, ok := permissions[pid]
if !ok {
permissions[pid] = gorbac.NewStdPermission(pid)
}
role.Assign(permissions[pid])
}
rbac.Add(role)
}
users, err := users.Load()
if err != nil {
log.Fatal("Failed to load users:", err)
}
router := mux.NewRouter()
router.HandleFunc("/api/{asset}", server.Handler).Methods("GET", "POST", "DELETE")
router.Use(
authz.Middleware(&authorizer{users: users, rbac: rbac, permissions: permissions}),
)
server.Start(router)
}
In this example, we initialize an gorbac
instance and a gorbac.Permissions
map. We then load the roles information from the roles.json
file, iterate over each role a permission tuple and add it to the gorbac
instance. Finally, we load the users
file and initialize the router and middleware just like in the previous example.
Click here to view the full goRBAC
implementation.
Aserto
Aserto takes a fundamentally different approach to authorization than all of the examples we’ve seen above. First and foremost - Aserto is an authorization service, with an SDK that allows easy integration into the application. Aserto can be deployed as a sidecar to your application - which guarantees maximum availability as well as single millisecond response time for authorization decisions.
There are a couple of additional key differences that sets Aserto apart from the other libraries we’ve reviewed so far.
Policy as Code
What we’ve seen in the examples so far could be grouped into an approach called “Policy as Data”, where the policy itself is reasoned through the data that represents it. Aserto uses a different approach where the policy is expressed and reasoned about as code.
Reasoning about the policy as code makes the policy a lot more natural to write and maintain by developers. It takes away the need to traverse and reason about complex graphs or data structures. It also allows for more flexibility in the policy definition, as policies can be defined in a much more declarative way. Instead of convoluted data structures, developers can write the policy in a way that is a lot more concise and readable - and changes to the policy are made by changing the rules of the policy as opposed to rows in a database.
Users as first-class citizens
With Aserto, users and their roles are first-class citizens. It provides a directory of users and their roles which is continuously synchronized with the Aserto authorizer. This allows Aserto to reason about users and their roles as part of the policy itself - without requiring role resolution as an additional external step (This is why the users.json
file or the resolveUserRoles
function are not going to be required as you’ll see below). Having the role resolution as part of the application comes with its own set of risks - and the directory eliminates the risk of contaminating the decision engine with untrustworthy data.
Aserto policies
Let’s take a look at how policies are defined in Aserto. For the use case we presented, we’ll need a policy for every route the application exposes. Let’s start by reviewing the policy for the GET /api/:asset
route:
package rickandmorty.GET.api.__asset
import future.keywords.in
default allowed = false
allowed {
roles := {"clone", "sidekick", "evil_genius"}
some x in roles
input.user.attributes.roles[_] == x
input.resource.asset == data.assets[_]
}
The first line of the policy defines the name of the package, and it matches the route it protects. Next, we define that by default, the allowed
decision will be false
- this means we’re defaulting to a closed system, where access has to be explicitly granted.
The allowed
clause iterates over each role in the roles
list, and checks if the user has one of these roles assigned to them. The user roles are automatically resolved by Aserto based on the user’s identity.
The last line of the allowed
clause checks whether the asset the user is trying to access is listed in the data.assets
object, which is part of the policy. The asset is passed to the policy as part of the resource context (more details below). A policy can have a data file (data.json
) attached which could be used in the context of the policy. In our case, it includes the list of assets users can access:
{
"assets": ["megaSeeds", "timeCrystals"]
}
Using a separate data file to define the protected assets, we don’t have to explicitly define them in the policy (as we had to do in the previous examples).
The policies for POST /api/:asset
and DELETE /api/:asset
are similar to the ones for GET /api/:asset
, except that the roles associated with each are different.
POST
endpoint:
package rickandmorty.POST.api.__asset
import future.keywords.in
default allowed = false
allowed {
roles := {"sidekick", "evil_genius"}
some x in roles
input.user.attributes.roles[_] == x
input.resource.asset == data.assets[_]
}
DELETE
endpoint:
package rickandmorty.DELETE.api.__asset
default allowed = false
allowed {
input.user.attributes.roles[_] == "evil_genius"
input.resource.asset == data.assets[_]
}
As you can see, the policy for the consume
route is allowing both sidekick
and evilGenius
access, while the policy for the destroy
route is allowing access only to evilGenius
.
Application implementation
The implementation for the application using Aserto is slightly different than the previous examples. Let’s start by examining the dependencies:
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/aserto-demo/go-rbac/pkg/server"
"github.com/aserto-dev/aserto-go/authorizer/grpc"
"github.com/aserto-dev/aserto-go/client"
"github.com/aserto-dev/aserto-go/middleware"
"github.com/aserto-dev/aserto-go/middleware/http/std"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
)
To use the Aserto authorizer, we import the authorizer/grpc
package, as well as the Aserto Golang client and middleware packages. Similar to the other examples, we use gorilla/mux
to handle the routing. Finally, we use godotenv
to load the environment variables from the .env
file.
The authorizer function is implemented as follows:
func AsertoAuthorizer(addr, tenantID, apiKey, policyID string) (*std.Middleware, error) {
ctx := context.Background()
authClient, err := grpc.New(
ctx,
client.WithAddr(addr),
client.WithTenantID(tenantID),
client.WithAPIKeyAuth(apiKey),
)
if err != nil {
return nil, err
}
mw := std.New(
authClient,
middleware.Policy{
ID: policyID,
Decision: "allowed",
},
)
mw.Identity.Mapper(func(r *http.Request, identity middleware.Identity) {
if username, _, ok := r.BasicAuth(); ok {
identity.Subject().ID(username)
}
})
mw.WithPolicyFromURL("rickandmorty")
return mw, nil
}
To initialize the authorizer, we need to pass in the Aserto authorizer address, as well as the API key and Tenant ID. We create the middleware by passing the authorizer client we created and then passing the policy ID we are using, as well as the policy decision we’re interested in getting.
To construct the identity context needed for the authorizer, we use the mw.Identity.Mapper
function which extracts the identity of the user from the http
request, and sets it as a subject-name identifier on the middleware’s identity object.
The mw.WithPolicyFromURL
function tells the middleware to construct the policy path from the incoming request URL. It takes the policy root (in this case rickandmorty
) and then builds the path as <prefix>.METHOD.path.from.request
. So for example, if the request is GET /api/users/:id
(where :id is a parameter), we’ll end up with the policy path rickandmorty.GET.api.users.__id
.
The main
function is straightforward:
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
authorizerAddr := os.Getenv("AUTHORIZER_ADDRESS")
if authorizerAddr == "" {
authorizerAddr = "authorizer.prod.aserto.com:8443"
}
apiKey := os.Getenv("AUTHORIZER_API_KEY")
policyID := os.Getenv("POLICY_ID")
tenantID := os.Getenv("TENANT_ID")
authorizer, err := AsertoAuthorizer(authorizerAddr, tenantID, apiKey, policyID)
if err != nil {
log.Fatal("Failed to create authorizer:", err)
}
router := mux.NewRouter()
router.HandleFunc("/api/{asset}", server.Handler).Methods("GET", "POST", "DELETE")
router.Use(authorizer.Handler)
server.Start(router)
}
We load the .env
file from the root of the project into the environment, and then get the authorizer address, API key, policy ID, and Tenant ID from the file. We then create the authorizer, define the routes as we did in previous examples, and start the server.
Click here to view the full Aserto implementation.
Setting up Aserto
Aserto offers a console for managing policies - to create a new policy, you’ll need to sign in. If you don’t already have an Aserto account, you can create one here.
Add the Acmecorp IDP
To simulate the behavior of a user directory, we’ll add the “Acmecorp IDP”, which include mock users that will be added to our directory. Head on to the Aserto Console, select the “Connections” tab and click the “Add Connection” button.

From the drop-down menu, select “Acmecorp”

Name the provider acmecorp
and give it a description.
Finally, click “Add connection”:

Set up a policy
Click on the “Connections” tab and click on the “Add a connection” button.

From the drop-down menu, select opcr-public
. Name the connection “public-images”, set the display name to be “OPCR Public Images” and click “Add connection”.

Create a policy instance
Click here to create a new policy instance.
Select the “OPCR Public Images” option, select the aserto-templates
organization and the rick-and-morty-rbac
policy repository, then select the 1.0.0
tag.

Name your policy instance go-rbac-dev
and click the “Create an instance” button:

Click on the newly created policy instance, and click on the “Policy Settings” tab. Copy the Policy ID, Authorizer API Key, and Tenant ID to your .env
file.

After updating your .env
file you can start the Aserto example by running go run .
in the aserto
folder.
Testing
To test the authorizer, we can use the following commands:
curl -X GET -f -u beth@the-smiths.com:x http://localhost:8080/api/megaSeeds
curl -X DELETE -f -u rick@the-citadel.com:x http://localhost:8080/api/megaSeeds
Both commands will return:
"Got permission"
To test cases where permission is denied, we can use the following commands:
curl -X POST -f -u beth@the-smiths.com:x http://localhost:8080/api/megaSeeds
curl -X DELETE -f -u morty@the-citadel.com:x http://localhost:8080/api/megaSeeds
Both commands will return:
curl: (22) The requested URL returned error: 403 Forbidden
Summary
In the post, we reviewed multiple ways of adding RBAC to your application. We’ve seen that in most cases, users are not considered a first-class citizen in the authorization offering and that the process of role resolution is left to the developer, and ends up as part of the application itself, which introduces many risks. We’ve also seen that most solutions take the “Policy-as-Data” approach as opposed to the “Policy-as-Code” approach.
While it might seem easier to use a library to implement RBAC in your Golang application, it is important to consider the lifecycle of the application and how it’ll grow. How will new users and roles be added? What would be the implications of changing the authorization policy? How will we reason about the authorization policy when it gets to be more complex?
Using a library means that you assume ownership of the authorization component - which requires time and effort to build and maintain. By using a service such as Aserto you can offload the responsibility of managing the authorization flow - without sacrificing the performance or availability of your application.
This post was written in collaboration with Ronen Hilewicz - Thanks Ronen!

Roie Schwaber-Cohen
Developer Advocate

Ronen Hilewicz
Principal Engineer
Stay informed
Related Content

Adding authorization to a Go app with Aserto
In this tutorial, we'll learn how to add authorization to a Todo app written in Go, using the Aserto Go SDK.
Apr 14th, 2022

Creating a Rego policy for a Todo application
Aserto uses the Open Policy Agent (OPA) as the decision engine for evaluating authorization decisions. Rego is the policy language for defining rules that are evaluated by the OPA engine. In this tutorial, we’ll demonstrate building a Rego policy for a simple Todo application.
Apr 28th, 2022

Policy-as-Code or Policy-as-Data? Why choose?
What is the difference between the policy-as-code and policy-as-data approaches? Which is better? Do you really have to choose? Find out more in this post.
May 26th, 2022
Stay informed