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

簡介

Casbin是一個強大的,高效的開源訪問控制框架,其權限管理機制支持多種訪問控制模型mysql

支持編程語言

不一樣語言中支持的特性

咱們一直致力於讓 Casbin 在不一樣的編程語言中擁有相同的特性。 可是現實老是不完美的。 上方的表格展現了當前的進度。 Watcher 和 Role Manager 的 ✅ 僅表明 Casbin 對該編程語言有接口, 是否實現了 watcher 或 role manager 接口則是另外一回事了。git

Casbin是什麼?

Casbin 能夠:github

  1. 支持自定義請求的格式,默認的請求格式爲{subject, object, action}。
  2. 具備訪問控制模型model和策略policy兩個核心概念。
  3. 支持RBAC中的多層角色繼承,不止主體能夠有角色,資源也能夠具備角色。
  4. 支持內置的超級用戶 例如:root或administrator。超級用戶能夠執行任何操做而無需顯式的權限聲明。
  5. 支持多種內置的操做符,如 keyMatch,方便對路徑式的資源進行管理,如 /foo/bar 能夠映射到 /foo*

Casbin 不能:sql

  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權限。dom

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

使用代碼

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
相關文章
相關標籤/搜索