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

casbin介绍

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

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

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

Casbin做了什么

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

Casbin不做的事情

  1. 身份认证authentication(即验证用户的用户名、密码),casbin只负责访问控制。应该有其他专门的组件负责身份认证,然后由casbin进行访问 控制,二者是相互配合的关系。
  2. 管理用户列表或角色列表。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

可以看到这个配置文件主要定义了RequestPolicy的组成结构.Policy effectMatchers则灵活的多,可以包含一些自定义的表达式 比如我们要加入一个名叫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, read
p, 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, read
p, admin, domain1, data1, write
p, admin, domain2, data2, read
p, admin, domain2, data2, write
g, alice, admin, domain1
g, 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/*, GET
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/resource1, *
p, bob, /dataset2/resource2, GET
p, bob, /dataset2/folder1/*, POST

main.go

package main

import (
    "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 main

import (
    "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/*, GET
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/resource1, *
p, bob, /dataset2/resource2, GET
p, bob, /dataset2/folder1/*, POST
p, cathrin, /dataset2/resource2, GET
p, dataset1_admin, /dataset1/*, *
g, cathrin, dataset1_admin

main.go

package main

import (
    "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 main

import (
    "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)
}

提示

  1. 以上的go iris都是使用Basic Auth,用postman测试请选择Authorization选项
  2. *.conf文件是配置规则模型,*.csv是具体规则的体现,当然也可不使用这些东西,用户数据或者其他代替
  3. 解释一下我对这些的理解
//policy策略
p, alice, /dataset1/*, GET         //alice 用户有对 method为GET路径满足 /dataset1/*的访问权限 下面同理
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/resource1, *
p, bob, /dataset2/resource2, GET
p, bob, /dataset2/folder1/*, POST
p, cathrin, /dataset2/resource2, GET
p, 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(策略)规定资源

results matching ""

    No results matching ""