Go 每日一庫之 casbin

簡介

權限管理在幾乎每一個系統中都是必備的模塊。若是項目開發每次都要實現一次權限管理,無疑會浪費開發時間,增長開發成本。所以,casbin庫出現了。casbin是一個強大、高效的訪問控制庫。支持經常使用的多種訪問控制模型,如ACL/RBAC/ABAC等。能夠實現靈活的訪問權限控制。同時,casbin支持多種編程語言,Go/Java/Node/PHP/Python/.NET/Rust。咱們只須要一次學習,多處運用html

快速使用

咱們依然使用 Go Module 編寫代碼,先初始化:mysql

$ mkdir casbin && cd casbin
$ go mod init github.com/darjun/go-daily-lib/casbin
複製代碼

而後安裝casbin,目前是v2版本:git

$ go get github.com/casbin/casbin/v2
複製代碼

權限實際上就是控制能對什麼資源進行什麼操做。casbin將訪問控制模型抽象到一個基於 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。所以切換或更新受權機制只須要簡單地修改配置文件。github

policy是策略或者說是規則的定義。它定義了具體的規則。golang

request是對訪問請求的抽象,它與e.Enforce()函數的參數是一一對應的sql

matcher匹配器會將請求與定義的每一個policy一一匹配,生成多個匹配結果。數據庫

effect根據對請求運用匹配器得出的全部結果進行彙總,來決定該請求是容許仍是拒絕編程

下面這張圖很好地描繪了這個過程:bash

咱們首先編寫模型文件:微信

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

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

[policy_effect]
e = some(where (p.eft == allow))
複製代碼

上面模型文件規定了權限由sub,obj,act三要素組成,只有在策略列表中有和它徹底相同的策略時,該請求才能經過。匹配器的結果能夠經過p.eft獲取,some(where (p.eft == allow))表示只要有一條策略容許便可。

而後咱們策略文件(即誰能對什麼資源進行什麼操做):

p, dajun, data1, read
p, lizi, data2, write
複製代碼

上面policy.csv文件的兩行內容表示dajun對數據data1read權限,lizi對數據data2write權限。

接下來就是使用的代碼:

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "data1", "read")
  check(e, "lizi", "data2", "write")
  check(e, "dajun", "data1", "write")
  check(e, "dajun", "data2", "read")
}
複製代碼

代碼其實不復雜。首先建立一個casbin.Enforcer對象,加載模型文件model.conf和策略文件policy.csv,調用其Enforce方法來檢查權限。運行程序:

$ go run main.go
dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
複製代碼

請求必須徹底匹配某條策略才能經過。("dajun", "data1", "read")匹配p, dajun, data1, read("lizi", "data2", "write")匹配p, lizi, data2, write,因此前兩個檢查經過。第 3 個由於"dajun"沒有對data1write權限,第 4 個由於dajundata2沒有read權限,因此檢查都不能經過。輸出結果符合預期。

sub/obj/act依次對應傳給Enforce方法的三個參數。實際上這裏的sub/obj/actread/write/data1/data2是我本身隨便取的,你徹底可使用其它的名字,只要能先後一致便可。

上面例子中實現的就是ACL(access-control-list,訪問控制列表)。ACL顯示定義了每一個主體對每一個資源的權限狀況,未定義的就沒有權限。咱們還能夠加上超級管理員,超級管理員能夠進行任何操做。假設超級管理員爲root,咱們只須要修改匹配器:

[matchers]
e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"
複製代碼

只要訪問主體是root一概放行。

驗證:

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "root", "data1", "read")
  check(e, "root", "data2", "write")
  check(e, "root", "data1", "execute")
  check(e, "root", "data3", "rwx")
}
複製代碼

由於sub = "root"時,匹配器必定能經過,運行結果:

$ go run main.go
root CAN read data1
root CAN write data2
root CAN execute data1
root CAN rwx data3
複製代碼

RBAC 模型

ACL模型在用戶和資源都比較少的狀況下沒什麼問題,可是用戶和資源量一大,ACL就會變得異常繁瑣。想象一下,每次新增一個用戶,都要把他須要的權限從新設置一遍是多麼地痛苦。RBAC(role-based-access-control)模型經過引入角色(role)這個中間層來解決這個問題。每一個用戶都屬於一個角色,例如開發者、管理員、運維等,每一個角色都有其特定的權限,權限的增長和刪除都經過角色來進行。這樣新增一個用戶時,咱們只須要給他指派一個角色,他就能擁有該角色的全部權限。修改角色的權限時,屬於這個角色的用戶權限就會相應的修改。

