Team author: Jock

What is Casbin

Casbin is a powerful and efficient open source access control framework. Its permission management mechanism supports multiple access control models. Casbin is only responsible for access control.

Its functions are:

  • Supports custom request format. The default request format is{subject, object, action}.
  • It has two core concepts: access control model and policy.
  • Multiple layers of role inheritance are supported in RBAC, where not only principals can have roles, but also resources.
  • Support for built-in superusers such as:rootoradministrator. A superuser can perform any operation without explicit permission declarations.
  • Supports a variety of built-in operators, such askeyMatchTo facilitate the management of path-based resources, for example/foo/barCan be mapped to/foo*

How Casbin works

In Casbin, the access control model is abstracted as a file based on **PERM **(Policy, Effect, Request, Matcher) [Policy, Effect, Request, Matcher].

  • Policy: defines permission rules
  • Effect: Defines the result of combining multiple policies
  • Request: indicates an access Request
  • Matcher: checks whether the Request meets the Policy

Matcher is used to determine whether the Request and Policy match. Effect is used to determine whether the match result is Allow or Deny.

The core concept of Casbin

Model

Model is the concrete access Model of Casbin, which is mainly in the form of a file, usually with a.conf suffix.

  • The Model CONF should contain at least four parts:[request_definition].[policy_definition].[policy_effect].[matchers].
  • If the Model uses RBAC, you need to add it[role_definition]Part.
  • The Model CONF file can contain comments. Comments begin with #, which comments the rest of the line.

Such as:

# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

# Role definition
[role_definition]
g = _, _

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

# matcher definition
[matchers]
m = g(r.sub, p.sub) && r.obj = = p.obj && r.act = = p.act
Copy the code
  • Request_definition: The definition used for request, which specifies e.enforce (…) The definition of parameters in a function,sub, obj, actRepresents classical triples: access entities (Subject), access resources (Object), and access methods (Action).
  • Policy_definition: used to define a policy. Each rule is usually defined as followspthepolicy typeAt the beginning, likep,joker,data1,readIs a rule that the joker has datA1 read permission.
  • Role_definition: defines the RBAC role inheritance relationship.gIs an RBAC system,_, _Indicates the preceding and subsequent items of the role inheritance relationship, that is, the permission of the former item to inherit the latter role.
  • Policy_effect: defines the scope of a policy that makes a unified decision on the outcome of a request, for examplee = some(where (p.eft == allow))It means that if there exists any decision result ofallow, then the final decision result isallow.p.eftRepresents the decision result of the policy rule, which can beallowordenyWhen the decision result of the rule is not specified, the default value is usedallow 。
  • Matchers: Defines policy matchers. A matcher is a set of expressions that define how to match policy rules against requests

Policy

A Policy specifies the mapping between roles, resources, and behaviors.

Such as:

p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin
Copy the code

Its relational rules are very simple, mainly to choose which way to store rules, currently officially provides CSV file storage and load configuration files from other storage systems through adapter adapter. For example, MySQL, PostgreSQL, SQL Server, SQLite3, directing, Redis, Cassandra DB, etc.

practice

Create a project

Start by creating a project called casbin_test.

The directory structure in the project is as follows:

├ ─ configs         # config file
├ ─ global				  # global variables
├ ─ internal        # Internal module
  ├ ─ dao					# Data processing module
  ├ ─ middleware   # middleware
  ├ ─ model        # model layer
  ├ ─ the router       # routing
    └ ─ API
        └ ─ v1    # view
  └ ─ service      # Business logic layer
└ ─ PKG             # Internal module package
    ├ ─ app         # of packages
    ├ ─ errcode     Error code package
    └ ─ setting     # configuration package
Copy the code

Download the dependency packages as follows:

go get -u github.com/gin-gonic/gin
# Go casbin dependencies
go get github.com/casbin/casbin
# GORm adapter dependency package
go get github.com/casbin/gorm-adapter
Mysql driver dependencies
go get github.com/go-sql-driver/mysql
# gorm package
go get github.com/jinzhu/gorm
Copy the code

Create a database as follows:

