How to Implement Authorization Using Cerbos in Go | Cerbos

How to implement authorization using Cerbos in Go

KKunal VermaFebruary 18, 202515 min read

Authorization is the process of determining whether a user has the permission to access specific resources or perform certain actions within an application. It ensures that users can only perform actions they are allowed to based on their role or permission level.

In this guide, we'll learn how to implement robust authorization in a web application using Cerbos, a powerful authorization engine, and Echo, a fast and minimalist Go web framework for building our application's backend.

By the end of this guide, you'll know how to set up a secure RESTful API in Go and enforce access control policies using Cerbos.

Understanding the application structure

Before we delve into the implementation, it's crucial to understand the different components of our web application and its overall architecture.

Our application is essentially a simple RESTful API which enables you to perform the basic CRUD operations (create, read, update and delete) on blog posts. To demonstrate the authorization mechanism effectively, we have defined the following users and their corresponding roles:

Users Passwords Roles
kunal kunalPass admin
bella bellaPass user
john johnPass user

Additionally, each role is associated with a distinct set of permissions:

A detailed look at the architecture (file structure) of our application is shown below:

Prerequisites

Before we begin with the tutorial, ensure you have the following:

Step 1 - Initial project setup

In this step, we'll perform the following tasks:

Let's begin by creating a new directory for our project and initializing a new Go module ( go.mod):

$ mkdir blogapi-cerbos
$ cd blogapi-cerbos
$ go mod init github.com/USERNAME/blogapi-cerbos

Next, let's install the modules necessary for our project:

  1. Echo Web Framework
  2. Cerbos Go SDK
# echo web framework
$ go get github.com/labstack/echo/v4

# cerbos Go SDK
$ go get github.com/cerbos/cerbos-sdk-go

These commands will install the latest versions of both modules, which includes all the necessary packages we’ll need to first build our REST API and then implement authorization using Cerbos.

Step 2 - Building the RESTful API

Let us first start with building the core of our application, which is the REST API for blog posts. There are three main components we need to configure:

  1. Setting up the Database - For Users and Posts
  2. Defining the API routes and handlers using Echo
  3. Starting the Echo server

Setting up the database

Let's start by creating an in-memory database for both users and posts using Go structs. This approach simplifies things by storing data in RAM rather than a traditional database system.

For the blog posts database, create a file named db/posts.go and add the following code:

package db

import (
    "errors"
)

type Post struct {
    PostId uint64 `json:"postId"`
    Title  string `json:"title"`
    Owner  string `json:"owner"`
}

type PostDB struct {
    postCounter uint64
    posts       map[uint64]*Post
}

// initiatialise a new PostDB instance with an empty map of posts
func NewPostDB() *PostDB {
    return &PostDB{
        posts: make(map[uint64]*Post),
    }
}

// create a new post
func (pdb *PostDB) CreatePost(owner string, post Post) uint64 {

pdb.postCounter++
    pdb.posts[pdb.postCounter] = &Post{
        PostId: pdb.postCounter,
        Title:  post.Title,
        Owner:  owner,
    }

return pdb.postCounter
}

// update a post
func (pdb *PostDB) UpdatePost(postId uint64, post Post) error {

po, found := pdb.posts[postId]
    if !found {
        return errors.New("post not found")
    }

// update title
    po.Title = post.Title

return nil
}

// delete a post
func (pdb *PostDB) DeletePost(postId uint64) error {

_, found := pdb.posts[postId]
    if !found {
        return errors.New("post not found")
    }

// delete a post from the map, having the id
    delete(pdb.posts, postId)

return nil
}

// get a post by Id
func (pdb *PostDB) GetPost(postId uint64) (Post, error) {

po, found := pdb.posts[postId]
    if !found {
        return Post{}, errors.New("post not found")
    }

return *po, nil
}

Here's a breakdown of the core logic:

For the users database, create a file named db/users.go and add the following code:

package db

import (
    "context"
    "errors"
)

type UserRecord struct {
    Password []byte
    Roles    []string
    Blocked  bool
}

var users = map[string]*UserRecord{
    "kunal": {
        Password: []byte(`$2y$10$s3QvUpMDYhdxO8LbPyiDou7KSTup.Hj9ip5ntB2h0NkW1fHIbYMm6`),
        Roles:    []string{"admin"},
        Blocked:  false,
    },
    "bella": {
        Password: []byte(`$2y$10$0V3N6CPkEozFKWhRgYSXJeXUEra2G7IYWr5BCSGwBSILRpLsfpVUm`),
        Roles:    []string{"user"},
        Blocked:  false,
    },
    "john": {
        Password: []byte(`$2y$10$RW1ItHGul1VXGZFs03YLFuwIvBijMv86uHq2pSHCkgvnvPHx10gj6`),
        Roles:    []string{"user"},
        Blocked:  false,
    },
}

