[TOC]
  • casbin实现基于角色的HTTP权限控制casbin介绍Casbin做了什么Casbin不做的事情配置示例模型与策略定制具体规则设置示例iris示例代码1.中间件格式 错误返回forbidden目录结构代码示例2.路由修饰模式 错误返回403目录结构代码示例提示

casbin实现基于角色的HTTP权限控制

casbin介绍

casbin是由北大的一位博士生主导开发的一个基于Go语言的权限控制库。支持 ACL,RBAC,ABAC 等常用的访问控制模型。

casbin是Golang项目的强大而高效的开源访问控制库。 它支持基于各种访问控制模型实施授权。

casbin的核心是一套基于PERM metamodel(Policy, Effect, Request, Matchers)的DSL。Casbin从用这种DSL定义的配置文件中读取访问控制模型,作为后续权限验证的基础

Casbin做了什么

  • 支持自定义请求的格式,默认的请求格式为{subject, object, action}。
  • 具有访问控制模型model和策略policy两个核心概念。
  • 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  • 支持超级用户,如root或Administrator,超级用户可以不受授权策略的约束访问任意资源。
  • 支持多种内置的操作符,如keyMatch,方便对路径式的资源进行管理,如/foo/bar可以映射到/foo*

Casbin不做的事情

  • 身份认证authentication(即验证用户的用户名、密码),casbin只负责访问控制。应该有其他专门的组件负责身份认证,然后由casbin进行访问控制,二者是相互配合的关系。
  • 管理用户列表或角色列表。Casbin认为由项目自身来管理用户、角色列表更为合适,用户通常有他们的密码,但是Casbin的设计思想并不是把它作为一个存储密码的容器。而是存储RBAC方案中用户和角色之间的映射关系。

配置示例

模型与策略定制

//sub   "alice"// 想要访问资源的用户.//obj  "data1" // 要访问的资源.//act  "read"  // 用户对资源执行的操作.# Request definition[request_definition]r = sub, obj, act# Policy definition[policy_definition]p = sub, obj, act# Policy effect[policy_effect]e = some(where (p.eft == allow))# Matchers[matchers]m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

可以看到这个配置文件主要定义了Request和Policy的组成结构.Policy effect和Matchers则灵活的多,可以包含一些自定义的表达式比如我们要加入一个名叫root的超级管理员,就可以这样写:

[matchers]m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

又比如我们可以用正则匹配来判断权限是否匹配:

[matchers]m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

具体规则设置

p, alice, data1, readp, bob, data2, write

意思就是 alice 可以读 data1,bob 可以写 data2

示例

模型与策略定制 test.conf

[request_definition]r = sub, dom, obj, act[policy_definition]p = sub, dom, obj, act[role_definition]g = _, _, _[policy_effect]e = some(where (p.eft == allow))[matchers]m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

具体规则设置 test.csv

p, admin, domain1, data1, readp, admin, domain1, data1, writep, admin, domain2, data2, readp, admin, domain2, data2, writeg, alice, admin, domain1g, bob, admin, domain2

如上所示,alice 和 bob 分别是 domian1 和 domain2 的管理员

iris示例代码

1.中间件格式 错误返回forbidden

目录结构

主目录middleware

    —— casbinmodel.conf    —— casbinpolicy.csv    —— main.go    —— main_test.go

代码示例

casbinmodel.conf