casbin中使用RBAC模型須要在模型文件中添加role_definition模塊:

[role_definition]
g = _, _

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
複製代碼

g = _,_定義了用戶——角色,角色——角色的映射關係,前者是後者的成員,擁有後者的權限。而後在匹配器中,咱們不須要判斷r.subp.sub徹底相等,只須要使用g(r.sub, p.sub)來判斷請求主體r.sub是否屬於p.sub這個角色便可。最後咱們修改策略文件添加用戶——角色定義:

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, dajun, admin
g, lizi, developer
複製代碼

上面的policy.csv文件規定了,dajun屬於admin管理員,lizi屬於developer開發者,使用g來定義這層關係。另外admin對數據datareadwrite權限,而developer對數據data只有read權限。

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "data", "read")
  check(e, "dajun", "data", "write")
  check(e, "lizi", "data", "read")
  check(e, "lizi", "data", "write")
}
複製代碼

很顯然lizi所屬角色沒有write權限:

dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data
複製代碼

多個RBAC

casbin支持同時存在多個RBAC系統,即用戶和資源都有角色:

[role_definition]
g=_,_
g2=_,_

[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
複製代碼

上面的模型文件定義了兩個RBAC系統gg2,咱們在匹配器中使用g(r.sub, p.sub)判斷請求主體屬於特定組,g2(r.obj, p.obj)判斷請求資源屬於特定組,且操做一致便可放行。

策略文件:

p, admin, prod, read
p, admin, prod, write
p, admin, dev, read
p, admin, dev, write
p, developer, dev, read
p, developer, dev, write
p, developer, prod, read
g, dajun, admin
g, lizi, developer
g2, prod.data, prod
g2, dev.data, dev
複製代碼

先看角色關係,即最後 4 行,dajun屬於admin角色,lizi屬於developer角色,prod.data屬於生產資源prod角色,dev.data屬於開發資源dev角色。admin角色擁有對proddev類資源的讀寫權限,developer只能擁有對dev的讀寫權限和prod的讀權限。

check(e, "dajun", "prod.data", "read")
check(e, "dajun", "prod.data", "write")
check(e, "lizi", "dev.data", "read")
check(e, "lizi", "dev.data", "write")
check(e, "lizi", "prod.data", "write")
複製代碼

第一個函數中e.Enforce()方法在實際執行的時候先獲取dajun所屬角色admin,再獲取prod.data所屬角色prod,根據文件中第一行p, admin, prod, read容許請求。最後一個函數中lizi屬於角色developer,而prod.data屬於角色prod,全部策略都不容許,故該請求被拒絕:

dajun CAN read prod.data
dajun CAN write prod.data
lizi CAN read dev.data
lizi CAN write dev.data
lizi CANNOT write prod.data
複製代碼

多層角色

casbin還能爲角色定義所屬角色,從而實現多層角色關係,這種權限關係是能夠傳遞的。例如dajun屬於高級開發者seniorseinor屬於開發者,那麼dajun也屬於開發者,擁有開發者的全部權限。咱們能夠定義開發者共有的權限,而後額外爲senior定義一些特殊的權限。

模型文件不用修改,策略文件改動以下:

p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer
複製代碼

上面policy.csv文件定義了高級開發者senior對數據datawrite權限,普通開發者developer對數據只有read權限。同時senior也是developer,因此senior也繼承其read權限。dajun屬於senior,因此dajundatareadwrite權限,而lizi只屬於developer,對數據data只有read權限。

check(e, "dajun", "data", "read")
check(e, "dajun", "data", "write")
check(e, "lizi", "data", "read")
check(e, "lizi", "data", "write")
複製代碼

RBAC domain

casbin中,角色能夠是全局的,也能夠是特定domain(領域)或tenant(租戶),能夠簡單理解爲。例如dajun在組tenant1中是管理員,擁有比較高的權限,在tenant2可能只是個弟弟。

使用RBAC domain須要對模型文件作如下修改:

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

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _,_,_

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.obj
複製代碼

g=_,_,_表示前者在後者中擁有中間定義的角色,在匹配器中使用g要帶上dom

p, admin, tenant1, data1, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2
複製代碼

tenant1中,只有admin能夠讀取數據data1。在tenant2中,只有admin能夠讀取數據data2dajuntenant1中是admin,可是在tenant2中不是。

func check(e *casbin.Enforcer, sub, domain, obj, act string) {
  ok, _ := e.Enforce(sub, domain, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s in %s\n", sub, act, obj, domain)
  } else {
    fmt.Printf("%s CANNOT %s %s in %s\n", sub, act, obj, domain)
  }
}

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "tenant1", "data1", "read")
  check(e, "dajun", "tenant2", "data2", "read")
}
複製代碼

