HTTP Authorization in Go using Casbin, Redis and PostgreSQL
Published: 2023-06-24 04:10:12

## First scenario :

To understand how casbin works, i started with very simple authorization requirement

### Requirements :

1) I have 2 api’s ‘/project’ and ‘/channel’ 
2) I want only authorized user should be able to access this api’s

### Approach :

Rather then implementing our own authorization model we decided of using an existing/opensource package, we came across [Casbin](https://casbin.org) which is flexible and has many feature’s inbuilt.

Basically in casbin there are rules which you need to first configure in **.conf** file and specify each rule in **.csv** file

The conf file is based on **PERM** metamodel (Policy, Effect, Request, Matchers)

**Request:**

For instance, a request definition may look like this: `r={sub,obj,act}`

It actually defines the parameter name and order which we should provide for access control matching function.

**Policy:**

Define the model of the access strategy. It defines the name and order of the fields in the Policy rule document.

**Matchers:**

Matching rules of Request and Policy.

For example: `m = r.sub == p.sub && r.act == p.act && r.obj == p.obj`

**Effect:**

It can be understood as a model in which a logical combination judgment is performed again on the matching results of Matchers.

Example: `e = some(where(p.eft == allow))`

Create a **policy.csv** as below

```
p, alice, /project, *
p, alice, /channel, *
p, bob, /channel, GET
p, bob, /project, GET
```

1) I want alice should have full access to all api’s 
2) I want bob should only be able to read

The conf file for above requirement looks like this

**model.conf**

```
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
```

Integrating together with golang code

```go
package main

import (
	"net/http"

	"github.com/casbin/casbin"
	"github.com/labstack/echo"
)

type Enforcer struct {
	enforcer *casbin.Enforcer
}

func (e *Enforcer) Enforce(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		user, _, _ := c.Request().BasicAuth()
		method := c.Request().Method
		path := c.Request().URL.Path

		result, _ := e.enforcer.EnforceSafe(user, path, method)

		if result {
			return next(c)
		}
		return echo.ErrForbidden
	}
}

func main() {
	e := echo.New()
	enforcer := Enforcer{enforcer: casbin.NewEnforcer("model.conf", "policy.csv")}
	e.Use(enforcer.Enforce)
	e.GET("/project", func(c echo.Context) error {
		return c.JSON(http.StatusOK, "project get allowed")
	})
	e.POST("/project", func(c echo.Context) error {
		return c.JSON(http.StatusOK, "project post allowed")
	})

	e.GET("/channel", func(c echo.Context) error {
		return c.JSON(http.StatusOK, "channel get allowed")
	})

	e.POST("/channel", func(c echo.Context) error {
		return c.JSON(http.StatusOK, "channel post allowed")
	})
	e.Logger.Fatal(e.Start("0.0.0.0:3000"))
}
```

[Github link](https://gist.github.com/girishg4t/ec23712fe260e2dd819d5be892b53081)

**Tests:**

```bash
curl -X POST http://bob:@0.0.0.0:3000/channel                                                                 
{"message":"Forbidden"}
curl http://bob:@0.0.0.0:3000/channel                                                                         
"ok channel get"
curl -X POST http://alice:@0.0.0.0:3000/channel                                                               
"ok channel post"
curl -X GET http://alice:@0.0.0.0:3000/channel                                                                
"ok channel get"
curl -X POST http://bob:@0.0.0.0:3000/project                                                                 
{"message":"Forbidden"}
```


## More practical scenario :

After getting some understanding, i want to implement it in my project

**Requirements:**

1) I want the policy/rules to be added dynamically though api’s and we want it get applied as soon as it get added
2) I don’t want to fetch all the rules from db for every request, so we want to use some caching layer
3) I want to group the rules as per roles, so that i don’t have to add for every verbs(GET, POST etc), i just want to give a role to a user and whatever rule it is associated with should get applied, some thing like this

```
p, admin, /project, *
p, admin, /channel, *
p, user, /channel, GET
p, user, /project, GET
g, alice, admin
g, bob, user
```

The conf file for above requirement will look like this

```
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = (r.sub == p.sub || g(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
```

### Approach:

1) Add middleware which will take care of authorization
2) Check the rule in redis if user has the access (to reduce load on db)
3) If cache miss happens then load the policy’s from db and store the rule for that user in redis for subsequent requests.

**Note**: on every update of the rule though api, we need to delete the cache for that user, this is not covered in this example

Storing the Policy’s in the DB :

