天天看點

Go之Casbin簡介,安裝,模型,存儲,函數

Go之Casbin簡介,安裝,模型,存儲,函數

簡介

Casbin是一個強大的,高效的開源通路控制架構,其權限管理機制支援多種通路控制模型

支援程式設計語言

Go之Casbin簡介,安裝,模型,存儲,函數

不同語言中支援的特性

Go之Casbin簡介,安裝,模型,存儲,函數

我們一直緻力于讓 Casbin 在不同的程式設計語言中擁有相同的特性。 但是現實總是不完美的。 上方的表格展示了目前的進度。 Watcher 和 Role Manager 的 ✅ 僅代表 Casbin 對該程式設計語言有接口, 是否實作了 watcher 或 role manager 接口則是另一回事了。

Casbin是什麼?

Casbin 可以:

  1. 支援自定義請求的格式,預設的請求格式為{subject, object, action}。
  2. 具有通路控制模型model和政策policy兩個核心概念。
  3. 支援RBAC中的多層角色繼承,不止主體可以有角色,資源也可以具有角色。
  4. 支援内置的超級使用者 例如:root或administrator。超級使用者可以執行任何操作而無需顯式的權限聲明。
  5. 支援多種内置的操作符,如 keyMatch,友善對路徑式的資源進行管理,如 /foo/bar 可以映射到 /foo*

Casbin 不能:

  1. 身份認證 authentication(即驗證使用者的使用者名、密碼),casbin隻負責通路控制。應該有其他專門的元件負責身份認證,然後由casbin進行通路控制,二者是互相配合的關系。
  2. 管理使用者清單或角色清單。 Casbin 認為由項目自身來管理使用者、角色清單更為合适, 使用者通常有他們的密碼,但是 Casbin 的設計思想并不是把它作為一個存儲密碼的容器。 而是存儲RBAC方案中使用者和角色之間的映射關系。

常見通路控制模型

ABAC: 基于屬性的通路控制。

DAC: 自主通路控制模型(DAC,Discretionary Access Control)是根據自主通路控制政策建立的一種模型,允許合法使用者以使用者或使用者組的身份通路政策規定的客體,同時阻止非授權使用者通路客體。擁有客體權限的使用者,可以将該客體的權限配置設定給其他使用者。

ACL:  ACL是最早也是最基本的一種通路控制機制,它的原理非常簡單:每一項資源,都配有一個清單,這個清單記錄的就是哪些使用者可以對這項資源執行CRUD中的那些操作。當系統試圖通路這項資源時,會首先檢查這個清單中是否有關于目前使用者的通路權限,進而确定目前使用者可否執行相應的操作。總得來說,ACL是一種面向資源的通路控制模型,它的機制是圍繞“資源”展開的。

RBAC: 基于角色的通路控制(RBAC, Role Based Access Control)在使用者和權限之間引入了“角色(Role)”的概念,角色解耦了使用者和權限之間的關系

安裝

go get github.com/casbin/casbin/v2
           

快速使用(ACL)

編寫模型檔案

// model.conf

[request_definition]
r = sub, obj, act

// definition  (defer 勒行 "定義")
[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))
           

權限實際上就是控制誰能對什麼資源進行什麼操作。casbin将通路控制模型抽象到一個基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置檔案(模型檔案)中。是以切換或更新授權機制隻需要簡單地修改配置檔案。

policy (潑喔c "政策") 是政策或者說是規則的定義。它定義了具體的規則, effect 用來判斷如果一個請求滿足了規則,是否需要同意請求.

request_definition 是對通路請求的抽象,它與e.Enforce()函數的參數是一一對應的, r=sub,obj,act 代表一個請求有三個标準元素: 請求主體,請求對象,請求操作.

matcher (麥覺 "比對器" ) 比對器會将請求與定義的每個policy一一比對,生成多個比對結果, 有請求,有規則,那麼請求是否比對某個規則,則是matcher進行判斷的.

effect (呃 fai "影響") 根據對請求運用比對器得出的所有結果進行彙總,來決定該請求是允許還是拒絕。