結果不出意料:

dajun CAN read data1 in tenant1
dajun CANNOT read data2 in tenant2
複製代碼

ABAC

RBAC模型對於實現比較規則的、相對靜態的權限管理很是有用。可是對於特殊的、動態的需求,RBAC就顯得有點力不從心了。例如,咱們在不一樣的時間段對數據data實現不一樣的權限控制。正常工做時間9:00-18:00全部人均可以讀寫data,其餘時間只有數據全部者能讀寫。這種需求咱們能夠很方便地使用ABAC(attribute base access list)模型完成:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[matchers]
m = r.sub.Hour >= 9 && r.sub.Hour < 18 || r.sub.Name == r.obj.Owner

[policy_effect]
e = some(where (p.eft == allow))
複製代碼

該規則不須要策略文件:

type Object struct {
  Name  string
  Owner string
}

type Subject struct {
  Name string
  Hour int
}

func check(e *casbin.Enforcer, sub Subject, obj Object, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  } else {
    fmt.Printf("%s CANNOT %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  }
}

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  o := Object{"data", "dajun"}
  s1 := Subject{"dajun", 10}
  check(e, s1, o, "read")

  s2 := Subject{"lizi", 10}
  check(e, s2, o, "read")

  s3 := Subject{"dajun", 20}
  check(e, s3, o, "read")

  s4 := Subject{"lizi", 20}
  check(e, s4, o, "read")
}
複製代碼

顯然lizi20:00不能read數據data

dajun CAN read data at 10:00
lizi CAN read data at 10:00
dajun CAN read data at 20:00
lizi CANNOT read data at 20:00
複製代碼

咱們知道,在model.conf文件中能夠經過r.subr.objr.act來訪問傳給Enforce方法的參數。實際上sub/obj能夠是結構體對象,得益於govaluate庫的強大功能,咱們能夠在model.conf文件中獲取這些結構體的字段值。如上面的r.sub.Namer.Obj.Owner等。govaluate庫的內容能夠參見我以前的一篇文章《Go 每日一庫之 govaluate》

使用ABAC模型能夠很是靈活的權限控制,可是通常狀況下RBAC就已經夠用了。

模型存儲

上面代碼中,咱們一直將模型存儲在文件中。casbin也能夠實如今代碼中動態初始化模型,例如get-started的例子能夠改寫爲:

func main() {
  m := model.NewModel()
  m.AddDef("r", "r", "sub, obj, act")
  m.AddDef("p", "p", "sub, obj, act")
  m.AddDef("e", "e", "some(where (p.eft == allow))")
  m.AddDef("m", "m", "r.sub == g.sub && r.obj == p.obj && r.act == p.act")

  a := fileadapter.NewAdapter("./policy.csv")
  e, err := casbin.NewEnforcer(m, a)
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "data1", "read")
  check(e, "lizi", "data2", "write")
  check(e, "dajun", "data1", "write")
  check(e, "dajun", "data2", "read")
}
複製代碼

一樣地,咱們也能夠從字符串中加載模型:

func main() {
  text := ` [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 && r.obj == p.obj && r.act == p.act `

  m, _ := model.NewModelFromString(text)
  a := fileadapter.NewAdapter("./policy.csv")
  e, _ := casbin.NewEnforcer(m, a)

  check(e, "dajun", "data1", "read")
  check(e, "lizi", "data2", "write")
  check(e, "dajun", "data1", "write")
  check(e, "dajun", "data2", "read")
}
複製代碼

可是這兩種方式並不推薦。

策略存儲