CREATE DATABASE `casbin_test` DEFAULT CHARACTER SET utf8;
GRANT Alter.Alter Routine, Create.Create Routine, Create Temporary Tables, Create View.Delete.Drop, Event, Execute, Index, Insert, Lock Tables, References.Select.Show View.Trigger, Update ON `casbin\_test`.* TO `ops`@`%`;
FLUSH PRIVILEGES;
DROP TABLE IF EXIST `casbin_rule`;
CREATE TABLE `casbin_rule` (
  `p_type` varchar(100) DEFAULT NULL COMMENT 'Rule type',
  `v0` varchar(100) DEFAULT NULL COMMENT 'character ID',
  `v1` varchar(100) DEFAULT NULL COMMENT 'API path',
  `v2` varchar(100) DEFAULT NULL COMMENT 'API access method',
  `v3` varchar(100) DEFAULT NULL,
  `v4` varchar(100) DEFAULT NULL,
  `v5` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Permission Rule List';
/* Insert permission rules for operation casbin API */
INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p'.'admin'.'/api/v1/casbin'.'POST');
INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p'.'admin'.'/api/v1/casbin/list'.'GET');
Copy the code

Code development

Due to the large number of codes, I will not post all the codes here. All the codes have been put in the Gitee warehouse and can be read by myself. These only post part of the key codes.

(1) Create the rbac_model.conf file in the configs directory.

[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 && ParamsMatch(r.obj,p.obj) && r.act ==  p.actCopy the code

(2) Create casbin.go file in internal/model directory and write the following code:

type CasbinModel struct {
	PType  string 'JSON :"p_type" GORm :"column:p_type" Description :" Policy type"'
	RoleId string 'JSON :"role_id" GORm :"column:v0" Description :" Role ID"'
	Path   string 'JSON :"path" GORm :"column:v1" Description :" API path"'
	Method string 'JSON :"method" GORm :"column:v2" Description :" Access method"'
}

func (c *CasbinModel) TableName(a) string {
	return "casbin_rule"
}

func (c *CasbinModel) Create(db *gorm.DB) error {
	e := Casbin()
	if success := e.AddPolicy(c.RoleId,c.Path,c.Method); success == false {
		return errors.New("Same API exists, add failed")}return nil
}

func (c *CasbinModel) Update(db *gorm.DB, values interface{}) error {
	if err := db.Model(c).Where("v1 = ? AND v2 = ?", c.Path, c.Method).Update(values).Error; err ! =nil {
		return err
	}
	return nil
}

func (c *CasbinModel) List(db *gorm.DB)[] []string {
	e := Casbin()
	policy := e.GetFilteredPolicy(0, c.RoleId)
	return policy
}

//@function: Casbin
//@description: Persisting to the database introduces custom rules
//@return: *casbin.Enforcer
func Casbin(a) *casbin.Enforcer {
	s := fmt.Sprintf("%s:%s@tcp(%s)/%s? charset=%s&parseTime=%t&loc=Local",
		global.DatabaseSetting.Username,
		global.DatabaseSetting.Password,
		global.DatabaseSetting.Host,
		global.DatabaseSetting.DBName,
		global.DatabaseSetting.Charset,
		global.DatabaseSetting.ParseTime,
	)
	db, _ := gorm.Open(global.DatabaseSetting.DBType, s)

	adapter := gormadapter.NewAdapterByDB(db)
	enforcer := casbin.NewEnforcer(global.CasbinSetting.ModelPath, adapter)
	enforcer.AddFunction("ParamsMatch", ParamsMatchFunc)
	_ = enforcer.LoadPolicy()
	return enforcer
}

//@function: ParamsMatch
//@description: custom rule function
//@param: fullNameKey1 string, key2 string
//@return: bool
func ParamsMatch(fullNameKey1 string, key2 string) bool {
	key1 := strings.Split(fullNameKey1, "?") [0]
	// Use casbin's keyMatch2 after stripping the path
	return util.KeyMatch2(key1, key2)
}

//@function: ParamsMatchFunc
//@description: custom rule function
//@param: args ... interface{}
//@return: interface{}, error
func ParamsMatchFunc(args ...interface{}) (interface{}, error) {
	name1 := args[0]. (string)
	name2 := args[1]. (string)

	return ParamsMatch(name1, name2), nil
}
Copy the code

(3) Create casbin.go in internal/ DAO directory and write the following code:

func (d *Dao) CasbinCreate(roleId string, path, method string) error {
	cm := model.CasbinModel{
		PType:  "p",
		RoleId: roleId,
		Path:   path,
		Method: method,
	}
	return cm.Create(d.engine)
}

func (d *Dao) CasbinList(roleID string)[] []string {
	cm := model.CasbinModel{RoleId: roleID}
	return cm.List(d.engine)
}
Copy the code

Create service.go in internal/service directory.

type CasbinInfo struct {
	Path   string `json:"path" form:"path"`
	Method string `json:"method" form:"method"`
}
type CasbinCreateRequest struct {
	RoleId      string       'json:"role_id" form:"role_id" Description :" Role ID"'
	CasbinInfos []CasbinInfo 'JSON :"casbin_infos" description:" Permission model list "'
}

type CasbinListResponse struct {
	List []CasbinInfo `json:"list" form:"list"`
}

type CasbinListRequest struct {
	RoleID string `json:"role_id" form:"role_id"`
}

func (s Service) CasbinCreate(param *CasbinCreateRequest) error {
	for _, v := range param.CasbinInfos {
		err := s.dao.CasbinCreate(param.RoleId, v.Path, v.Method)
		iferr ! =nil {
			return err
		}
	}
	return nil
}


func (s Service) CasbinList(param *CasbinListRequest)[] []string {
	return s.dao.CasbinList(param.RoleID)
}
Copy the code

Create casbin.go from internal/router/ API /v1

type Casbin struct{}func NewCasbin(a) Casbin {
	return Casbin{}
}

// Create godoc
// @summary new permission
// @description new permission
// @tags Permission management
// @Produce json
// @Security ApiKeyAuth
// @Param body body service.CasbinCreateRequest true "body"
// @success 200 {object} string "Success"
// @failure 400 {object} errcode.Error
// @failure 500 {object} errcode.Error "Internal Error"
// @Router /api/v1/casbin [post]
func (c Casbin) Create(ctx *gin.Context) {
	param := service.CasbinCreateRequest{}
	response := app.NewResponse(ctx)
	valid, errors := app.BindAndValid(ctx, &param)
	if! valid { log.Printf("app.BindAndValid errs: %v", errors)
		errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
		response.ToErrorResponse(errRsp)
		return
	}

	// Insert
	svc := service.NewService(ctx)
	err := svc.CasbinCreate(&param)
	iferr ! =nil {
		log.Printf("svc.CasbinCreate err: %v", err)
		response.ToErrorResponse(errcode.ErrorCasbinCreateFail)
	}
	response.ToResponse(gin.H{})
	return
}


// List godoc
// @summary get permission list
// @Produce json
// @tags Permission management
// @Security ApiKeyAuth
/ / @ Param data body service. CasbinListRequest true character ID ""
/ / @ 200 {object} service Success. CasbinListResponse "Success"
// @failure 400 {object} errcode.Error
// @failure 500 {object} errcode.Error "Internal Error"
// @Router /api/v1/casbin/list [post]
func (c Casbin) List(ctx *gin.Context) {
	param := service.CasbinListRequest{}
	response := app.NewResponse(ctx)
	valid, errors := app.BindAndValid(ctx, &param)
	if! valid { log.Printf("app.BindAndValid errs: %v", errors)
		errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
		response.ToErrorResponse(errRsp)
		return
	}
	// Business logic processing
	svc := service.NewService(ctx)
	casbins := svc.CasbinList(&param)
	var respList []service.CasbinInfo
	for _, host := range casbins {
		respList = append(respList, service.CasbinInfo{
			Path:   host[1],
			Method: host[2],
		})
	}
	response.ToResponseList(respList, 0)
	return
}
Copy the code

Create a test.go file in this directory for testing.

type Test struct{}func NewTest(a) Test {
	return Test{}
}

func (t Test) Get(ctx *gin.Context) {
	log.Println("Hello received a GET request..")
	response := app.NewResponse(ctx)
	response.ToResponse("Received GET request successful")}Copy the code

(6) Create casbin_handler.go in the internal/middleware directory and write:

func CasbinHandler(a) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		response := app.NewResponse(ctx)
		// Get the request URI
		obj := ctx.Request.URL.RequestURI()
		// Get the request method
		act := ctx.Request.Method
		// Get the user's role
		sub := "admin"
		e := model.Casbin()
		fmt.Println(obj, act, sub)
		// Check whether the policy exists
		success := e.Enforce(sub, obj, act)
		if success {
			log.Println("Congratulations, your permission has been verified.")
			ctx.Next()
		} else {
			log.Printf("e.Enforce err: %s"."Unfortunately, the permission verification failed.")
			response.ToErrorResponse(errcode.UnauthorizedAuthFail)
			ctx.Abort()
			return}}}Copy the code

Create router. Go from internal/router directory to router.

func NewRouter(a) *gin.Engine {
	r := gin.New()
	r.Use(gin.Logger())
	r.Use(gin.Recovery())
	casbin := v1.NewCasbin()
	test := v1.NewTest()
	apiv1 := r.Group("/api/v1")
	apiv1.Use(middleware.CasbinHandler())
	{
		// Test the route
		apiv1.GET("/hello", test.Get)

		// Permission policy management
		apiv1.POST("/casbin", casbin.Create)
		apiv1.POST("/casbin/list", casbin.List)
	}
	return r
}
Copy the code

Finally, launch the project for testing.

validation

(1) First access the test path, the current situation is not in the permission table, as follows:

(2) Add the test path to the permission list as follows:(3) Then access the test path again as follows:It can also be seen from the log as follows:

Reference Documents:

[1] casbin.org/ [2] casbin.org/docs/zh-CN/… [3] gitee.com/coolops/cas…