// retrieve user info from the database
func FindUser(ctx context.Context, username string) (*UserRecord, error) {

record, err := users[username]
    if !err {
        return nil, errors.New("record not found")
    }
    return record, nil
}

Here's a breakdown of the core logic:

Defining API routes and handlers using Echo

Let us now define the API routes and handlers using the Echo web framework. We'll set up routes for creating, viewing, updating, and deleting blog posts and implement the corresponding handler functions.

1. Initializing the Echo client and defining routes

First, let’s initialize the Echo client and define the routes for our API.

package service

import (
    "github.com/labstack/echo/v4"
)// Service implements the posts API.
type Service struct {
    posts  *db.PostDB
}

func (s *Service) Handler() *echo.Echo {
    // new echo instance
e := echo.New()

// API routes
e.PUT("/posts", s.handlePostCreate)
e.GET("/posts/:postId", s.handlePostView)
e.POST("/posts/:postId", s.handlePostUpdate)
e.DELETE("/posts/:postId", s.handlePostDelete)

return e
}

2. Implementing the handler functions

Next, we define the handler functions for each route. These functions will handle the HTTP requests and interact with our in-memory database.

func (s *Service) handlePostCreate(ctx echo.Context) error {
    var post db.Post
    if err := ctx.Bind(&post); err != nil {
        return ctx.String(http.StatusBadRequest, "Invalid post data")
    }

// Create the post
    username := getCurrentUser(ctx.Request().Context())
    postId := s.posts.CreatePost(username, post)

return ctx.JSON(http.StatusCreated, struct {
        PostId uint64 `json:"postId"`
    }{PostId: postId})
}
// view the post with id
func (s *Service) handlePostView(ctx echo.Context) error {
    // retrieve post info from request
    post, err := s.retrievePost(ctx)
    if err != nil {
        log.Printf("ERROR: %v", err)
        return ctx.String(http.StatusBadRequest, "Post not found")
    }

return ctx.JSON(http.StatusOK, post)
}
func (s *Service) handlePostUpdate(ctx echo.Context) error {
    postId := ctx.Param("postId")
    var post db.Post
    if err := ctx.Bind(&post); err != nil {
        return ctx.String(http.StatusBadRequest, "Invalid post data")
    }

err := s.posts.UpdatePost(postId, post)
    if err != nil {
        return ctx.String(http.StatusBadRequest, "Post not found")
    }

return ctx.String(http.StatusOK, "Post updated")
}
func (s *Service) handlePostDelete(ctx echo.Context) error {
    postId := ctx.Param("postId")

err := s.posts.DeletePost(postId)
    if err != nil {
        return ctx.String(http.StatusBadRequest, "Post not found")
    }

return ctx.String(http.StatusOK, "Post deleted")
}

3. Starting the Echo server

Finally, we'll set up the Echo server to handle incoming requests. Use the following code in the main.go file:

package main

import (
    "log"
    "net/http"
    "github.com/verma-kunal/blogapi-auth-cerbos/service"
)

func main() {
    // Create an instance of the Service struct
    svc := &service.Service{}

// Initialize the HTTP server
    srv := http.Server{
        Addr:    ":8080",          // Set the address and port for the server
        Handler: svc.Handler(),    // Set the Echo handler
    }

log.Printf("Listening on %s", ":8080")

// Start the server and listen for incoming requests
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}

With this setup, we've successfully implemented a RESTful API using Echo, capable of performing all basic CRUD operations.

Intermezzo - Introduction to Cerbos

In our previous section, we haven’t yet implemented any authorization mechanism in our application. This means that all the users in our database, regardless of their assigned roles, can perform any operation—which is not ideal to say the least!

We’ll be using Cerbos to implement this authorization mechanism, but before that let’s have a closer look into Cerbos and some of its key features.

What is Cerbos?

Cerbos is an open source authorization engine that enables developers to enforce access control policies within their applications. It works by evaluating access requests against predefined policies, typically defined in yaml format, to determine whether to allow or deny access to resources and actions within the application.

Cerbos supports three primary access control paradigms:

All of them offer developers a flexible and granular approach to defining authorization configurations.

