# 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: ![application architecture](https://hackmd.io/_uploads/BJBU2XwmR.png) ## 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