在前面的例子中,咱們都是將策略存儲在policy.csv文件中。通常在實際應用中,不多使用文件存儲。casbin以第三方適配器的方式支持多種存儲方式包括MySQL/MongoDB/Redis/Etcd等,還能夠實現本身的存儲。完整列表看這裏casbin.org/docs/en/ada…。下面咱們介紹使用Gorm Adapter。先鏈接到數據庫,執行下面的SQL

CREATE DATABASE IF NOT EXISTS casbin;

USE casbin;

CREATE TABLE IF NOT EXISTS casbin_rule (
  p_type VARCHAR(100) NOT NULL,
  v0 VARCHAR(100),
  v1 VARCHAR(100),
  v2 VARCHAR(100),
  v3 VARCHAR(100),
  v4 VARCHAR(100),
  v5 VARCHAR(100)
);

INSERT INTO casbin_rule VALUES
('p', 'dajun', 'data1', 'read', '', '', ''),
('p', 'lizi', 'data2', 'write', '', '', '');
複製代碼

而後使用Gorm Adapter加載policyGorm Adapter默認使用casbin庫中的casbin_rule表:

package main

import (
  "fmt"

  "github.com/casbin/casbin/v2"
  gormadapter "github.com/casbin/gorm-adapter/v2"
  _ "github.com/go-sql-driver/mysql"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  a, _ := gormadapter.NewAdapter("mysql", "root:12345@tcp(127.0.0.1:3306)/")
  e, _ := casbin.NewEnforcer("./model.conf", a)

  check(e, "dajun", "data1", "read")
  check(e, "lizi", "data2", "write")
  check(e, "dajun", "data1", "write")
  check(e, "dajun", "data2", "read")
}
複製代碼

運行:

dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
複製代碼

使用函數

咱們能夠在匹配器中使用函數。casbin內置了一些函數keyMatch/keyMatch2/keyMatch3/keyMatch4都是匹配 URL 路徑的,regexMatch使用正則匹配,ipMatch匹配 IP 地址。參見casbin.org/docs/en/fun…。使用內置函數咱們能很容易對路由進行權限劃分:

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act
複製代碼
p, dajun, user/dajun/*, read
p, lizi, user/lizi/*, read
複製代碼

不一樣用戶只能訪問其對應路由下的 URL:

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "user/dajun/1", "read")
  check(e, "lizi", "user/lizi/2", "read")
  check(e, "dajun", "user/lizi/1", "read")
}
複製代碼

輸出:

dajun CAN read user/dajun/1
lizi CAN read user/lizi/2
dajun CANNOT read user/lizi/1
複製代碼

咱們固然也能夠定義本身的函數。先定義一個函數,返回 bool:

func KeyMatch(key1, key2 string) bool {
  i := strings.Index(key2, "*")
  if i == -1 {
    return key1 == key2
  }

  if len(key1) > i {
    return key1[:i] == key2[:i]
  }

  return key1 == key2[:i]
}
複製代碼

這裏實現了一個簡單的正則匹配,只處理*

而後將這個函數用interface{}類型包裝一層:

func KeyMatchFunc(args ...interface{}) (interface{}, error) {
  name1 := args[0].(string)
  name2 := args[1].(string)

  return (bool)(KeyMatch(name1, name2)), nil
}
複製代碼

而後添加到權限認證器中:

e.AddFunction("my_func", KeyMatchFunc)
複製代碼

這樣咱們就能夠在匹配器中使用該函數實現正則匹配了:

[matchers]
m = r.sub == p.sub && my_func(r.obj, p.obj) && r.act == p.act
複製代碼

接下來咱們在策略文件中爲dajun賦予權限:

p, dajun, data/*, read
複製代碼

dajun對匹配模式data/*的文件都有read權限。

驗證一下:

check(e, "dajun", "data/1", "read")
check(e, "dajun", "data/2", "read")
check(e, "dajun", "data/1", "write")
check(e, "dajun", "mydata", "read")
複製代碼

dajundata/1沒有write權限,mydata不符合data/*模式,也沒有read權限:

dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata
複製代碼

總結

casbin功能強大,簡單高效,且多語言通用。值得學習。

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

  1. casbin GitHub:github.com/casbin/casb…
  2. casbin 官網:casbin.org/
  3. 一種基於元模型的訪問控制策略描述語言:www.jos.org.cn/html/2020/2…
  4. Go 每日一庫 GitHub:github.com/darjun/go-d…

個人博客:darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索