If you're interested in a policy administration and management platform for externalized authorization - feel free to check out Cerbos Hub.

Feature highlights

Some of the key features offered by Cerbos are as follows:

Spotlight on access control policies

As mentioned previously. Cerbos relies on access control policies to govern resource access and actions. For our blog application, we'll focus on two types of policies:

Step 3 - Implementing authorization using Cerbos

In order to implement authorization using Cerbos in our Go application, here are the different components we need to configure:

  1. Defining Cerbos Policies
  2. Initializing the Cerbos PDP Client
  3. Implementing the Authorization Logic Using Echo

Defining policies

While defining Cerbos policies, we need to consider the following components:

If you're new to Cerbos or unsure how to write a policy, you can use Cerbos Playground to create and test policies in your browser.

For our project today, we are defining the following policies:

apiVersion: "api.cerbos.dev/v1"
derivedRoles:
    name: custom-roles
    definitions:
    - name: post-owner
      parentRoles: ["user"]
      condition:
        match:
          expr: request.resource.attr.owner == request.principal.id
apiVersion: api.cerbos.dev/v1
resourcePolicy:
    version: "default"
    importDerivedRoles:
    - custom-roles
resource: post
rules:
# Any user can create a new post
    - actions: ["CREATE"]
      roles:
        - user
        - admin
      effect: EFFECT_ALLOW

# A post can only be viewed by the user who created it or the admin.
    - actions: ["VIEW"]
      derivedRoles:
        - post-owner
      roles:
        - admin
      effect: EFFECT_ALLOW

# A post can only be updated/deleted by the user who created it or the admin.
    - actions: ["UPDATE", "DELETE"]
      derivedRoles:
        - post-owner
      roles:
        - admin
      effect: EFFECT_ALLOW

Here, we are defining the actual permissions for our post resource. This includes the following rules:

Initializing the Cerbos PDP client

Before moving on to implementing the core authorization logic, let us initialize the Cerbos PDP client.

  1. Add the Cerbos GRPC client to the Service type in service/service.go:
type Service struct {
       cerbos *cerbos.GRPCClient
       posts  *db.PostDB
}
  1. Create a function that utilizes Cerbos.New() to initialize a new Cerbos client in service/service.go:
       // new cerbos instance
func New(cerbosAddr string) (*Service, error) {
       cerbosInstance, err := cerbos.New(cerbosAddr, cerbos.WithPlaintext())
       if err != nil {
           return nil, err
       }

return &Service{cerbos: cerbosInstance, posts: db.NewPostDB()}, nil
}
  1. Initialize the service instance and provide the Cerbos server address in main.go:
cerbosAddr := flag.String("cerbos", "localhost:3593", "Address of the Cerbos server")
flag.Parse()

// start the service API
svc, err := service.New(*cerbosAddr)
if err != nil {
    log.Fatalf("Failed to create service: %v", err)
}

Implementing the authorization logic using Echo

In this section, we’ll implement the following steps to integrate Cerbos authorization in our Go application:

1. Creating a Cerbos post resource

Now that we have defined the policies against the post resource, we need to define the resource itself. Here’s the implementation to define the new Cerbos post resource:

// cerbos resource for the given post
func postResource(post db.Post) *cerbos.Resource {

return cerbos.NewResource("post", strconv.FormatUint(post.PostId, 10)).
        WithAttr("title", post.Title).
        WithAttr("owner", post.Owner)
}

Here, the postResource function converts a blog post into a Cerbos resource ( *cerbos.Resource) and attaches additional attributes like the post's title and owner.

2. Implementing the authorization middleware using Echo

The authorization middleware intercepts requests to authenticate users and attaches their authentication context to the request. Here’s how to implement it:

func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(ctx echo.Context) error {

// get basic auth creds from the request
        user, pass, ok := ctx.Request().BasicAuth()

if ok {
            // check the password and retrieve the auth context.
            authCtx, err := buildAuthContext(user, pass, ctx)
            if err != nil {
                log.Printf("Failed to authenticate user [%s]: %v", user, err)
            } else {
                // Add the retrieved principal to the context.
                newCtx := context.WithValue(ctx.Request().Context(), authCtxKey, authCtx)
                ctx.SetRequest(ctx.Request().WithContext(newCtx)) // setting the new request context

return next(ctx)
            }
        }

return next(ctx)
    }
}

Here’s a brief breakdown of the core logic:

3. Function for policy check - isAllowedByCerbos()

Next, we’ll create a function that checks if a user is authorized to perform a specific action on a Cerbos resource. This function will be used within our handlers to enforce authorization rules.

