概要
Graphql 請求靈活性遠非 RestFul 的請求能比, 但是 Graphql 的 Endpoint 一般都是統一的一個, 根據 body 中的請求內容和參數來決定返回何種數據.
因此, 對於 Graphql 請求的權限控制不能像 RestFul 請求那樣根據請求的 URL 和 method 來判斷是否有權限. 關於 Graphql 的權限判斷, 曾經困擾了好長一段時間, 一直沒有找到合適的方式來判斷.
直至最近一段時間, 在 github.com/graphql-go 這個庫中找到能夠解析 graphql 請求的方法, 然后結合 casbin 的 rbac 模型, 才算是將 Graphql 權限問題的解決推進了一大步.
實現方式
實現 Graphql 的權限認證, 主要包含以下 3 個部分:
- adapter: 用來連接 graphql-engine 的 casbin adapter, 也就是可以將權限策略持久存儲到數據庫
- middleware: 基於 golang gin 框架的中間件, 攔截請求並判斷其是否符合權限要求
- 權限 api: 提供操作權限的 API, 基於 RBAC 的, 所以只提供的基礎的幾個接口
adapter
casbin 默認是將權限策略存儲在 csv 文件中的, 這只能在 demo 中用用, 實際系統用明顯不合適. 因此, 我們需要寫個 adapter, 將權限策略寫入數據庫.
adapter 很簡單, 只要仿照 casbin 已有的那些 adapter 實現相應的接口即可:
1 package auth
2
3 import (
4 "runtime"
5
6 imodel "illuminant/model"
7
8 "github.com/casbin/casbin/v2/model"
9 "github.com/casbin/casbin/v2/persist"
10 )
11
12 // Adapter represents the hasura graphql
13 type Adapter struct{}
14
15 // finalizer is the destructor for Adapter.
16 func finalizer(a *Adapter) {}
17
18 // NewAdapter is the constructor for Adapter.
19 func NewAdapter() (*Adapter, error) {
20 a := &Adapter{}
21
22 // Call the destructor when the object is released.
23 runtime.SetFinalizer(a, finalizer)
24
25 return a, nil
26 }
27
28 func loadPolicyLine(line *imodel.CasbinRule, model model.Model) {
29 lineText := line.PType
30 if line.V0 != "" {
31 lineText += ", " + line.V0
32 }
33 if line.V1 != "" {
34 lineText += ", " + line.V1
35 }
36 if line.V2 != "" {
37 lineText += ", " + line.V2
38 }
39 if line.V3 != "" {
40 lineText += ", " + line.V3
41 }
42 if line.V4 != "" {
43 lineText += ", " + line.V4
44 }
45 if line.V5 != "" {
46 lineText += ", " + line.V5
47 }
48
49 persist.LoadPolicyLine(lineText, model)
50 }
51
52 // LoadPolicy loads policy from database.
53 func (a *Adapter) LoadPolicy(model model.Model) error {
54 lines, err := GetRules()
55 if err != nil {
56 return err
57 }
58
59 for _, line := range lines {
60 loadPolicyLine(line, model)
61 }
62
63 return nil
64 }
65
66 func savePolicyLine(ptype string, rule []string) imodel.CasbinRule {
67 line := imodel.CasbinRule{}
68
69 line.PType = ptype
70 if len(rule) > 0 {
71 line.V0 = rule[0]
72 }
73 if len(rule) > 1 {
74 line.V1 = rule[1]
75 }
76 if len(rule) > 2 {
77 line.V2 = rule[2]
78 }
79 if len(rule) > 3 {
80 line.V3 = rule[3]
81 }
82 if len(rule) > 4 {
83 line.V4 = rule[4]
84 }
85 if len(rule) > 5 {
86 line.V5 = rule[5]
87 }
88
89 return line
90 }
91
92 // SavePolicy saves policy to database.
93 func (a *Adapter) SavePolicy(model model.Model) error {
94 err := DeleteRules(imodel.CasbinRule{})
95 if err != nil {
96 return err
97 }
98
99 var lines = make([]imodel.CasbinRule, 0)
100
101 for ptype, ast := range model["p"] {
102 for _, rule := range ast.Policy {
103 line := savePolicyLine(ptype, rule)
104 lines = append(lines, line)
105 }
106 }
107
108 for ptype, ast := range model["g"] {
109 for _, rule := range ast.Policy {
110 line := savePolicyLine(ptype, rule)
111 lines = append(lines, line)
112 }
113 }
114
115 return AddRules(lines)
116 }
117
118 // AddPolicy adds a policy rule to the storage.
119 func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
120 line := savePolicyLine(ptype, rule)
121 return AddRule(line)
122 }
123
124 // RemovePolicy removes a policy rule from the storage.
125 func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
126 line := savePolicyLine(ptype, rule)
127 return DeleteRules(line)
128 }
129
130 // RemoveFilteredPolicy removes policy rules that match the filter from the storage.
131 func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
132 line := imodel.CasbinRule{}
133
134 line.PType = ptype
135 filter := []string{}
136 filter = append(filter, "p_type")
137 if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) {
138 line.V0 = fieldValues[0-fieldIndex]
139 filter = append(filter, "v0")
140 }
141 if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) {
142 line.V1 = fieldValues[1-fieldIndex]
143 filter = append(filter, "v1")
144 }
145 if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) {
146 line.V2 = fieldValues[2-fieldIndex]
147 filter = append(filter, "v2")
148 }
149 if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) {
150 line.V3 = fieldValues[3-fieldIndex]
151 filter = append(filter, "v3")
152 }
153 if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) {
154 line.V4 = fieldValues[4-fieldIndex]
155 filter = append(filter, "v4")
156 }
157 if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) {
158 line.V5 = fieldValues[5-fieldIndex]
159 filter = append(filter, "v5")
160 }
161
162 return DeleteRules(line)
163 }
其中, GetRules, DeleteRules, AddRule, AddRules 是實際和 graphql-engin 交互的函數.
middleware
基於 golang gin 的中間件, 目的是將權限檢查和業務 API 的職責分開, 便於開發和維護.
1 package middleware
2
3 import (
4 "bytes"
5 "illuminant/config"
6 "illuminant/logger"
7 "illuminant/middleware/auth"
8 "illuminant/util"
9 "io/ioutil"
10 "strings"
11
12 jwt "github.com/appleboy/gin-jwt/v2"
13 "github.com/casbin/casbin/v2"
14 "github.com/casbin/casbin/v2/model"
15 "github.com/gin-gonic/gin"
16 )
17
18 // NewAuthorizer returns the authorizer, uses a Casbin enforcer as input
19 func NewAuthorizer() gin.HandlerFunc {
20 cnf := config.GetConfig()
21 lg := logger.GetLogger()
22 adp, err := auth.NewAdapter()
23 if err != nil {
24 lg.Err(err).Msg("casbin adapter error")
25 panic(err)
26 }
27
28 m, err := model.NewModelFromString(cnf.Auth.RBACModel)
29 if err != nil {
30 lg.Err(err).Msg("casbin model from string error")
31 panic(err)
32 }
33
34 e, err := casbin.NewEnforcer(m, adp)
35 if err != nil {
36 lg.Err(err).Msg("casbin enforcer error")
37 panic(err)
38 }
39
40 a := &RBACAuthorizer{enforcer: e}
41 return func(c *gin.Context) {
42 if !a.CheckPermission(c) {
43 a.RequirePermission(c)
44 c.Abort()
45 }
46 }
47 }
48
49 // RBACAuthorizer stores the casbin handler
50 type RBACAuthorizer struct {
51 enforcer *casbin.Enforcer
52 }
53
54 // CheckPermission checks the user/method/path combination from the request.
55 // Returns true (permission granted) or false (permission forbidden)
56 func (a *RBACAuthorizer) CheckPermission(c *gin.Context) bool {
57 lg := logger.GetLogger()
58 claims := jwt.ExtractClaims(c)
59
60 method := c.Request.Method
61 path := c.Request.URL.Path
62
63 // a.ReloadPermissions()
64 if strings.Index(path, "/api/v1/graphql") < 0 {
65 return a.RestFullPermission(claims["id"].(string), path, method)
66 }
67
68 // graphql api
69 body, err := c.GetRawData()
70 if err != nil {
71 lg.Err(err).Msg("get body raw data")
72 return false
73 }
74 c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
75
76 return a.GraphqlPermission(claims["id"].(string), body)
77 }
78
79 // RequirePermission returns the 403 Forbidden to the client
80 func (a *RBACAuthorizer) RequirePermission(c *gin.Context) {
81 util.Fail(c, util.AUTH_PERMISSION_DENIED, "權限不足", nil)
82 }
83
84 func (a *RBACAuthorizer) ReloadPermissions() {
85 a.enforcer.LoadPolicy()
86 }
87
88 func (a *RBACAuthorizer) RestFullPermission(userId, path, method string) bool {
89 lg := logger.GetLogger()
90
91 allowed, err := a.enforcer.Enforce(userId, path, method)
92 if err != nil {
93 lg.Err(err).Msg("RestFullPermission check error")
94 return false
95 }
96
97 return allowed
98 }
99
100 func (a *RBACAuthorizer) GraphqlPermission(userId string, body []byte) bool {
101 lg := logger.GetLogger()
102
103 funcs, err := util.GetGraphqlFunc(body)
104 if err != nil {
105 lg.Err(err).Msg("GetGraphqlFunc error")
106 return false
107 }
108
109 for _, f := range funcs {
110 allowed, err := a.enforcer.Enforce(userId, f, "*")
111 if err != nil {
112 lg.Err(err).Msg("GraphqlPermission check error")
113 return false
114 }
115
116 if !allowed {
117 return allowed
118 }
119 }
120
121 return true
122 }
這個中間件同時支持 RestFul 和 Graphql 的接口, 只要將相應的策略存入數據庫即可.
基礎權限 API
基礎的 API 主要有 5 個:
- 獲取某個角色的所有權限
- 給角色增加權限
- 給角色刪除權限
- 給用戶添加角色
- 給用戶刪除角色
有這 5 個基礎 API 之后, 基本就滿足了管理 casbin 中 RBAC 策略的需求了.
總結
上面的功能是在我自己一個正在逐步完善的一個后端快速開發平台上是實現的, 所有代碼都在那個平台上.
即: illuminant的代碼 以及 illuminant 的文檔
上面的代碼位於: middleware 模塊, middleware 模塊下的 auth 模塊 以及 controller 模塊下的 auth_controller.go