Go语言学习笔记——访问权限控制框架casbin


Golang访问权限控制框架casbin

官方文档

https://casbin.org/docs/zh-CN/overview

casbin简介

Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。支持的语言也很多,例如:go、java、node.js、python等等.

Casbin 是什么?

Casbin 可以:

  1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}
  2. 具有访问控制模型model和策略policy两个核心概念。
  3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  4. 支持内置的超级用户 例如:rootadministrator。超级用户可以执行任何操作而无需显式的权限声明。
  5. 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*

Casbin 不能:

  1. 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
  2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。

访问控制模型

控制访问模型有哪几种?我们需要先来了解下这个。

  • 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就是用这么一种方法。

  • MAC(强制访问控制)

    强制访问控制和DAC相反,它不将某些权限下放给用户,而是在更高维度(比如操作系统)上将所有的用户设置某些策略,这些策略是需要所有用户强制执行的。这种访问控制也是基于某些安全因素考虑。

简单使用

安装

go get github.com/casbin/casbin
go get github.com/casbin/casbin/v2 // 可能兼容还有问题

go get github.com/casbin/gorm-adapter

go get github.com/go-sql-driver/mysql

go get github.com/jinzhu/gorm

创建model配置文件

model.conf

# 请求定义
[request_definiation]
r = sub,obj,act
# sub ——> 想要访问资源的用户角色(Subject)——请求实体
# obj ——> 访问的资源(Object)
# act ——> 访问的方法(Action: get、post...)


# 策略定义
# 策略(.csv文件p的格式,定义的每一行为policy rule;p,p2为policy rule的名字。)
[policy_definiation]
p = sub,obj,act
# p2 = sub,act 表示sub对所有资源都能执行act


# 组定义
[role_definiation]
g = _, _
# g = _,_定义了用户——角色,角色——角色的映射关系,前者是后者的成员,拥有后者的权限。
# _,_表示用户,角色/用户组


# 策略效果
[policy_effect]
e = some(where(p.eft = allow))
# 上面表示有任意一条 policy rule 满足, 则最终结果为 allow;p.eft它可以是allow或deny,它是可选的,默认是allow

# 匹配器
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

# 上面模型文件规定了权限由sub,obj,act三要素组成,只有在策略列表中有和它完全相同的策略时,该请求才能通过。

policy文件(policy.csv)

p,zxp,data1,read
p,zhang,data2,write

代码实现

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() {
	//首先创建一个casbin.Enforcer对象,加载模型文件model.conf和策略文件policy.csv,调用其		Enforce方法来检查权限
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }
  check(e, "zxp", "data1", "read")
  check(e, "zhang", "data2", "write")
  check(e, "zxp", "data1", "write")
  check(e, "zxp", "data2", "read")
}

测试结果:

zxp CAN read data1
zhang CAN write data2
zxp CANNOT write data1
zxp CANNOT read data2

超级管理员实现

[matchers]
e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

g = _, _的用法

g = _,__定义了用户——角色或角色——角色的映射关系,前者是后者的成员,拥有后者的权限。

model文件配置为

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

policy文件配置为

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, zxp, admin
g, zhang, developer

admin对数据datareadwrite权限,而developer对数据data只有read权限。

zxp拥有admin的权限所以可读可写 zhang只能读

多个g _ , _

model文件配置改为:

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

[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
# 只改这两个,其他不变

policy文件举例如下:

p, admin, data1, read
p, admin, data1, write
p, admin, data2, read
p, admin, data2, write
p, developer, data2, read
p, developer, data2, write
p, developer, data1, read
g, zxp, admin
g, zhang, developer
g2, data1.data, data1
g2, data2.data, data2

zxp属于admin所以对data1data2可读可写

zhang属于developer所以对data1可读。对data2可读可写

data1.data属于data1,所以zxp可读可写,zhangdata1.data可读

data2.data属于data2,所以zhang可读可写

多层角色

model文件不用修改

policy文件举例如下:

p, senior, data, write
p, developer, data, read
g, zxp, senior
g, senior, developer
g, zhang, developer

seniordatawrite权限

developer只有read权限,同时senior也是developer(前者拥有后者的权限),所以senior也继承其read权限。

zxp可读可写data

domain领域

model文件修改如下

[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _,_,_
# g2 = _,_,_ 表示用户, 角色/用户组,(也就是租户)


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

policy文件举例如下:

p, admin, dom1, data1, read
p, admin, dom2, data2, read
g, zxp, admin, dom1

dom1中,只有admin可以读取数据data1

dom2中,只有admin可以读取数据data2

zxpdata1中是admin,但是在data2中不是。

代码实现

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", "dom1", "data1", "read")
  check(e, "dajun", "dom2", "data2", "read")
}

动态控制可读可写

根据时间控制

model文件修改如下

[matchers]
m = r.sub.Hour >= 5 && r.sub.Hour < 20 || r.sub.Name == r.obj.Owner> 

注意:该模式下不需要policy文件配置

代码实现

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)
  }

  //r.sub.Hour < 18 || r.sub.Name == r.obj.Owner 这两个满足一个就可读
  o := Object{"data", "zxp"}
    
    
  s1 := Subject{"zxp", 10}
  check(e, s1, o, "read")//可读

  s2 := Subject{"zhang", 10}
  check(e, s2, o, "read")//可读

  s4 := Subject{"zhang", 20}
  check(e, s4, o, "read")//不可读
    
}

模式存储

动态初始化model文件(代码内实现model)

package main

import (
	"fmt"
	"log"

	fileadapter "github.com/casbin/casbin/persist/file-adapter"
	"github.com/casbin/casbin/v2"
	"github.com/casbin/casbin/v2/model"
)

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() {
	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, "zxp", "data1", "read")
	check(e, "zhang", "data2", "write")
	check(e, "zxp", "data1", "write")
	check(e, "zxp", "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)
// }

策略储存policy.csv实际运用

使用Gorm Adapter。先连接到数据库,执行下面的SQL

CREATE DATABASE IF NOT EXISTS casbin;

USE casbin;

CREATE TABLE IF NOT EXISTS casbin_rule (
  p_type VARCHAR(100) NOT NULL,
  s0 VARCHAR(100),
  s1 VARCHAR(100),
  s2 VARCHAR(100)
);

INSERT INTO casbin_rule VALUES
('p', zxp', 'data1', 'read', '', '', ''),
('p', 'zhang', 'data2', 'write', '', '', ''); 

代码实现

package main

import (
	"fmt"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter"
)

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", "root:12345@tcp(127.0.0.1:3306)/test_db", false)
	e, _ := casbin.NewEnforcer("./model.conf", a)

	check(e, "zxp", "data1", "read")    //可读
	check(e, "zhang", "data2", "write") //可写

}

版权声明:本文为qq_39280718原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。