```sql
CREATE TABLE casbin_rule (
    id SERIAL PRIMARY KEY,
    ptype VARCHAR(100),
    v0 VARCHAR(100),
    v1 VARCHAR(100),
    v2 VARCHAR(100),
    v3 VARCHAR(100),
    v4 VARCHAR(100),
    v5 VARCHAR(100)
);

INSERT INTO casbin_rule(ptype, v0, v1, v2) VALUES('p', 'admin', '/project', '*');
INSERT INTO casbin_rule(ptype, v0, v1, v2) VALUES('p', 'admin', '/channel', '*');
INSERT INTO casbin_rule(ptype, v0, v1, v2) VALUES('p', 'user', '/project', 'GET');
INSERT INTO casbin_rule(ptype, v0, v1, v2) VALUES('p', 'user', '/channel', 'GET');

INSERT INTO casbin_rule(ptype, v0, v1) VALUES('g', 'alice', 'admin');
INSERT INTO casbin_rule(ptype, v0, v1) VALUES('g', 'bob', 'user');
```

Middleware and code : 

```go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"github.com/labstack/echo/v4"
)

func Authenticate(adapter *gormadapter.Adapter) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(e echo.Context) (err error) {

			ctx := e.Request().Context()

			user, _, _ := e.Request().BasicAuth()
			method := e.Request().Method
			path := e.Request().URL.Path

			key := fmt.Sprintf("%s-%s-%s", user, path, method)

			result := RedisCache.Get(ctx, key)
			val, err := result.Result()
			if err == nil {
				boolValue, err := strconv.ParseBool(val)
				if err != nil {
					log.Fatal(err)
				}

				if !boolValue {
					return &echo.HTTPError{
						Code:    http.StatusForbidden,
						Message: "not allowed",
					}
				}
				return next(e)
			}

			// Casbin enforces policy
			ok, err := enforce(ctx, user, path, method, adapter)
			if err != nil || !ok {

				return &echo.HTTPError{
					Code:    http.StatusForbidden,
					Message: "not allowed",
				}
			}
			if !ok {
				return err
			}
			return next(e)
		}
	}
}

func enforce(ctx context.Context, sub string, obj string, act string, adapter *gormadapter.Adapter) (bool, error) {
	// Load model configuration file and policy store adapter
	enforcer, err := casbin.NewEnforcer("./examples/group_model.conf", adapter)
	if err != nil {
		return false, fmt.Errorf("failed to load policy from DB: %w", err)
	}
	// Load policies from DB dynamically
	err = enforcer.LoadPolicy()
	if err != nil {
		return false, fmt.Errorf("error in policy: %w", err)
	}
	// Verify
	ok, err := enforcer.Enforce(sub, obj, act)
	if err != nil {
		return false, fmt.Errorf("error in policy: %w", err)
	}
	key := fmt.Sprintf("%s-%s-%s", sub, obj, act)
	RedisCache.Set(ctx, key, strconv.FormatBool(ok), time.Hour)
	return ok, nil
}
```

Done, that’s all you need to do, below are the tests with result

```bash
curl -X POST http://hari:@0.0.0.0:3000/project                                                                  ✔  10849  12:37:10
{"message":"Forbidden"}
curl -X POST http://alice:@0.0.0.0:3000/project                                                                 ✔  10851  12:39:21
"project post allowed"
curl -X POST http://bob:@0.0.0.0:3000/project                                                                   ✔  10852  12:39:29
{"message":"Forbidden"}
curl -X GET http://bob:@0.0.0.0:3000/project                                                                    ✔  10853  12:39:42
"project get allowed"
curl -X GET http://alice:@0.0.0.0:3000/project                                                                  ✔  10854  12:39:50
"project get allowed"
curl -X POST http://bob:@0.0.0.0:3000/channel                                                                   ✔  10855  12:40:11
{"message":"Forbidden"}
curl -X POST http://gt:@0.0.0.0:3000/channel                                                                    ✔  10856  12:40:28
{"message":"Forbidden"}
```

Now if you add another user say

```sql
INSERT INTO casbin_rule(ptype, v0, v1) VALUES('g', 'ray', 'user');
```

and do

```bash
curl -X GET http://ray:@0.0.0.0:3000/channel                                                                  ✔  10854  12:39:50
"channel get allowed"
```

it will work as expected, no need to restart the server

Boom! all things are working as expected happy coding, let me know you suggestion/feedback so that i can improve “learning should never stop”.

[Source code](https://github.com/girishg4t/go-casbin)