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
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
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"))
}
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"}
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 :
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 :
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
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
INSERT INTO casbin_rule(ptype, v0, v1) VALUES('g', 'ray', 'user');
and do
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”.
Top comments (1)
This is very helpful, thanks! :D