DEV Community

Girish Talekar
Girish Talekar

Posted on • Updated on

HTTP Authorization in Go using Casbin, Redis and PostgreSQL

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 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
Enter fullscreen mode Exit fullscreen mode

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 == "*")
Enter fullscreen mode Exit fullscreen mode

Integrating together with golang code

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"))
}
Enter fullscreen mode Exit fullscreen mode

Github link

Tests:

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 == "*")
Enter fullscreen mode Exit fullscreen mode

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 :

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');
Enter fullscreen mode Exit fullscreen mode

Middleware and code :

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
}
Enter fullscreen mode Exit fullscreen mode

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

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"}
Enter fullscreen mode Exit fullscreen mode

Now if you add another user say

INSERT INTO casbin_rule(ptype, v0, v1) VALUES('g', 'ray', 'user');
Enter fullscreen mode Exit fullscreen mode

and do

curl -X GET http://ray:@0.0.0.0:3000/channel                                                                  ✔  10854  12:39:50
"channel get allowed"
Enter fullscreen mode Exit fullscreen mode

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

Top comments (1)

Collapse
 
mrdjeb profile image
MrDjeb

This is very helpful, thanks! :D