Casbin

According to the user’s use scenario, we provide Casbin Middleware that adapted to Hertz.

Casbin is a powerful and efficient open-source access control library that supports various access control models ( such as ACL/RBAC/ABAC) for enforcing authorization across the board.

According to the user’s use scenario, we provide Casbin Middleware that adapted to Hertz.

Install

go get github.com/hertz-contrib/casbin

Import

import "github.com/hertz-contrib/casbin"

Example

package main

import (
    "context"
    "log"

    "github.com/cloudwego/hertz/pkg/app"
    "github.com/cloudwego/hertz/pkg/app/server"
    "github.com/hertz-contrib/casbin"
    "github.com/hertz-contrib/sessions"
    "github.com/hertz-contrib/sessions/cookie"
)

func main() {
    h := server.Default()

    // using sessions to store user info.
    store := cookie.NewStore([]byte("secret"))
    h.Use(sessions.New("session", store))
    auth, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
    if err != nil {
        log.Fatal(err)
    }

    h.POST("/login", func(ctx context.Context, c *app.RequestContext) {
        // verify username and password.
        // ...

        // store username (casbin subject) in session
        session := sessions.Default(c)
        session.Set("name", "alice")
        err := session.Save()
        if err != nil {
            log.Fatal(err)
        }
        c.String(200, "you login successfully")
    })

    h.GET("/book", auth.RequiresPermissions("book:read", casbin.WithLogic(casbin.AND)), func(ctx context.Context, c *app.RequestContext) {
        c.String(200, "you read the book successfully")
    })

    h.POST("/book", auth.RequiresRoles("user", casbin.WithLogic(casbin.AND)), func(ctx context.Context, c *app.RequestContext) {
        c.String(200, "you posted a book successfully")
    })

    h.Spin()
}

// subjectFromSession get subject from session.
func subjectFromSession(ctx context.Context, c *app.RequestContext) string {
    // get subject from session.
    session := sessions.Default(c)
    if subject, ok := session.Get("name").(string); !ok {
        return ""
    } else {
        return subject
    }
}

Config

By Casbin Middleware, hertz is capable of controlling user access permissions.

use this extension, you need to initialize middleware and use method that provided by this middleware to authorize.

Initialize middleware

NewCasbinMiddleware

You need to provide Model, Policy and LookupHandler (handler that get subject) to initialize middleware.

This function will initialize *casbin.Enforcer by provided configs to perform authentication operation.

The function signature is as follows:

func NewCasbinMiddleware(modelFile string, adapter interface{}, lookup LookupHandler) (*Middleware, error)

Sample code:

func exampleLookupHandler(ctx context.Context, c *app.RequestContext) string {
    // get subject from session
    session := sessions.Default(c)
    if subject, ok := session.Get("name").(string); !ok {
        return ""
    } else {
        return subject
    }
}

func main() {
	...
    casbinMiddleware, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", exampleLookupHandler)
    if err != nil {
        log.Fatal(err)
    }
    ...
}

NewCasbinMiddlewareFromEnforcer

You need to provide enforcer and LookupHandler (handler that get subject) to initialize middleware.

The function signature is as follows:

func NewCasbinMiddlewareFromEnforcer(e casbin.IEnforcer, lookup LookupHandler) (*Middleware, error)

Sample code:

func exampleLookupHandler(ctx context.Context, c *app.RequestContext) string {
    // get subject from session
    session := sessions.Default(c)
    if subject, ok := session.Get("name").(string); !ok {
        return ""
    } else {
        return subject
    }
}

func main() {
	...
	enforcer, err := casbinsdk.NewEnforcer("example/config/model.conf", "example/config/policy.csv")
	if err != nil{
		log.Fatal(err)
	}

    casbinMiddleware, err := casbin.NewCasbinMiddlewareFromEnforcer(enforcer, exampleLookupHandler)
    if err != nil {
        log.Fatal(err)
    }
    ...
}

Middleware method

middleware provide methods that perform authentication operation.

method parameter format is as follows:

func (m *Middleware) exampleMiddlwareMethod(expression string, opts ...Option) app.HandlerFunc

it contains expression and opts two params.

parameters descriptions is as follows:

  • expression

    expression has one or more params, each param is divided by space, the expression format is related to Logic (see option description),

    the calculated final value of the expression is either True or False, True represents for has passed casbin middleware,

    False represents for has not passed casbin middleware.

    If Logic is AND or OR, the format is:

    "var 1 var2 var3 var4", like "book:read book:write"

    If Logic is CUSTOM, the format is :

    "var1 opr1 var2 opr2 var3" like "book:read && book:write || book:all"

  • opts

    OptionDescriptionDefault
    WithLogicLogic is the logical operation (AND/OR/CUSTOM) used in permission checks that multiple permissions or roles are specifiedAND
    WithPermissionParserPermissionParserFunc is used for parsing the permission to extract object and action usuallyPermissionParserWithSeparator(":")
    WithPermissionParserSeparatorPermissionParserSeparator is used to set separator that divide permission to object and action usually:
    WithUnauthorizedUnauthorized defined the response body for unauthorized responsesfunc(ctx context.Context, c *app.RequestContext) { c.AbortWithStatus(consts.StatusUnauthorized) }
    WithForbiddenForbidden defines the response body for forbidden responsesfunc(ctx context.Context, c *app.RequestContext) { c.AbortWithStatus(consts.StatusForbidden) }

