Golang最強大的訪問控制框架casbin全解析
Casbin是一個強大的、高效的開源訪問控制框架,其權限管理機制支持多種訪問控制模型。目前這個框架的生態已經發展的越來越好了。提供了各種語言的類庫,自定義的權限模型語言,以及模型編輯器。在各種語言中,golang的支持還是最全的,所以我們就研究casbin的golang實現。
訪問控制模型
控制訪問模型有哪幾種?我們需要先來了解下這個。
UGO(User, Group, Other)
這個是Linux中對於資源進行權限管理的訪問模型。Linux中一切資源都是文件,每個文件都可以設置三種角色的訪問權限(文件創建者,文件創建者所在組,其他人)。這種訪問模型的缺點很明顯,只能為一類用戶設置權限,如果這類用戶中有特殊的人,那么它無能為力了。
ACL(訪問控制列表)
它的原理是,每個資源都配置有一個列表,這個列表記錄哪些用戶可以對這項資源進行CRUD操作。當系統試圖訪問這項資源的時候,會首先檢查這個列表中是否有關於當前用戶的訪問權限,從而確定這個用戶是否有權限訪問當前資源。linux在UGO之外,也增加了這個功能。
setfacl -m user:yejianfeng:rw- ./test
[yejianfeng@ipd-itstool ~]$ getfacl test
# file: test
# owner: yejianfeng
# group: yejianfeng
user::rw-
user:yejianfeng:rw-
group::rw-
mask::rw-
other::r--
當我們使用getfacl和setfacl命令的時候我們就能對某個資源設置增加某個人,某個組的權限列表。操作系統會根據這個權限列表進行判斷,當前用戶是否有權限操作這個資源。
RBAC(基於角色的權限訪問控制)
這個是很多業務系統最通用的權限訪問控制系統。它的特點是在用戶和具體權限之間增加了一個角色。就是先設置一個角色,比如管理員,然后將用戶關聯某個角色中,再將角色設置某個權限。用戶和角色是多對多關系,角色和權限是多對多關系。所以一個用戶是否有某個權限,根據用戶屬於哪些角色,再根據角色是否擁有某個權限來判斷這個用戶是否有某個權限。
RBAC的邏輯有更多的變種。
變種一:角色引入繼承
角色引入了繼承概念,那么繼承的角色有了上下級或者等級關系。
變種二:角色引入了約束
角色引入了約束概念。約束概念有兩種,
一種是靜態職責分離:
a、互斥角色:同一個用戶在兩個互斥角色中只能選擇一個
b、基數約束:一個用戶擁有的角色是有限的,一個角色擁有的許可也是有限的
c、先決條件約束:用戶想要獲得高級角色,首先必須擁有低級角色
一種是動態職責分離:
可以動態的約束用戶擁有的角色,如一個用戶可以擁有兩個角色,但是運行時只能激活一個角色。
變種三:既有角色約束,又有角色繼承
就是前面兩種角色變種的集合。
ABAC(基於屬性的權限驗證)
Attribute-based access control,這種權限驗證模式是用屬性來標記資源權限的。比如k8s中就用到這個權限驗證方法。比如某個資源有pod屬性,有命名空間屬性,那么我設置的時候可以這樣設置:
Bob 可以在命名空間 projectCaribou 中讀取 pod:
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "bob", "namespace": "projectCaribou", "resource": "pods", "readonly": true}}
這個權限驗證模型的好處就是擴展性好,一旦要增加某種權限,就可以直接增加某種屬性。
DAC(自主訪問控制)
在ACL的訪問控制模式下,有個問題,能給資源增加訪問控制的是誰,這里就有幾種辦法,比如增加一個super user,這個超級管理員來做統一的操作。還有一種辦法,有某個權限的用戶來負責給其他用戶分配權限。這個就叫做自主訪問控制。
比如我們常用的windows就是用這么一種方法。
很多的wiki權限也是這樣的權限管理方式。
MAC(強制訪問控制)
強制訪問控制和DAC相反,它不將某些權限下放給用戶,而是在更高維度(比如操作系統)上將所有的用戶設置某些策略,這些策略是需要所有用戶強制執行的。這種訪問控制也是基於某些安全因素考慮。
casbin的基本使用
casbin使用配置文件來設置訪問控制模型。我們可以通過casbin的模型編輯器來查看。
它有兩個配置文件,model.conf 和 policy.csv。其中 model.conf 存儲的是我們的訪問控制模型,policy.csv 存儲的是我們具體的用戶權限配置。
權限本質上就是最終詢問這么一個問題“某個用戶,對某個資源,是否可以進行某種操作”。casbin的使用非常精煉。基本上就生成一個結構,Enforcer,構造這個結構的時候加載 model.conf 和 policy.csv。使用示例如下:
import "github.com/casbin/casbin/v2"
e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
sub := "alice" // the user that wants to access a resource.
obj := "data1" // the resource that is going to be accessed.
act := "read" // the operation that the user performs on the resource.
ok, err := e.Enforce(sub, obj, act) // 查看alice是否對data1z這個資源有read權限
if err != nil {
// handle err
}
if ok == true {
// permit alice to read data1
} else {
// deny the request, show an error
}
當然,casbin 可以讀取具體 policy 的時候不僅僅可以通過 csv 文件進行讀取,也可以通過數據庫進行讀取。這樣我們甚至可以寫一個用戶管理后台來配置不同的用戶權限。model.conf 也是可以從配置文件中獲取,也可以從代碼中獲取,從代碼中獲取就可以擴展為先讀取數據庫,再代碼加載。但是 model.conf 一旦修改,對應的 policy 就需要進行同步修改,所以 model 在一個系統中不要進行頻繁修改。
PML
casbin 是一種典型的“配置即一切”的軟件思路,那么它的配置語法就顯得格外重要。我們可以通過 casbin 的在線配置編輯器 https://casbin.org/en/editor 來進行學習。
casbin 的理論基礎是這么一篇論文:PML:一種基於Interpreter的Web服務訪問控制策略語言 。這篇論文是北大的三個學生一起發表的。要理解 casbin 的配置文件,就需要先看這篇論文。
論文的作者覺得現在雲計算時代,權限管理系統是各種雲非常重要的組成部分,但是各種權限管理模型在各個雲廠商,或者各種雲時代的產品又都不一樣。那么是否有一種權限模型來統一描述各種權限訪問方式呢?如果有的化,這種權限模型又需要獨立於各種語言而存在,才能被各種語言的雲產品所通用。
於是論文就創造除了這么一種語言:PML(PERM modeling language)。其中的 PERM 指的是 Policy-Effect-Request-Matcher 。 下面我們需要一一了解每一個概念。
Request
Request 代表的是請求,它的寫法是
request ::= r : attributes
attributes ::= {attr1, attr2, attr3, ..}
比如我們寫一行:
r = sub, obj, act
代表一個請求有三個標准的元素,請求主體,請求對象,請求操作。其中的sub, obj, act 可以是自己定義的,只要你在一個配置文件中定義的元素標識符一致就行。
Policy
Policy 代表策略,它表示具體的權限定義的規則是什么。
它同樣是形如 p = sub, obj, act 的表示方法,比如我們定義了 policy 的規則如此,那么我們在 policy.csv 中每一行定義的 policy_rule 就必須和這個屬性一一對應。
Policy_Rule
在 policy.csv 文件中定義的策略就是 policy_rule。它和 Policy 是一一對應的。
比如 policy 為
p = sub, obj, act
我設置的一條 policy_rule 為
p, bob, data2, write
表示bob(p.sub = bob)可以對data2 (p.obj = data2)進行 write (p.act = write) 操作這個規則。
policy 默認的最后一個屬性為決策結果,字段名eft,默認值為allow,即通過情況下,p.eft就設置為allow。
Matcher
有請求,有規則,那么請求是否匹配某個規則,則是matcher進行判斷的。
matcher ::=< boolean expr > (variables, constants, stub functions)
variables ::= {r.attr1, r.attr2, .., p.attr1, p.attr2, ..}
constants ::= {const1, const2, const3, ..}
比如下面這個matcher :
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
表示當(r.sub == p.sub && r.obj == p.obj && r.act == p.act )的時候,返回true,否則返回false。
Effect
Effect 用來判斷如果一個請求滿足了規則,是否需要同意請求。它的規則比較復雜一些。
effect ::=< boolean expr > (effect term1, effect term2, ..)
effect term ::= quantifier, condition
quantif ier ::= some|any|max|min
condition ::=< expr > (variables, constants, stub functions)
variables ::= {r.attr1, r.attr2, .., p.attr1, p.attr2, ..}
constants ::= {const1, const2, const3, ..}
這里的 quantifier一般是some(論文中支持max和min),some表示括號中的表達式個數大於等於1就行。max/min表示括號中表達式的結果取最大/小的。(這里我不是很理解,不過好像casbin也沒有實現min和max)
下面這個例子:
e = some(where (p.eft == allow))
這句話的意思就是將 request 和所有 policy 比對完之后,所有 policy 的策略結果(p.eft)為allow的個數 >=1,整個請求的策略就是為 true。
自定義函數
自定義函數是在 matcher 中使用的。我們可以自己定義一個函數,然后注冊進enforcer,在matcher中我們就可以使用了。
比如
func KeyMatch(key1 string, 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]
}
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
casbin中有一些自定義的函數:
關系
上面幾個概念關系如下:
大概解釋一下:
1 我們先定義屬性,通用的一些屬性如 subject, object, action。
2 定義的屬性可以作為 Request 的屬性,也可以作為 Policy的屬性。
3 Policy_Rule 是 Policy 的具體規則。
4 使用定義的 Matcher 將 Request 和 Policy 進行匹配,這個匹配的過程可能使用到自定義函數。
5 所有的 Policy 匹配完成的結果,通過 Effect 規則得出最終是否可以訪問的結果。
例子:ACL
理解上面的知識,我們應該能理解這個ACL的例子:
這個例子中定義了兩個 Policy_Rule: (alice 對 data1 有 read 權限) 和 (bob 對 data2 有 write 權限)
當request (alice, data1, read)進來的時候,它匹配了其中一條規則,所以some 之后的最終結果為true。
例子:RESTFUL
RESTFUL接口使用URL和HTTP請求方法表示資源的增刪改查,那么我們可以使用自定義函數來判斷是否可以進行某個請求
更多
這個論文還有一些其他的定義:
Has_Role
其實這個就是一個自定義函數的概念,只是它的參數是請求的主體和角色。這里引入了一個角色的概念。這個也是RBAC 權限模型所定義的。Has_Role 本質就是定義了一個 g 函數,這個 g 用於判斷哪個用戶是否屬於哪個角色。這個 g 的函數也可以用配置寫規則:
g = _, _
然后在 Policy 寫規則:
g, alice, data2_admin
表示 alice 屬於角色 data2_admin。
matcher 就可以寫成這樣:
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
例子:RBAC
我們來看下下面這個RBAC的規則:
我們可以看這里的 Policy 中,其實用戶和角色是分不出來的,(比如我們單看policy里面的p,是不了解data2_admin是用戶,還是角色的)。但是我們有一個 g (has_role)的規則,說明了alice 是有 data2_admin的角色的。
那么最終判斷請求, alice, data2, read, 由於alice 有data2_admin的角色,它滿足了(p, data2_admin, data2, read) 這條規則,所以最終判定結果為 true。
其實有了這個has_role,我們也可以把一個用戶屬於另一個用戶的關系做出來。這個也就是 RBAC1 的。
Has Tenant Role
g 函數同時也可以有三個參數,兩個參數的時候表示“誰 是 什么角色”,三個參數的時候表示“誰 在 什么域 是 什么角色”。
這個還是直接看例子:
在這個例子里面,有個域的概念,它就相當於可以表示“某個用戶在某個域(租戶)中是什么角色”。
這個是實現了一種基於RBAC的分權分域用戶權限系統。
總結
Casbin 支持的權限模型有:
- ACL (Access Control List, 訪問控制列表)
- 具有 超級用戶 的 ACL
- 沒有用戶的 ACL: 對於沒有身份驗證或用戶登錄的系統尤其有用。
- 沒有資源的 ACL: 某些場景可能只針對資源的類型, 而不是單個資源, 諸如 write-article, read-log等權限。 它不控制對特定文章或日志的訪問。
- RBAC (基於角色的訪問控制)
- 支持資源角色的RBAC: 用戶和資源可以同時具有角色 (或組)。
- 支持域/租戶的RBAC: 用戶可以為不同的域/租戶設置不同的角色集。
- ABAC (基於屬性的訪問控制): 支持利用resource.Owner這種語法糖獲取元素的屬性。
- RESTful: 支持路徑, 如 /res/*, /res/: id 和 HTTP 方法, 如 GET, POST, PUT, DELETE。
- 拒絕優先: 支持允許和拒絕授權, 拒絕優先於允許。
- 優先級: 策略規則按照先后次序確定優先級,類似於防火牆規則。
我們可以通過這個頁面上的鏈接看每個權限模型的配置:https://casbin.org/docs/zh-CN/supported-models
源碼閱讀
我們閱讀的是 v2.1.2版本
源碼地址:https://github.com/casbin/casbin 。
注釋版地址:https://github.com/jianfengye/inside-go/tree/master/casbin-2.1.2。
按照大象裝進冰箱的邏輯,我們也很容易想象得到 casbin 應該分為幾個步驟:
1 加載 model 的配置
2 加載 policy 的配置
3 具體請求進來之后,和 model 和 policy 進行匹配判斷。
確實源碼也就是這么寫的。
整個 casbin 最核心的結構是
// Enforcer是權限驗證的主體
type Enforcer struct {
modelPath string // model文件地址
model model.Model // model結構
fm model.FunctionMap // 自定義函數
eft effect.Effector // effecter的邏輯
adapter persist.Adapter // 持久化的Adapter,就是police的Adapter
watcher persist.Watcher
rm rbac.RoleManager // 這個要是這個模型是rbac(根據是否有g判斷),就增加這個角色管理器
enabled bool // 這個Enforcer的開關
autoSave bool // 如果調用了api修改的Policy,是否自動保存到Adapter中
autoBuildRoleLinks bool
}
所有加載的 model 和 policy 都是豐富這個結構體。
比如:
func (e *Enforcer) LoadModel() error
或者
func (e *Enforcer) LoadPolicy() error
里面的邏輯就不細說了,可以去看 https://github.com/jianfengye/inside-go/tree/master/casbin-2.1.2 看這個的源碼注釋。我就說幾個我覺得這個項目代碼值得學習或者比較特別的點。
代碼依賴少
這個項目的 config, log 都是自己從標准庫從頭開始寫的。我認為,作者一開始對項目就定位很清晰,這個是一個基礎類庫,能依賴盡可能少的項目就依賴盡可能少的項目。
文件夾及文件定義很明確
casbin 的文件夾結構並不是扁平的,而是樹形結構,基本上是兩層,甚至用到了三層。我個人覺得一個小的類庫倒是沒有必要分割這么多文件夾,很容易讓人感覺很復雜。不過在 casbin 這個項目中,文件夾分割的還是比較清晰的。基本上是順着 enforcer 這個結構體的定義,在涉及到需要擴展的字段的時候就起一個文件夾。每個擴展的文件夾基本都是 interface + implement 的方式。比如 enforcer 中有個 adapter 字段,使用了persist文件夾進行接口的定義和實現。
文件的定義也很清晰。比如 enforcer_interface 定義了80多個接口,在一個文件中全部實現並不是很好的寫法,它就分成了同一個文件夾下的幾個文件來寫(internal_api, rbac_api, management_api,rbac_api_with_domain)。這樣不僅減少了代碼的長度,還使用文件名稱把接口進行了分類。
基於 enforcer 擴展了 CacheEnforcer 和 SyncedEnforcer
我們寫 enforcer 可能會想到是否需要緩存,是否配置變化能及時更新,這里使用了一個父類和兩個子類的方式來實現。把是否使用緩存,是否使用配置的選擇權交給用戶。而不是簡單在一個結構體里面塞上這些功能。
先定義接口
基本上每個可以擴展的結構都考慮到使用接口進行定義。定義接口擴展性提高了,且可以更加豐富了。
adapter
我覺得 persist/adapter 里面這個寫法挺好的。
首先它定義了 adapter 這個接口,讓實現接口的類來具體實現我從哪個持久化存儲里面讀取配置文件,但是希望每一行都有統一的讀取規則,所以它就把 adapter 接口和 LoadPolicyLine 方法放同一個文件里面。
其次繼承結構:
它實現了接口的繼承,filterdApapter 接口繼承 Adapter,file-filterdAdapter繼承了file-Adapter。
如果我們寫,有可能就簡單定義了一個 filterLine 的函數,它這里更近一步,可以直接設置根據 p 或者 g 的某個字段進行過濾。
if err := e.LoadFilteredPolicy(&fileadapter.Filter{
P: []string{"", "domain1"},
G: []string{"", "", "domain1"},
}); err != nil {
t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}
使用起來就大大方便了。
總結
casbin 項目最核心的一定是那篇 PML 的論文。基於理論論文發展出來的這個項目也是非常牛X的。casbin 應該能滿足大多數的權限管理系統的要求了。如果 casbin 不能實現你的權限需求的話,我覺得應該先思考下,產品經理提出的需求是不是靠譜的。。。哈哈哈