# Implementing Authorization Using Cerbos In Go and Echo Server
Imagine this: You're using an online banking app. As the account owner, you expect that only you can see the account balance, transfer money, and view transaction history. Now, imagine if anyone could access your account. Not good, right?
That's where authorization comes into play. 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](https://docs.cerbos.dev/), a powerful authorization engine, and [Echo](https://echo.labstack.com/docs), 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](https://github.com/verma-kunal/blogapi-auth-cerbos) 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:
- `admin` role:
- Can create, read, update, and delete their own posts
- Can perform all operations on other users' posts
- `user` role:
- Can create, read, update, and delete their own posts
- Cannot perform any operations on other users' posts
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:
- [Go](https://go.dev/doc/install) installed - Make sure to have Go (version 1.16 or later) installed on your system.
- [Cerbos CLI](https://docs.cerbos.dev/cerbos/latest/cli/cerbos) installed - This is needed to interact with the Cerbos server (PDP).
## Step 1 - Initial Project Setup
In this step, we'll perform the following tasks:
- Set up the Go project environment.
- Install the required modules.
Let's begin by creating a new directory for our project and initializing a new Go module (**`go.mod`**):
```bash
$ 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](https://echo.labstack.com/docs/quick-start)
2. [Cerbos Go SDK](https://github.com/cerbos/cerbos-sdk-go)
```bash
# 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:
```go
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:
- **Structs for Data Models -** Defines the structure of a blog post and manages the collection of posts.
- **In-Memory Storage - `PostDB`** uses a map to store posts in memory, allowing quick access and manipulation of post data.
- **CRUD Operations -** Methods like **`CreatePost`**, **`UpdatePost`**, **`DeletePost`**, and **`GetPost`** enable basic CRUD operations on posts.
For the users database, create a file named **`db/users.go`** and add the following code:
```go
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:
- **`UserRecord` Struct -** Defines the structure for user records, including fields for password (stored as a byte slice), roles (a list of roles assigned to the user), and a blocked status.
- **In-Memory User Data -** Uses a map (**`users`**) to store user records in memory, with predefined users and their corresponding attributes.
- **`FindUser()` -** Retrieves a user record by `username` from the in-memory map.
### 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.
```go
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.
- **Create a new Post**
```go
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 a Post**
```go
// 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)
}
```
- **Update a Post**
```go
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")
}
```
- **Delete a Post**
```go
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")
}
```
> **Note:** The functions [`getCurrentUser()`](https://github.com/verma-kunal/blogapi-auth-cerbos/blob/main/service/utils.go#L35) and [`retrievePost()`](https://github.com/verma-kunal/blogapi-auth-cerbos/blob/main/service/utils.go#L13) are utility functions used in the above section for retrieving user information and post details from the current request, respectively.
#### 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:
```go
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 🎉
## Introduction to Cerbos
In our previous section, we haven’t yet implemented any authorization mechanism in our application. That means, all the users in our database, regardless of their assigned roles, can perform any operation — which is not our ideal case that we want!
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](https://github.com/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](https://www.cerbos.dev/features-benefits-and-use-cases/pbac), typically defined in **`yaml`** format, to determine whether to allow or deny access to resources and actions within the application.
Cerbos supports two primary access control paradigms:
- [Attribute-based access control (ABAC)](https://www.cerbos.dev/features-benefits-and-use-cases/abac) and,
- [Role-based access control (RBAC)](https://www.cerbos.dev/features-benefits-and-use-cases/rbac),
offering developers a flexible and granular approach to defining authorization configurations.
### Feature Highlights
Some of the key features offered by Cerbos are as follows:
- **Flexible Policy Definition**: Cerbos allows you to define access control policies using a simple and intuitive policy language. These policies can specify who can access what resources and under what conditions, providing fine-grained control over access permissions.
- **Dynamic Evaluation**: Cerbos dynamically evaluates access requests at runtime, taking into account contextual information such as user attributes, resource attributes, and environmental factors. This enables adaptive access control based on the current state of the application and its users.
- **Policy Versioning and Rollback**: It supports versioning of policies which allows you to manage changes to access control rules over time. You can roll back to previous policy versions if needed, ensuring consistency and auditability of access control decisions.
- **Centralized Policy Management**: With Cerbos, you can manage access control policies centrally, making it easier to maintain and update authorization rules across multiple applications and services. This centralized approach streamlines policy management and ensures consistent enforcement of access control across your ecosystem.
### 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:
- [**Resource Policies**](https://docs.cerbos.dev/cerbos/latest/policies/resource_policies.html): These define access control rules for specific resources, such as blog posts. These policies specify which users or roles are allowed to perform actions (e.g., create, read, update, delete) on the resource.
- [**Derived Roles**](https://docs.cerbos.dev/cerbos/latest/policies/derived_roles.html): These are dynamically generated roles based on user attributes or other contextual information. These roles can be used to grant or restrict access to resources based on some dynamic criteria, enhancing the flexibility and adaptability of access control 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 Cerbos Policies
While defining Cerbos policies, we need to consider the following components:
- **Role** - Assigned to different users of our application, based on which their permissions will be assigned. In our case, we have two roles defined - `admin` and `user`.
- **Resource** - The actual entity in our application that we want to protect. In our case, we have the blog post on which different operations are being performed.
- **Permission** - This defines what kind of action users can perform on which resources, based on a specific role assigned to that user.
If you're new to Cerbos or unsure how to write a policy, you can use [Cerbos Playground](https://www.notion.so/Rough-TOC-2adefe672b9142689fea93a50d92b470?pvs=21) to create and test policies online, or Cerbos [RBAC Policy Generator](https://www.notion.so/Rough-TOC-2adefe672b9142689fea93a50d92b470?pvs=21) to generate policies by providing simple details.
For our project today, we are defining the following policies:
- **Derived Role:**
```yaml
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
```
- **Resource Policy:**
```yaml
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:
- **Create Posts**:
- Allowed for users with the `user` or `admin` roles.
- **View Posts**:
- Allowed for users with the `post-owner` derived role (i.e., the creator of the post).
- Also allowed for users with the `admin` role.
- **Update/Delete Posts**:
- Allowed for users with the `post-owner` derived role.
- Also allowed for users with the `admin` role.
### 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`**:
```go
type Service struct {
cerbos *cerbos.GRPCClient
posts *db.PostDB
}
```
2. Create a function that utilizes `Cerbos.New()` to initialize a new Cerbos client in **`service/service.go`**:
```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
}
```
3. Initialize the service instance and provide the Cerbos server address in `main.go`:
```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:
- Creating a Cerbos **`post`** resource
- Implementing the Authorization Middleware using Echo
- Define a Function For Cerbos Policy Check
- Enforcing Policy Check in our Handlers
#### 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:
```go
// 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](https://echo.labstack.com/docs/category/middleware) intercepts requests to authenticate users and attaches their authentication context to the request. Here’s how to implement it:
```go
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:
- **Get Basic Auth Credentials**: Extracts the username and password from the request's basic authentication header.
- **Authenticate User**: Calls [`buildAuthContext()`](https://github.com/verma-kunal/blogapi-auth-cerbos/blob/main/service/service.go#L87) to validate the credentials and retrieve the authentication context.
- **Attach Auth Context**: Adds the authentication context to the current request context.
- **Proceed with Request**: Passes control to the next handler if authentication is successful.
#### 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.
```go
// 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:
- **Get Cerbos Principal**: Retrieves the current Cerbos principal context from the request context using the [`principalContext()`](https://github.com/verma-kunal/blogapi-auth-cerbos/blob/main/service/service.go#L127) function.
- **Check Permissions**: Uses the **`IsAllowed`** method to check if the action on the resource is permitted for the principal.
- **Return Result**: Returns **`true`** if the action is allowed, otherwise returns **`false`**.
#### 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.
- **Create a Post**
```go
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")
}
...
}
```
- **View a Post**
```go
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")
}
...
}
```
- **Update a Post**
```go
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")
}
...
}
```
- **Delete a Post**
```go
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:
- **Creating a Cerbos Resource**: Converted a blog post into a Cerbos resource with additional attributes like **`title`** and **`owner`**.
- **Authorization Middleware**: Built a middleware to authenticate users and set up their context for authorization.
- **Cerbos Policy Check Function**: Created a function to check if actions are allowed based on Cerbos policies. The action defined are: `CREATE`, `VIEW`, `UPDATE`, `DELETE`.
- **Enforcing Policy Checks in Handlers**: Integrated the policy checks into our request handlers to ensure that only authorized users can perform specific actions on blog posts.
## 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 Echo server:
```bash
$ cerbos run --set=storage.disk.directory=cerbos/policies -- go run main.go
```
Here are a few examples to try out:
- **kunal** can create, view a new post (having `admin` role):
```bash
$ 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"}
```
- **bella** can create, view a new post (having `user` role):
```bash
$ 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"}
```
- **bella** cannot view, edit, delete **kunal’s** post:
```bash
$ 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
```
- **kunal** can view, edit, delete **bella’s** post:
```bash
$ 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.
## Resources
- Source Code: https://github.com/verma-kunal/blogapi-auth-cerbos
- Go Client SDK: https://github.com/cerbos/cerbos-sdk-go
- Cerbos Documentation: https://docs.cerbos.dev