// Cerbos check function
func (s *Service) isAllowedByCerbos(ctx context.Context, resource *cerbos.Resource, action string) bool {
    // Get current Cerbos principal
    principalCtx := s.principalContext(ctx)
    if principalCtx == nil {
        return false
    }

// Using the IsAllowed() utility function from "cerbos Principal Context"
    allowed, err := principalCtx.IsAllowed(ctx, resource, action)
    if err != nil {
        return false
    }

return allowed
}

Here’s a brief breakdown of the core logic:

4. Enforcing policy check in our handlers

Now that we have the core authorization logic in place, we’ll integrate the policy checks into our existing Echo handler functions.

func (s *Service) handlePostCreate(ctx echo.Context) error
...
      // create a new cerbos resource
      cerbosResource := cerbos.NewResource("post", "new").
          WithAttr("title", post.Title).
          WithAttr("owner", post.Owner)

// cerbos auth
      if !s.isAllowedByCerbos(ctx.Request().Context(), cerbosResource, "CREATE") {
          return ctx.String(http.StatusForbidden, "Operation not allowed")
}
...
}
func (s *Service) handlePostView(ctx echo.Context) error {
...
// cerbos policy check
      if !s.isAllowedByCerbos(ctx.Request().Context(), postResource(post), "VIEW") {
          return ctx.String(http.StatusForbidden, "Operation not allowed")
      }
...
}
func (s *Service) handlePostUpdate(ctx echo.Context) error {
...
      // cerbos auth
      if !s.isAllowedByCerbos(ctx.Request().Context(), postResource(post), "UPDATE") {
          return ctx.String(http.StatusForbidden, "Operation not allowed")
      }
...
}
func (s *Service) handlePostDelete(ctx echo.Context) error {
...
      // cerbos auth
      if !s.isAllowedByCerbos(ctx.Request().Context(), postResource(post), "DELETE") {
          return ctx.String(http.StatusForbidden, "Operation not allowed")
      }
...
}

Let us quickly summarize what all we have accomplished in this section:

Step 4 - Performing CRUD operations with access control

Now that we have implemented the core authorization logic using Cerbos, we can test all our API endpoints with access control in place.

Make sure to start the Echo server before making the API requests. Use the following command to start the PDP and Echo server:

$ cerbos run --set=storage.disk.directory=cerbos/policies -- go run main.go

Here are a few examples to try out:

$ curl -i -u kunal:kunalPass -X PUT http://localhost:8080/posts -d '{"title": "gitops 101", "owner": "kunal"}'

# output
{"postId":1}

$ curl -i -u kunal:kunalPass -X GET http://localhost:8080/posts/1

# output
{"postId":1,"title":"gitops 101"","owner":"kunal"}
$ curl -i -u bella:bellaPass -X PUT http://localhost:8080/posts -d '{"title": "cebos-test", "owner": "bella"}'

# output
{"postId":2}

$ curl -i -u bella:bellaPass -X GET http://localhost:8080/posts/2

#output
{"postId":2,"title":"cebos-test","owner":"bella"}
$ curl -i -u bella:bellaPass -X GET http://localhost:8080/posts/1

# output
Operation not allowed

$ curl -i -u bella:bellaPass -X POST http://localhost:8080/posts/1 -d '{"title": "gitops 101", "owner": "kunal"}'

# output
Operation not allowed

$ $ curl -i -u bella:bellaPass -X DELETE http://localhost:8080/posts/1

# output
Operation not allowed
$ curl -i -u kunal:kunalPass -X GET http://localhost:8080/posts/2

# output
{"postId":2,"title":"cebos-test","owner":"bella"}

$ curl -i -u kunal:kunalPass -X POST http://localhost:8080/posts/2 -d '{"title": "edited-by-admin", "owner": "bella"}'

#output
Post updated

$ curl -i -u kunal:kunalPass -X DELETE http://localhost:8080/posts/2

# output
Post Deleted

Final thoughts

In this guide, we learned how to set up a robust authorization mechanism in a Go application using the Echo framework and Cerbos. By combining these technologies, we created an efficient role-based access control (RBAC) system in a RESTful API, which controls who can access and manage blog posts.

Cerbos makes fine-grained access controls easy to implement and manage, making authorization more secure and more adaptable to changing requirements, while saving months of developer time. If you’re interested in implementing authorization - try out Cerbos or book a call with a Cerbos engineer to see how our solution can help streamline access control in your applications.

Resources