Kratos微服务结构完成权鉴 – Casbin

Casbin(github.com/casbin/casb…)是一套拜访操控开源库,致力于帮助杂乱系统解决权限管理的难题。一起也是一个国产开源项目。Casbin采用了元模型的规划思维,既支撑ACL(拜访操控列表),RBAC(基于人物拜访操控),ABAC(基于属性拜访操控)等经典的拜访操控模型,也支撑用户按照本身需求灵活界说权限。Casbin已经被Intel、IBM、腾讯云、VMware、RedHat、T-Mobile等公司开源运用,被Cisco、Verizon等公司闭源运用。详细详见Casbin主页(casbin.org/)。

Casbin由北京大学罗杨博士在2017年4月发起,罗杨博士的研讨方向为云核算拜访操控,现在已发表数十篇相关学术论文,曾经在ICWS、IEEE CLOUD、ICICS等多个尖端学术会议进行论文宣讲。Casbin项目则是其研讨成果的落地。

Casbin最初是一个用Go语言打造的开源轻量级的统一拜访操控结构。现在已逐渐开展,扩展到Go、JavaNode.js、Javascript(React)、Python、PHP、.NET、Delphi、Rust等多种语言,在GitHub开源(github.com/casbin/casb…),主项目在GitHub上已有1.3w+ stars。该项目现在已经有一个上百人稳定的团队进行保护,并在持续不断开展中。

了解Casbin

宏观上,Casbin能够分为三个中心概念:

  1. 恳求(Request);
  2. 模型(Model);
  3. 战略(Policy)。

以上三个中心概念,在官方供给的编辑器里边具有直观的体现:casbin.org/zh/editor,它实质上是一个交互式解说器,你也能够在其间测试模型和战略。

简略来说,用户bob发起的恳求/users;模型供给了断定的规矩,运用RBAC模型;战略,供给了用户账户与人物、资源、行为等的映射关系,可知bob是超级用户人物,能够拜访一切。

从微观上,一个恳求由以下一个三元组组成:

  1. 拜访实体 (Subject);
  2. 拜访资源 (Object);
  3. 拜访办法 (Action)。

断定办法e.Enforce的入参传入三元组,并施行判别。

装备解析

Casbin的装备有两个:模型和战略。其间,战略装备因为经常变化,所以更多时候会被耐久化到数据库傍边。

模型(Access Control Model)

Casbin 的拜访操控模型被笼统成了一个装备文件,这个装备文件由以下五部分组成

以一个最简略的RABC模型举例:

# 恳求界说
[request_definition]
r = sub, obj, act
# 战略界说
[policy_definition]
p = sub, obj, act
# 人物界说
[role_definition]
g = _, _
# 战略效果
[policy_effect]
e = some(where (p.eft == allow))
# 匹配器界说
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

1. 恳求界说 (Request Definition)

界说了在 Enforcer.Enforce 办法中恳求的参数和这些传入参数的顺序;一个基本的 Request 由一个三元组组成:[subject,obj,act]subject 是指拜访的实体,也便是用户;obj 是指恳求的资源,act 是指对这个资源的操作,界说如下:

[request_definition]
r = sub,obj,act

2. 战略界说 (Policy Definition)

界说了拜访战略的模型,其实便是界说了在 Policy Document 中战略规矩的字段称号以及顺序,界说如下:

[policy_definition]
p = sub, obj, act

3. 匹配器界说 (Matcher)

界说了 request 和 policy 之间的匹配规矩,例如:

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

上面的这个匹配规矩便是当恳求的参数 (r.sub, r.obj, r.act) 在界说的战略文件中能找到,说明就匹配成功了,返回的成果会存放在 p.eft 傍边。

4. 战略效果 (Policy Effect)

Effect 能够说是在 Matcher 的匹配成果之上,再次进行逻辑组合判别,判别的成果才是该用户是否有操作权限的成果。

下面是一个例子:

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

上面这个逻辑表达式的意思便是说:当在 matcher匹配的成果中存在任何一个 p.eft == allow 的成果,那么这个逻辑表达式的成果就为 true

5. 人物界说 (Role Definition)(可选)

上面的四个是最基本的,假如运用 RBAC 的 Access model,那么还需求 Role 模型的界说,便是界说用户人物的模型,如下所示:

[role_definition]
g = _, _

战略文档 (Policy Document)

战略文档便是依据 Access Control Model 中界说的 [policy_definition] 生成的一条条 policy rule (战略规矩),比方:

p,alice,data1,read  // 表明:alice 能够 read data1
p,bob,data2,write   // 表明:bob 能够 write data2