RequiresPermission

Use Subject (provided by LookupHandler) and expression (see the following text) to check subject is whether satisfies the permission list relationship.

vars inside expression is param list behind sub in Model :

[request_definition]
r = sub, xxx, xxx

like:

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

when use default PermissionParser, expression format should be like "book:read".

like:

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

when use default PermissionParser, expression format should be like ""book1.com:book:read.

The function signature is as follows:

func (m *Middleware) RequiresPermissions(expression string, opts ...Option) app.HandlerFunc

Sample code:

when user has book:read permission,

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

    h.GET("/book",
		m.RequiresPermissions("book:read"), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

	h.GET("/book",
		m.RequiresPermissions("book:read book:write"), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

RequiresRoles

Use Subject (provided by LookupHandler) and expression (see the following text) to check roles to which the user belongs is whether satisfies the role list relationship.

The function signature is as follows:

func (m *Middleware) RequiresRoles(expression string, opts ...Option) app.HandlerFunc

Sample code:

when user has role of user and reader,

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

	h.POST("/book",
		auth.RequiresRoles("user"), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you posted a book successfully")
		},
	)

    h.POST("/book",
		auth.RequiresRoles("user reader"), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you posted a book successfully")
		},
	)

    h.POST("/book",
		auth.RequiresRoles("user reader admin"), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you posted a book successfully")
		},
	)
    ...
}

Attention: This method is only valid when use RBAC.

Options description

WithLogic

Logic is the logical operation (AND/OR/CUSTOM) used in permission checks that multiple permissions or roles are specified.

The function signature is as follows:

func WithLogic(logic Logic) Option

option:

const (
	AND Logic = iota
	OR
	CUSTOM
)

AND

all variables in expression will perform Logic AND operation.

Sample code:

when user has book:read permission,

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

    h.GET("/book",
		m.RequiresPermissions("book:read", casbin.WithLogic(casbin.AND)), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

	h.GET("/book",
		m.RequiresPermissions("book:read book:write", casbin.WithLogic(casbin.AND)), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

OR

all variables in expression will perform Logical OR operation.

Sample code:

when user has book:read permission,

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

    h.GET("/book",
		m.RequiresPermissions("book:read", casbin.WithLogic(casbin.OR)), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

	h.GET("/book",
		m.RequiresPermissions("book:read book:and", casbin.WithLogic(casbin.OR)), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

CUSTOM

expression will be parsed like C-like artithmetic/string expression。

Attention:

when using CUSTOM, use WithPermissionParser Option is forbidden, it is suggested using WithPermissionParserSeparator Option instead.

Sample code:

when user has book:read permission,

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

    h.GET("/book",
		m.RequiresPermissions("book:read", casbin.WithLogic(casbin.CUSTOM)), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

	h.GET("/book",
		m.RequiresPermissions("book:read && book:write", casbin.WithLogic(casbin.CUSTOM)), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

    h.GET("/book",
		m.RequiresPermissions("book:read || book:write", casbin.WithLogic(casbin.CUSTOM)), // passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)

    h.GET("/book",
		m.RequiresPermissions("!book:read", casbin.WithLogic(casbin.CUSTOM)), // not passed
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

WithPermissionParser

PermissionParserFunc is used for parsing the permission to extract object and action usually.

The function signature is as follows:

func WithPermissionParser(pp PermissionParserFunc) Option

Sample code:

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

	h.GET("/book",
		m.RequiresPermissions("book-read",
			casbin.WithPermissionParser(func(str string) []string {
				return strings.Split(str, "-")
			}),
		),
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

WithPermissionParserSeparator

PermissionParserSeparator is used to set separator that divide permission to object and action usually.

The function signature is as follows:

func WithPermissionParserSeparator(sep string) Option

Sample code:

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

	h.GET("/book",
		m.RequiresPermissions("book-read",
			casbin.WithPermissionParserSeparator("-"),
		),
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

WithUnauthorized

Unauthorized defined the response body for unauthorized responses.

The function signature is as follows:

func WithUnauthorized(u app.HandlerFunc) Option

Sample code:

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

	h.GET("/book",
          m.RequiresPermissions("book:read",
			casbin.WithUnauthorized(func(c context.Context, ctx *app.RequestContext) {
				ctx.AbortWithStatus(consts.StatusUnauthorized)
			}),
		),
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

WithForbidden

Forbidden defines the response body for forbidden responses.

The function signature is as follows:

func WithForbidden(f app.HandlerFunc) Option

Sample code:

func main(){
    ...
    h := server.Default()

    m, err := casbin.NewCasbinMiddleware("example/config/model.conf", "example/config/policy.csv", subjectFromSession)
	if err != nil {
		log.Fatal(err)
	}

	h.GET("/book",
          m.RequiresPermissions("book:read",
			casbin.WithForbidden(func(c context.Context, ctx *app.RequestContext) {
				ctx.AbortWithStatus(consts.StatusForbidden)
			}),
		),
		func(ctx context.Context, c *app.RequestContext) {
			c.String(200, "you read the book successfully")
		},
	)
    ...
}

Last modified June 24, 2024 : fix: correct the file path (#1096) (b3fa1a4)