[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 = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

casbinpolicy.csv

p, alice, /dataset1/*, GETp, alice, /dataset1/resource1, POSTp, bob, /dataset2/resource1, *p, bob, /dataset2/resource2, GETp, bob, /dataset2/folder1/*, POST

main.go

package mainimport (    "github.com/kataras/iris"    "github.com/casbin/casbin"    cm "github.com/iris-contrib/middleware/casbin")// $ go get github.com/casbin/casbin// $ go run main.go// Enforcer映射模型和casbin服务的策略,我们也在main_test上使用此变量。var Enforcer = casbin.NewEnforcer("casbinmodel.conf", "casbinpolicy.csv")func newApp() *iris.Application {    casbinMiddleware := cm.New(Enforcer)    app := iris.New()    app.Use(casbinMiddleware.ServeHTTP)    app.Get("/", hi)    app.Get("/dataset1/{p:path}", hi) // p, alice, /dataset1/*, GET    app.Post("/dataset1/resource1", hi)    app.Get("/dataset2/resource2", hi)    app.Post("/dataset2/folder1/{p:path}", hi)    app.Any("/dataset2/resource1", hi)    return app}func main() {    app := newApp()    app.Run(iris.Addr(":8080"))}func hi(ctx iris.Context) {    ctx.Writef("Hello %s", cm.Username(ctx.Request()))}

main_test.go

package mainimport (    "testing"    "github.com/iris-contrib/httpexpect"    "github.com/kataras/iris/httptest")func TestCasbinMiddleware(t *testing.T) {    app := newApp()    e := httptest.New(t, app, httptest.Debug(false))    type ttcasbin struct {        username string        path     string        method   string        status   int    }    tt := []ttcasbin{        {"alice", "/dataset1/resource1", "GET", 200},        {"alice", "/dataset1/resource1", "POST", 200},        {"alice", "/dataset1/resource2", "GET", 200},        {"alice", "/dataset1/resource2", "POST", 404},        {"bob", "/dataset2/resource1", "GET", 200},        {"bob", "/dataset2/resource1", "POST", 200},        {"bob", "/dataset2/resource1", "DELETE", 200},        {"bob", "/dataset2/resource2", "GET", 200},        {"bob", "/dataset2/resource2", "POST", 404},        {"bob", "/dataset2/resource2", "DELETE", 404},        {"bob", "/dataset2/folder1/item1", "GET", 404},        {"bob", "/dataset2/folder1/item1", "POST", 200},        {"bob", "/dataset2/folder1/item1", "DELETE", 404},        {"bob", "/dataset2/folder1/item2", "GET", 404},        {"bob", "/dataset2/folder1/item2", "POST", 200},        {"bob", "/dataset2/folder1/item2", "DELETE", 404},    }    for _, tt := range tt {        check(e, tt.method, tt.path, tt.username, tt.status)    }}func check(e *httpexpect.Expect, method, path, username string, status int) {    e.Request(method, path).WithBasicAuth(username, "password").Expect().Status(status)}

2.路由修饰模式 错误返回403

目录结构

主目录wrapper

    —— casbinmodel.conf    —— casbinpolicy.csv    —— main.go    —— main_test.go

代码示例

casbinmodel.conf

[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 = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

casbinpolicy.csv

p, alice, /dataset1/*, GETp, alice, /dataset1/resource1, POSTp, bob, /dataset2/resource1, *p, bob, /dataset2/resource2, GETp, bob, /dataset2/folder1/*, POSTp, cathrin, /dataset2/resource2, GETp, dataset1_admin, /dataset1/*, *g, cathrin, dataset1_admin

main.go

package mainimport (    "github.com/kataras/iris"    "github.com/casbin/casbin"    cm "github.com/iris-contrib/middleware/casbin")// $ go get github.com/casbin/casbin// $ go run main.go// Enforcer映射模型和casbin服务的策略,我们也在main_test上使用此变量。var Enforcer = casbin.NewEnforcer("casbinmodel.conf", "casbinpolicy.csv")func newApp() *iris.Application {    casbinMiddleware := cm.New(Enforcer)    app := iris.New()    app.WrapRouter(casbinMiddleware.Wrapper())    app.Get("/", hi)    app.Any("/dataset1/{p:path}", hi) // p, dataset1_admin, /dataset1/*, * && p, alice, /dataset1/*, GET    app.Post("/dataset1/resource1", hi)    app.Get("/dataset2/resource2", hi)    app.Post("/dataset2/folder1/{p:path}", hi)    app.Any("/dataset2/resource1", hi)    return app}func main() {    app := newApp()    app.Run(iris.Addr(":8080"))}func hi(ctx iris.Context) {    ctx.Writef("Hello %s", cm.Username(ctx.Request()))}

main_test.go

package mainimport (    "testing"    "github.com/iris-contrib/httpexpect"    "github.com/kataras/iris/httptest")func TestCasbinWrapper(t *testing.T) {    app := newApp()    e := httptest.New(t, app)    type ttcasbin struct {        username string        path     string        method   string        status   int    }    tt := []ttcasbin{        {"alice", "/dataset1/resource1", "GET", 200},        {"alice", "/dataset1/resource1", "POST", 200},        {"alice", "/dataset1/resource2", "GET", 200},        {"alice", "/dataset1/resource2", "POST", 403},        {"bob", "/dataset2/resource1", "GET", 200},        {"bob", "/dataset2/resource1", "POST", 200},        {"bob", "/dataset2/resource1", "DELETE", 200},        {"bob", "/dataset2/resource2", "GET", 200},        {"bob", "/dataset2/resource2", "POST", 403},        {"bob", "/dataset2/resource2", "DELETE", 403},        {"bob", "/dataset2/folder1/item1", "GET", 403},        {"bob", "/dataset2/folder1/item1", "POST", 200},        {"bob", "/dataset2/folder1/item1", "DELETE", 403},        {"bob", "/dataset2/folder1/item2", "GET", 403},        {"bob", "/dataset2/folder1/item2", "POST", 200},        {"bob", "/dataset2/folder1/item2", "DELETE", 403},    }    for _, tt := range tt {        check(e, tt.method, tt.path, tt.username, tt.status)    }    ttAdmin := []ttcasbin{        {"cathrin", "/dataset1/item", "GET", 200},        {"cathrin", "/dataset1/item", "POST", 200},        {"cathrin", "/dataset1/item", "DELETE", 200},        {"cathrin", "/dataset2/item", "GET", 403},        {"cathrin", "/dataset2/item", "POST", 403},        {"cathrin", "/dataset2/item", "DELETE", 403},    }    for _, tt := range ttAdmin {        check(e, tt.method, tt.path, tt.username, tt.status)    }    Enforcer.DeleteRolesForUser("cathrin")    ttAdminDeleted := []ttcasbin{        {"cathrin", "/dataset1/item", "GET", 403},        {"cathrin", "/dataset1/item", "POST", 403},        {"cathrin", "/dataset1/item", "DELETE", 403},        {"cathrin", "/dataset2/item", "GET", 403},        {"cathrin", "/dataset2/item", "POST", 403},        {"cathrin", "/dataset2/item", "DELETE", 403},    }    for _, tt := range ttAdminDeleted {        check(e, tt.method, tt.path, tt.username, tt.status)    }}func check(e *httpexpect.Expect, method, path, username string, status int) {    e.Request(method, path).WithBasicAuth(username, "password").Expect().Status(status)}

提示

  • 以上的go iris都是使用Basic Auth,用postman测试请选择Authorization选项
  • .conf文件是配置规则模型,.csv是具体规则的体现,当然也可不使用这些东西,用户数据或者其他代替
  • 解释一下我对这些的理解
//policy策略p, alice, /dataset1/*, GET         //alice 用户有对 method为GET路径满足 /dataset1/*的访问权限 下面同理p, alice, /dataset1/resource1, POSTp, bob, /dataset2/resource1, *p, bob, /dataset2/resource2, GETp, bob, /dataset2/folder1/*, POSTp, cathrin, /dataset2/resource2, GETp, dataset1_admin, /dataset1/*, *g, cathrin, dataset1_admin  //cathrin用户属于dataset1_admin组,也就是dataset1_admin能访问的cathrin都能访问,反之不然
//配置[request_definition]        //请求定义r = sub, obj, act[policy_definition]         //策略定义,也就是*.cvs文件 p 定义的格式p = sub, obj, act[role_definition]           //组定义,也就是*.cvs文件 g 定义的格式g = _, _[policy_effect]              e = some(where (p.eft == allow))[matchers]                 //满足条件m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")//请求用户与满足*.cvs p(策略)且满足g(组规则)且请求资源满足p(策略)规定资源