假如是运用 RBAC model,那么还会在这个文件中依据 [role_definition] 生成用户和人物的实例,比方:

p,alice,data1,read
p,bob,data2,read
p,data2_admin,data2,read  //表明 data2_admin 能够 read data2
p,data2_admin,data2,write  //表明 data2_admin 能够 write data2
​
g,alice,data2_admin  //表明 alice 是 data2_admin

一个最简略的Casbin的Golang程序

import (
  "github.com/casbin/casbin/v2"
  "fmt"
)
​
func main() {
    e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
        return
   }
  sub := "alice"
  obj := "data1"
  act := "read"
  ok, err := e.Enforce(sub, obj, act)
  if err != nil {
    return
   }
  if ok {
    fmt.Println("matched")
   } else {
    fmt.Println("mismatched")
   }
}

以上的代码,加载了文件形式的装备文件。Enforce办法对写死的一组三元组数据进行断定。

尽管上面的代码看起来很简略。可是,假如不能够了解基础概念,要上手起来还是会比较难。

将Casbin施行封装

Casbin的模型,一般定了之后,基本上都不会变,所以,写在装备文件傍边并没有问题。

而战略是经常变的,所以一般来说,是需求耐久化到数据库傍边去的。官方库傍边供给了许许多多丰富的Adapter完成,我默许完成了一个加载内存战略的完成,当然,要替换成其他完成也是简单的。

package casbin
import (
	"errors"
	"github.com/casbin/casbin/v2/model"
)
type Adapter struct {
	policies map[string]interface{}
}
func newAdapter() *Adapter {
	return &Adapter{
		policies: map[string]interface{}{},
	}
}
func (sa *Adapter) LoadPolicy(model model.Model) error {
	policiesInterface, ok := sa.policies["policies"]
	if ok {
		policies := policiesInterface.([]PolicyRule)
		for _, line := range policies {
			if err := line.LoadPolicyLine(model); err != nil {
				return err
			}
		}
	}
	return nil
}
func (sa *Adapter) SavePolicy(_ model.Model) error {
	return errors.New("not implemented")
}
func (sa *Adapter) AddPolicy(_ string, _ string, _ []string) error {
	return errors.New("not implemented")
}
func (sa *Adapter) RemovePolicy(_ string, _ string, _ []string) error {
	return errors.New("not implemented")
}
func (sa *Adapter) RemoveFilteredPolicy(_ string, _ string, _ int, _ ...string) error {
	return errors.New("not implemented")
}
func (sa *Adapter) SetPolicies(policies map[string]interface{}) {
	sa.policies = policies
}

然后将Casbin封装成一个引擎:

package casbin
import (
	"context"
	stdCasbin "github.com/casbin/casbin/v2"
	"github.com/casbin/casbin/v2/model"
	"github.com/tx7do/kratos-authz/engine"
)
var _ engine.Engine = (*State)(nil)
type State struct {
	model    model.Model
	policy   *Adapter
	enforcer *stdCasbin.SyncedEnforcer
	projects engine.Projects
	wildcardItem              string
	authorizedProjectsMatcher string
}
func New(_ context.Context, opts ...OptFunc) (*State, error) {
	s := State{
		policy:                    newAdapter(),
		projects:                  engine.Projects{},
		wildcardItem:              "*",
		authorizedProjectsMatcher: "g(r.sub, p.sub, p.dom) && (keyMatch(r.dom, p.dom) || p.dom == '*')",
	}
	for _, opt := range opts {
		opt(&s)
	}
	var err error
	if s.model == nil {
		s.model, err = model.NewModelFromString(DefaultRestfullWithRoleModel)
		if err != nil {
			return nil, err
		}
	}
	s.enforcer, err = stdCasbin.NewSyncedEnforcer(s.model, s.policy)
	if err != nil {
		return nil, err
	}
	return &s, nil
}
func (s *State) ProjectsAuthorized(_ context.Context, subjects engine.Subjects, action engine.Action, resource engine.Resource, projects engine.Projects) (engine.Projects, error) {
	result := make(engine.Projects, 0, len(projects))
	var err error
	var allowed bool
	for _, project := range projects {
		for _, subject := range subjects {
			if allowed, err = s.enforcer.Enforce(string(subject), string(resource), string(action), string(project)); err != nil {
				return nil, err
			} else if allowed {
				result = append(result, project)
			}
		}
	}
	return result, nil
}
func (s *State) FilterAuthorizedPairs(_ context.Context, subjects engine.Subjects, pairs engine.Pairs) (engine.Pairs, error) {
	result := make(engine.Pairs, 0, len(pairs))
	project := engine.Project(s.wildcardItem)
	var err error
	var allowed bool
	for _, p := range pairs {
		for _, subject := range subjects {
			if allowed, err = s.enforcer.Enforce(string(subject), string(p.Resource), string(p.Action), string(project)); err != nil {
				return nil, err
			} else if allowed {
				result = append(result, p)
			}
		}
	}
	return result, nil
}
func (s *State) FilterAuthorizedProjects(_ context.Context, subjects engine.Subjects) (engine.Projects, error) {
	result := make(engine.Projects, 0, len(s.projects))
	resource := engine.Resource(s.wildcardItem)
	action := engine.Action(s.wildcardItem)
	var err error
	var allowed bool
	for _, project := range s.projects {
		for _, subject := range subjects {
			if allowed, err = s.enforcer.EnforceWithMatcher(s.authorizedProjectsMatcher, string(subject), string(resource), string(action), string(project)); err != nil {
				return nil, err
			} else if allowed {
				result = append(result, project)
			}
		}
	}
	return result, nil
}
func (s *State) IsAuthorized(_ context.Context, subject engine.Subject, action engine.Action, resource engine.Resource, project engine.Project) (bool, error) {
	if len(project) == 0 {
		project = engine.Project(s.wildcardItem)
	}
	var err error
	var allowed bool
	if allowed, err = s.enforcer.Enforce(string(subject), string(resource), string(action), string(project)); err != nil {
		return false, err
	} else if allowed {
		return true, nil
	}
	return false, nil
}
func (s *State) SetPolicies(_ context.Context, policyMap engine.PolicyMap, _ engine.RoleMap) error {
	s.policy.SetPolicies(policyMap)
	err := s.enforcer.LoadPolicy()
	projects, ok := policyMap["projects"]
	if ok {
		switch t := projects.(type) {
		case engine.Projects:
			s.projects = t
		}
	}
	return err
}

需求留意的是,在这个完成里边,实践上规划的是四元组,而事实上Casbin支撑的是三元组,要支撑四元组有点头疼,所以,我基本上没有支撑,所以看起来会有一些古怪。

将Casbin整合进Kratos

上面的封装有好几个接口,可是要用到的其实只有一个接口:IsAuthorized,咱们将之封装成一个中间件以供Kratos调用。

package middleware
import (
	"context"
	"github.com/go-kratos/kratos/v2/errors"
	"github.com/go-kratos/kratos/v2/middleware"
	"github.com/tx7do/kratos-authz/engine"
)
const (
	reason string = "FORBIDDEN"
)
var (
	ErrUnauthorized  = errors.Forbidden(reason, "unauthorized access")
	ErrMissingClaims = errors.Forbidden(reason, "missing authz claims")
	ErrInvalidClaims = errors.Forbidden(reason, "invalid authz claims")
)
func Server(authorizer engine.Authorizer, opts ...Option) middleware.Middleware {
	o := &options{}
	for _, opt := range opts {
		opt(o)
	}
	if authorizer == nil {
		return nil
	}
	return func(handler middleware.Handler) middleware.Handler {
		return func(ctx context.Context, req interface{}) (interface{}, error) {
			var (
				allowed bool
				err     error
			)
			claims, ok := engine.AuthClaimsFromContext(ctx)
			if !ok {
				return nil, ErrMissingClaims
			}
			if claims.Subject == nil || claims.Action == nil || claims.Resource == nil {
				return nil, ErrInvalidClaims
			}
			var project engine.Project
			if claims.Project == nil {
				project = ""
			} else {
				project = *claims.Project
			}
			allowed, err = authorizer.IsAuthorized(ctx, *claims.Subject, *claims.Action, *claims.Resource, project)
			if err != nil {
				return nil, err
			}
			if !allowed {
				return nil, ErrUnauthorized
			}
			return handler(ctx, req)
		}
	}
}

中间件的完成很简略,所以不再赘述。

详细的运用办法能够在单元测试里边详细看到,另外我还开源了一个CMS也有实践使用。

相关代码

相关代码已经开源,欢迎拉取参考学习:

  • github.com/tx7do/krato…
  • gitee.com/tx7do/krato…

使用方面的代码,我开源了一个简略的CMS,完好的使用可在傍边找到:

  • github.com/tx7do/krato…
  • gitee.com/tx7do/krato…

参考资料

  • Casbin – Github
  • Casbin – 官方网站
  • Casbin – 交互式解说器
  • 授权结构 Casbin
  • 集成Casbin进行拜访权限操控