上面模型檔案規定了權限由sub,obj,act三要素組成,隻有在政策清單中有和它完全相同的政策時,該請求才能通過。比對器的結果可以通過p.eft擷取,some(where (p.eft == allow))表示隻要有一條政策允許即可。

然後我們政策檔案(即誰能對什麼資源進行什麼操作):

// policy.csv

p, dajun, data1, read
p, lizi, data2, write
           

上面policy.csv檔案的兩行内容表示dajun對資料data1有read權限,lizi對資料data2有write權限。

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

Go之Casbin簡介,安裝,模型,存儲,函數

使用代碼

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"沒有對data1的write權限,第 4 個因為dajun對data2沒有read權限,是以檢查都不能通過。輸出結果符合預期。

sub/obj/act依次對應傳給Enforce方法的三個參數。實際上這裡的sub/obj/act和read/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一律放行

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, "root", "data1", "read")
	check(e, "root", "data2", "write")
	check(e, "root", "data1", "execute")
	check(e, "root", "data3", "rwx")
}

// 通路
youmen@youmendeMacBook-Pro casbin_demo1 % 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)這個中間層來解決這個問題。每個使用者都屬于一個角色,例如開發者、管理者、運維等,每個角色都有其特定的權限,權限的增加和删除都通過角色來進行。這樣新增一個使用者時,我們隻需要給他指派一個角色,他就能擁有該角色的所有權限。修改角色的權限時,屬于這個角色的使用者權限就會相應的修改。

單個RBAC

添加role_definition子產品

在casbin中使用RBAC模型需要在模型檔案中添加role_definition子產品:

// model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

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

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

g = _,_定義了使用者——角色,角色——角色的映射關系,前者是後者的成員,擁有後者的權限。然後在比對器中,我們不需要判斷r.sub與p.sub完全相等,隻需要使用g(r.sub, p.sub)來判斷請求主體r.sub是否屬于p.sub這個角色即可。最後我們修改政策檔案添加使用者——角色定義.

編寫policy

// policy.csv

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對資料data用read和write權限,而developer對資料data隻有read權限.

編寫main.go

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權限:
youmen@youmendeMacBook-Pro casbin_demo1 % go run main.go 
dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data
           

多個RBAC

casbin支援同時存在多個RBAC系統,即使用者和資源都有角色

編寫modl.conf

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

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

上面的模型檔案定義了兩個RBAC系統g和g2,我們在比對器中使用g(r.sub, p.sub)判斷請求主體屬于特定組,g2(r.obj, p.obj)判斷請求資源屬于特定組,且操作一緻即可放行。

編寫policy.csv

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角色擁有對prod和dev類資源的讀寫權限,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屬于進階開發者senior,seinor屬于開發者,那麼dajun也屬于開發者,擁有開發者的所有權限。我們可以定義開發者共有的權限,然後額外為senior定義一些特殊的權限。

編寫policy.csv

模型檔案不用修改,政策檔案改動如下:

p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer
           

上面policy.csv檔案定義了進階開發者senior對資料data有write權限,普通開發者developer對資料隻有read權限。同時senior也是developer,是以senior也繼承其read權限。dajun屬于senior,是以dajun對data有read和write權限,而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可能隻是個弟弟.

編寫model.conf

[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可以讀取資料data2。dajun在tenant1中是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))
           

該規則不需要政策檔案

package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)
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")
}

// 顯然lizi在20: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.sub和r.obj,r.act來通路傳給Enforce方法的參數。實際上sub/obj可以是結構體對象,得益于govaluate庫的強大功能,我們可以在model.conf檔案中擷取這些結構體的字段值。如上面的r.sub.Name、r.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加載policy,Gorm 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", "test:xxxxx@tcp(36.5.139.203: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")
}


// 運作
youmen@youmendeMacBook-Pro casbin_demo1 % go run main.go
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…。使用内置函數我們能很容易對路由進行權限劃分:

model.conf

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

policy.csv

p, dajun, user/dajun/*, read
p, lizi, user/lizi/*, read
           

不同使用者隻能通路對應路由下的URL

package main

import (
	"fmt"

	"github.com/casbin/casbin/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() {
	e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
	if err != nil {
		fmt.Printf("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")
           

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

dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata