分享

工作流引擎-权限系统设计

 F2967527 2024-09-27

电子书地址:https://workflow-engine-book.

权限系统,例如:ACL[1], RBAC[2], ABAC[3]等,它们本质上,都是在解决谁(Subject)可以对什么资源(Object)进行什么操作(Action)这个问题。

ACL权限模型

Access Control List(ACL,访问控制列表)是一种常见的权限管理技术,它用于定义谁可以访问特定资源以及可以执行的操作。ACL通常用于文件系统、数据库、网络设备等环境中,用于控制对资源的访问。

一个ACL是一个列表,其中每一项都包含一个主体(如用户或用户组)和一组权限。主体可以是单个用户、用户组或者所有用户(通常表示为“所有人”或“公共”)。权限则定义了主体可以对资源执行的操作,如读取、写入、执行等。

例如在API设计时应用ACL模型,权限可以这样设计:

用户(Subject)操作(Action)资源(Object)
AliceGET/article
AlicePUT/article
BobGET/article
BobDELETE/article

上面的ACL策略表示Alice可以去访问和修改文章,Bob可以访问和删除文章,其他人则没有任何权限。

同样,在Linux系统中,文件的访问权限基于ACL(Access Control List,访问控制列表)设计。

Linux文件系统中的每个文件或目录都有一个关联的ACL,用于定义三类主体对文件的访问权限:文件所有者(user)、文件所属的用户组(group)以及其他用户(other)。每一类主体可以拥有读(r)、写(w)和执行(x)三种权限。

Linux文件的ACL通常表示为一个由10个字符组成的字符串,如“-rw-r--r--”。这个字符串从左到右分为四部分:

  1. 1. 第一个字符表示文件类型:'-'表示普通文件,'d'表示目录,'l'表示符号链接等。

  2. 2. 接下来的三个字符表示文件所有者的权限:'r'表示可读,'w'表示可写,'x'表示可执行。没有某个权限的话,对应的位置会被标为'-'。

  3. 3. 再接下来的三个字符表示文件所属用户组的权限,同样用'r'、'w'和'x'表示。

  4. 4. 最后三个字符表示其他用户的权限,也是用'r'、'w'和'x'表示。

例如,下面的resource_2文件ACL字符串为“-rw-r-----”,表示:

  1. 1. 这是一个普通文件(-)。

  2. 2. 文件所有者具有读(r)和写(w)权限。

  3. 3. 文件所属的用户组具有读(r)权限。

  4. 4. 其他用户没有任何权限。

[root@VM-32-12-tencentos test]# ll
total 4
drwxr-xr-x 2 root root 4096 Dec  9 12:59 resource_1
-rw-r----- 1 root root    0 Dec  9 12:59 resource_2

在Linux系统中,可以使用chmod命令修改文件的ACL。例如,要将一个文件的权限设置为所有者可读写,用户组和其他用户只可读,可以执行以下命令:

chmod 644 filename

此外,Linux还支持扩展的ACL(Extended Access Control List),它允许为特定用户或用户组分配更细粒度的权限。扩展ACL可以使用getfaclsetfacl命令进行查询和设置。例如,要为用户Alice添加对文件的读写权限,可以执行以下命令:

setfacl -m u:alice:rw resource_2

通过getfacl可以查看到设置的ACL策略:

[root@VM-32-12-tencentos test]# getfacl resource_2
# file: resource_2
# owner: root
# group: root
user::rw-
user:alice:rw-
group::r--
mask::rw-
other::---

从上面的举例,可以看到ACL提供了一种灵活的权限管理机制,可以支持各种复杂的访问控制需求。

然而,管理大量的ACL可能会变得复杂,特别是在大型系统中。像前面API接口设计使用ACL模型,那么意味着后续每来一个新客户都需要添加对应的策略,随着新用户的增多,这个ACL策略表也会变得越来越大很难维护。

因此,许多系统还提供了角色或属性等其他机制,以简化权限管理和提高安全性。

RBAC权限模型

RBAC的基本组成

尽管ACL提供了一种灵活的权限管理机制,但在实际应用中,它也存在一些问题,例如:

  1. 1. 管理复杂性:当系统中的资源和用户数量较大时,管理大量的ACL可能变得非常复杂。为每个文件或资源分配权限可能会导致管理负担加重,尤其是当需要修改或更新权限时。

  2. 2. 难以追踪和审计:由于权限是分散在各个资源的ACL中的,追踪和审计用户的访问权限可能变得困难。例如,要查找具有特定权限的所有用户,可能需要检查所有资源的ACL。

  3. 3. 权限维护困难:当用户的角色或职责发生变化时,可能需要修改多个ACL以更新用户的权限。这不仅耗时,而且容易出错。

  4. 4. 缺乏抽象和封装:ACL直接将权限分配给用户或用户组,缺乏对权限的抽象和封装。这可能导致权限管理变得繁琐和低效。

RBAC(Role-Based Access Control,基于角色的访问控制)模型通过引入角色的概念来解决这些问题,使得权限管理更加简单、高效和可维护。在RBAC模型中,权限不再直接分配给用户,而是分配给角色。用户根据需要分配一个或多个角色,从而间接获得角色所包含的权限。

图片

以下是RBAC模型的关键组成部分:

  1. 1. 用户(Users):用户是系统中的实体,如人员、服务或应用程序。用户需要访问系统资源以完成特定任务。

  2. 2. 角色(Roles):角色是一组相关权限的集合,通常与特定职责或职位相对应。例如,管理员、普通用户和访客等。角色应该具有明确的职责和权限范围,遵循最小权限原则,即只分配角色所需的最小权限。

  3. 3. 权限(Permissions):权限是对资源的访问或操作的授权。例如,对文件的读、写和删除权限。在RBAC模型中,权限分配给角色,而不是直接分配给用户。

  4. 4. 资源(Resources):资源是系统中需要受到保护的对象,如文件、数据库表、服务等。资源可以根据需要分配给角色,以控制用户对资源的访问和操作。

RBAC的优点

通过以上步骤,可以实现基于RBAC模型的权限设计,这个模型可以弥补前面ACL模型的各种缺陷,具体来说就是有以下这些优势:

  1. 1. 简化管理:通过将权限分配给角色而非直接分配给用户,RBAC模型简化了权限管理。管理员只需要管理角色和用户与角色之间的关系,而不是为每个用户分配权限。

  2. 2. 更易于追踪和审计:在RBAC模型中,权限集中在角色中,更容易追踪和审计用户的访问权限。例如,要查找具有特定权限的所有用户,只需检查与该权限相关的角色,然后查找分配了这些角色的用户。

  3. 3. 权限维护更简单:当用户的角色或职责发生变化时,只需修改用户的角色分配,而无需逐个修改资源的ACL。这使得权限维护更加简单和高效。

  4. 4. 权限抽象和封装:RBAC模型通过角色对权限进行抽象和封装,使得权限管理更加模块化和结构化。这有助于提高权限管理的可维护性和可扩展性。

图片

如上图所示,一个基于RBAC的权限系统可以分成如下三大块进行管理:

权限管理

前面,我们说了权限管理本质上都是在解决谁(Subject)可以对什么资源(Object)进行什么操作(Action)这个问题。

资源对应的就是限制用户能访问的数据范围,操作就是限制用户能对这些资源做哪些行为。

这两个都是在后台做的一个权限限制,前端用户只能通过错误提示来得知。但在实际设计权限管理系统时,为了更好的用户体验,我们还会加多一个前端的权限控制,包括:菜单、页面、按钮的控制显示,如果没有对应权限就不显示相应的菜单、页面和按钮。没有权限的的操作,前端就直接不可见,这样用户体验上会更好。

所以,一个权限管理分别从如下方面来做限制:

图片

前端权限在实际实现时,一般通过控制菜单的可视范围来配置,比较少具体到页面和按钮这种细粒度的控制。

例如下面某个系统,一般系统左侧是菜单栏,包含一级或二级菜单,而对应到角色权限配置页面上,则是对应把菜单的所有层级结构展示出来,然后供管理员进行配置。对应的菜单会关联API操作权限。

图片 图片

所以在配置每个API的时候,还需要去整理关联每个API归属于哪个菜单,可以通过#号来区分层级。一般在设计接口路径时,每个路径的中间名称可以对应一个菜单,例如下面的/v1/work路径对应的Dashboard#工作台,/v1/index路径对应的是Dashboard#首页。这样API设计和菜单关联逻辑上保持一致,更容易后期整理维护。

API权限菜单
/v1/user/list用户管理
/v1/user/edit用户管理
/v1/index/indexDashboard#首页
/v1/work/overviewDashboard#工作台

那么角色在关联菜单的时候,实际上就关联了对应的API接口操作权限。

角色管理

在RBAC(Role-Based Access Control,基于角色的访问控制)模型中,角色管理是一个关键的组成部分。角色管理主要涉及角色的创建、分配、修改和删除等操作。以下是RBAC模型中角色管理的主要步骤:

  1. 1. 角色定义:首先,需要定义系统中的角色。角色通常对应于组织中的职位或职责,如“管理员”、“编辑”、“访客”等。每个角色都应该有一个明确的职责和权限范围。

  2. 2. 权限分配:为每个角色分配一组相应的权限。权限通常是对系统资源的访问或操作的授权,如“读”、“写”、“删除”等。

  3. 3. 用户与角色关联:将用户分配到一个或多个角色。用户通过角色获得访问系统资源的权限。

  4. 4. 角色修改和删除:当组织或业务需求发生变化时,可能需要修改或删除角色。例如,当一个角色的职责发生变化时,可能需要修改该角色的权限;当一个角色不再需要时,可以删除该角色。

在实际应用中,角色管理通常需要配合用户管理和权限管理一起使用。例如,当新用户加入系统时,需要为其分配适当的角色;当系统资源或权限策略发生变化时,可能需要更新角色的权限。

在进行角色管理时,需要采取如下的安全原则进行合理设计来提高系统安全性:

  • · 最小权限原则:RBAC可以将角色配置成其完成任务所需的最小的权限集合

  • · 责任分离原则:可以通过调用相互独立互斥的角色共同完成敏感的任务,例如要求一个记账员和财务管理员共同参与统一过账操作

  • · 数据抽象原则:可以通过权限的抽象来体现,例如财务操作用借款、存款等抽象权限,而不是使用典型的读、写等执行权限

用户管理

用户管理主要做两方面工作流:

  • · 确定用户在哪个组织

  • · 确定用户在这个组织里有哪些权限(即绑定什么角色)

图片

RBAC的四个层次

在RBAC模型中,为了更好地描述和理解不同程度的角色管理和访问控制,研究者将RBAC模型分为四个层次:RBAC0、RBAC1、RBAC2和RBAC3。这些层次从简单到复杂,逐步增加了角色管理和访问控制的功能。

RBAC0

  1. 1. RBAC0:RBAC0是RBAC模型的最基本层次,它包括用户(User)、角色(Role)和权限(Permission)三个基本元素。在RBAC0中,用户通过分配角色来获得权限。RBAC0模型关注于将权限分配给角色,以及将角色分配给用户。它不涉及角色之间的关系,也不包括角色继承。这种在大部分系统上是最常见的设计,可以满足绝大部分系统的权限模块设计需求。

图片

RBAC1

RBAC1:RBAC1在RBAC0的基础上引入了角色继承(Role Hierarchy)机制。角色继承允许一个角色继承另一个角色的权限。这种层次化的角色结构有助于简化权限管理,使权限分配更加结构化和模块化。

角色继承的主要特点如下:

  1. 1. 层次化:角色继承允许创建具有层次结构的角色。在这种结构中,一个角色可以继承一个或多个父角色的所有权限。这有助于组织和管理角色,使权限分配更加结构化和模块化。

  2. 2. 权限累积:当一个角色继承另一个角色时,它将自动获得被继承角色的所有权限。这意味着在分配权限时,只需为每个角色分配特定的权限,而无需重复分配共享的权限。

  3. 3. 灵活性:角色继承提供了一种灵活的方式来管理和分配权限。当需要调整权限时,只需修改角色继承关系,而无需逐个修改用户的权限。这使得权限管理更加灵活和高效。

图片

举个例子,假设我们有以下角色:

  • · 一般员工(Employee)

  • · 经理(Manager)

  • · 系统管理员(System Administrator)

在这个例子中,我们可以使用角色继承来实现以下权限分配:

  • · 一般员工(Employee)具有基本的访问权限,如查看文件、发送电子邮件等。

  • · 经理(Manager)继承一般员工(Employee)的所有权限,并具有额外的权限,如审批报告、管理项目等。

  • · 系统管理员(System Administrator)继承经理(Manager)的所有权限,并具有额外的权限,如管理用户、配置系统等。

RBAC2

RBAC2:RBAC2在RBAC1的基础上引入了约束(Constraint)机制。约束用于限制用户与角色、角色与权限之间的关联,以增强访问控制的安全性和灵活性。约束可以是静态的(例如,一个用户最多可以分配两个角色),也可以是动态的(例如,一个用户不能同时拥有某两个互斥的角色)。

图片

约束可以分为静态约束和动态约束,以下是一些常见的RBAC2约束类型及示例:

  1. 1. 互斥角色(Mutually Exclusive Roles):互斥角色约束要求一个用户不能同时拥有某些特定的角色。这可以防止潜在的冲突或滥用权限。例如,在一个银行系统中,一个用户可能不能同时拥有“出纳员”和“审计员”的角色,以防止内部欺诈。

  2. 2. 基数约束(Cardinality Constraint):基数约束限制了分配给用户的角色数量或分配给角色的权限数量。这有助于遵循最小权限原则,降低潜在的安全风险。例如,一个用户最多可以分配两个角色;或者一个角色最多可以拥有五个权限。

  3. 3. 权责分离约束(Separation of Duties,SoD):权责分离约束要求将潜在冲突的任务分配给不同的角色。这有助于防止滥用权限和内部欺诈。例如,在一个采购系统中,“采购申请者”和“采购审批者”应该是两个独立的角色,以确保采购过程的透明和公正。

  4. 4. 前置角色约束(Prrequisite Roles Constraint):只有当用户已是角色 B 的成员时,才能将其分配给角色 A。例如要经历过主管的角色之后,才能晋级总监角色。

在实际应用中,可以根据具体的业务需求和安全策略来定义和实施RBAC2约束。这些约束可以通过编程逻辑或数据库规则等方式来实现。

RBAC3

RBAC3 = RBAC1+RBAC2,既有角色继承机制也有约束机制

图片

总之,RBAC0、RBAC1、RBAC2和RBAC3是RBAC模型的四个层次,它们从简单到复杂,逐步增加了角色管理和访问控制的功能。在实际应用中,可以根据具体的业务需求和场景来选择合适的RBAC层次,对于大部分中小系统来说,RBAC0是够用的了。

案例改进

例如前面ACL的例子改成RBAC的模型实现,可以这样设计:

首先是设计角色关联的权限:

角色(Role)操作(Action)资源(Object)
EditorGET/article
EditorPUT/article
EditorDELETE/article
ViewerGET/article

然后是关联角色和用户:

用户(Subject)角色(Role)
AliceEditor
BobViewer

这样Alice就拥有Editor这个角色的所有权限,而Editor角色可以实现对文章的读取、更改和删除,而Bob是Viewer角色,则只能查看文章。

ABAC权限模型

尽管RBAC(Role-Based Access Control,基于角色的访问控制)模型相对于ACL模型简化了权限管理,提高了安全性和可维护性,但它也存在一些缺点或局限性:

  1. 1. 静态角色:RBAC模型中的角色通常是预先定义好的,这可能导致在面对复杂和动态的访问控制需求时,灵活性较低。例如,如果需要根据时间、地点或其他上下文信息来控制访问权限,RBAC模型可能难以满足需求。

  2. 2. 角色爆炸:在某些场景下,为了满足细粒度的访问控制需求,可能需要创建大量的角色,这会导致角色管理变得复杂,也称为“角色爆炸”。

  3. 3. 缺乏属性支持:RBAC模型主要依赖于角色来控制访问权限,而不支持基于用户、资源或环境等属性的访问控制。这可能导致在需要支持基于属性的访问控制的场景中,RBAC模型难以满足需求。

ABAC(Attribute-Based Access Control,基于属性的访问控制)模型正是为了解决这些问题而提出的。ABAC模型允许基于用户、资源、操作和环境等多个属性来控制访问权限。这些属性可以是静态的(例如用户的部门、资源的类型)或动态的(例如当前时间、用户的位置)。

ABAC模型通过引入属性和动态访问控制策略,解决了RBAC模型在复杂和动态访问控制场景中的缺点和局限性,使得权限管理更加灵活和高效。然而,实现ABAC模型可能相对复杂,需要对属性和策略进行管理和维护。

PERM元模型

PERM(Policy, Effect, Request, Matchers)建模语言(PERM modeling language, 简称PML),是一种用于访问控制的模型。模型中的每个元素都有其特定的含义和作用:

图片

  1. 1. Request(请求):请求是用户试图执行的操作,通常一个基础的请求是一个三元组,包括用户身份(subject)、目标资源(object)和操作类型(action)信息,即:r = {sub, obj, act}。例如,用户试图删除一个文件,这就构成了一个请求。

  2. 2. Policy(策略):策略是定义谁可以执行哪些操作,或者在哪些条件下可以访问哪些资源的规则,即:p = {sub, obj, act}或p={sub, obj, act, eft}。例如,策略可能会规定“管理员可以删除所有文件”,或者“只有在工作时间内,员工才能访问公司数据库”。

  3. 3. Matchers(匹配器):匹配器是用来比较请求(Request)和策略(Policy)的工具,如果请求和策略匹配,那么将执行策略定义的效果,匹配器可以是简单的比较操作,也可以是复杂的逻辑表达式。例如:m = r.sub == p.sub && r.action == p.action && r.resource == p.resource,这个简单的匹配规则表示,如果请求的参数(subject, object, action)能在策略中找到对应的匹配结果,则返回策略结果p.eft,其中策略结果保存在p.eft中。

  4. 4. Effect(效果):效果描述了当策略匹配请求时应该执行的操作,通常有两种效果:“允许”和“拒绝”。例如:e = some(where(p.eft == allow)),表示如果策略匹配结果理由有某些结果是“允许”,那么最终结果返回真。另一个例子:e = some(where (p.eft == allow)) && !some(where (p.eft == deny)),这个组合的逻辑表示:如果有策略匹配结果为允许,且没有策略匹配结果是拒绝时,结果返回真。也就是说,当匹配的策略都是允许是,结果为真;如果有任何一个拒绝,则结果为假。

如下图,访问控制的过程通常是这样的:首先,用户发出一个请求,然后系统使用匹配器将请求与策略进行比较,如果请求匹配策略,那么将执行策略定义的效果(允许或拒绝)。

图片

PERM模型的优点是它非常灵活,可以支持各种复杂的访问控制需求。同时,它也支持动态的访问控制,因为策略和匹配器都可以根据需要进行修改。

关于该模型的具体设计细节,可以参考原论文:PML: 基于解释器的Web服务访问控制策略语言[1]基于元模型的访问控制策略规范语言(中文)[2]

Casbin框架应用实践

Casbin是一个强大的、高效的开源访问控制库,用于Golang、Java、Node.js、PHP等多种编程语言。它支持多种访问控制模型,如ACL(访问控制列表)、RBAC(基于角色的访问控制)和ABAC(基于属性的访问控制),可以帮助开发者轻松地实现对应用程序中资源的访问控制。

前面介绍的是各个权限模型的原理,以及Casbin开源框架的理论基础:PERM元模型。接下来的内容,我们讲介绍和使用Casbin框架来实现前面的几种权限模型。

下图是Casbin的原理说明:

图片

Casbin的ACL实现

如下是一个ACL模型的定义:

# Request definition
[request_definition]
r=sub, obj, act

# Policy definition
[policy_definition]
p=sub, obj, act

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

# Matchers
[matchers]
m= r.sub== p.sub&& r.obj == p.obj && r.act == p.act
  • · request_definition 是系统的查询模板。例如,请求 alice, write, data1 可以解释为 '主体 Alice 能否对对象'data1'执行'写'操作?

  • · policy_definition 是系统的赋值模板。例如,通过创建 policy alice, write, data1,就等于为主体 Alice 分配了在对象 'data1 '上执行 '写 '操作的权限。

  • · policy_effect 定义了策略的效果。

  • · matchers使用 r.sub == p.sub && r.obj == p.obj && r.act == p.act 条件将请求与策略进行匹配。

下面举例一个策略来检测这个ACL模型:

p, alice, data1, read
p, bob, data2, write

这个策略表示:

  • · alice可以读取data1

  • · bob可以编写data2

下图是ACL模型中policy和request的匹配过程:

图片

上面的例子,我们通过casbin的golang库实现如下:

首先安装Casbin库:

go get -u github.com/casbin/casbin/v2

接着,我们创建一个名为main.go的文件,用于实现ACL权限模型。

package main

import(
'fmt'

'github.com/casbin/casbin/v2'
'github.com/casbin/casbin/v2/model'
)

func main(){
// 1. 初始化一个Casbin的Enforcer,这里我们直接使用字符串来定义模型
    m, _ := model.NewModelFromString(`
        [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
    `
)

    enforcer, _ := casbin.NewEnforcer(m)

// 2. 添加策略
// 用户alice可以访问数据1的读操作
    enforcer.AddPolicy('alice','data1','read')
// 用户bob可以访问数据2的写操作
    enforcer.AddPolicy('bob','data2','write')

// 3. 检查访问权限
// 检查alice是否可以访问data1的读操作
    ok, _ := enforcer.Enforce('alice','data1','read')
    fmt.Printf('Alice can read data1: %v\n', ok)

// 检查bob是否可以访问data2的写操作
    ok, _ = enforcer.Enforce('bob','data2','write')
    fmt.Printf('Bob can write data2: %v\n', ok)

// 检查alice是否可以访问data2的写操作
    ok, _ = enforcer.Enforce('alice','data2','write')
    fmt.Printf('Alice can write data2: %v\n', ok)
}

在这个示例中,我们首先定义了一个简单的ACL模型,然后添加了两条策略:

  • · alice可以访问data1的读操作

  • · bob可以访问data2的写操作。最后,我们检查了用户的访问权限。

运行这个程序,你将看到以下输出:

Alice can read data1: true
Bob can write data2: true
Alice can write data2: false

Casbin的RBAC实现

Casbin 支持约束规则,这些规则用于在访问控制过程中对权限进行更细粒度的控制。约束规则的定义通常使用 c 标识符。

在 Casbin 中,约束规则的定义为:

[policy_definition]
c = _, _, constraint_name

其中,constraint_name 是约束的名称,代表对应的约束规则。现在,我来介绍一些常见的约束规则以及它们的示例:

  1. 1. 日期约束: 允许用户只在特定日期范围内访问某个资源。

[policy_definition]
c = _, _, date

在这个例子中,date 可以是资源可以访问的日期范围,例如 2022-01-01 to 2022-12-31

  1. 1. 时间约束: 允许用户只在特定时间段内访问某个资源。

[policy_definition]
c = _, _, time

在这个例子中,time 可以是资源可以访问的时间范围,例如 9:00 AM to 5:00 PM

  1. 1. 数量约束:

[policy_definition]
c = _, _, max_operations

在这个例子中,max_operations 可以是用户对资源执行操作的最大次数,例如 10

  1. 1. 自定义约束:

允许用户定义自己的约束规则,例如根据用户属性、环境变量等条件限制访问。

[policy_definition]
c = _, _, custom_constraint

在这个例子中,custom_constraint 是用户自定义的约束规则。

当 Casbin 执行权限检查时,会根据加载的策略和匹配的请求条件,结合约束规则来判断是否允许访问。实际使用时,约束规则的定义和语义是根据业务需求而定的,用户可以根据具体情况定义和使用约束规则。

我们设计一个简单的RBAC模型,如下:

[request_definition]
r =sub, act, obj

[policy_definition]
p =sub, act, obj

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

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

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

我们创建一个名为main.go的文件,用于实现RBAC权限模型。

package main

import(
'fmt'

'github.com/casbin/casbin/v2'
'github.com/casbin/casbin/v2/model'
)

func main(){
// 1. 初始化一个Casbin的Enforcer,这里我们直接使用字符串来定义模型
    m, _ := model.NewModelFromString(`
        [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

    `
)

    enforcer, _ := casbin.NewEnforcer(m)

// 2.定义角色的策略
//admin角色可以访问data1的读写操作
    enforcer.AddPolicy('admin','data1','read')
    enforcer.AddPolicy('admin','data1','write')
// member角色只可以访问data2的读操作
    enforcer.AddPolicy('member','data2','read')

// 3. 给用户添加角色
// 用户alice拥有admin角色
    enforcer.AddGroupingPolicy('alice','admin')
// 用户bob拥有member角色
    enforcer.AddGroupingPolicy('bob','member')

// 4. 检查访问权限
// 检查alice是否可以访问data1的读操作
    ok, _ := enforcer.Enforce('alice','data1','read')
    fmt.Printf('Alice can read data1: %v\n', ok)

// 检查alice是否可以访问data1的写操作
    ok, _ = enforcer.Enforce('alice','data1','write')
    fmt.Printf('Alice can write data1: %v\n', ok)

// 检查bob是否可以访问data2的读操作
    ok, _ = enforcer.Enforce('bob','data2','read')
    fmt.Printf('Bob can read data2: %v\n', ok)

// 检查bob是否可以访问data1的写操作
    ok, _ = enforcer.Enforce('bob','data1','write')
    fmt.Printf('Bob can write data1: %v\n', ok)
}

下面是这个代码示例的解释:

  1. 1. 首先,我们使用字符串定义了一个RBAC模型。在这个模型中,我们定义了请求(r = sub, obj, act),策略(p = sub, obj, act),角色的定义(g = _, _),策略效果(e = some(where (p.eft == allow)))和匹配表达式(m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act)。

  2. 2. 然后,我们初始化了一个Casbin的Enforcer。

  3. 3. 添加了角色和策略。我们将alice添加到admin角色,将bob添加到member角色。然后定义admin角色可以对data1进行读写操作,member角色只可以对data2进行读操作。

  4. 4. 使用Enforce方法检查用户的访问权限。在这个示例中,我们检查了alice和bob对data1和data2的读写权限。

运行这个程序,你将看到以下输出:

Alice can read data1: true
Alice can write data1: true
Bob can read data2: true
Bob can write data1: false

Casbin的ABAC实现

我们创建一个名为main.go的文件,用于实现ABAC权限模型。

package main

import(
'fmt'

'github.com/casbin/casbin/v2'
'github.com/casbin/casbin/v2/model'
)

// User 定义用户属性
typeUserstruct{
Namestring
Ageint
Rolestring
}

// Resource 定义资源属性
typeResourcestruct{
Namestring
Locationstring
}

// 定义一个自定义的策略函数
func policyFunc(args ...interface{})(interface{},error){
    user := args[0].(*User)
    resource := args[1].(*Resource)
    action := args[2].(string)

// 如果用户角色是admin,或者用户角色是owner并且资源在USA,或者用户年龄大于18并且操作是read,则允许访问
if user.Role=='admin'||(user.Role=='owner'&& resource.Location=='USA')||(user.Age>18&& action =='read'){
returntrue,nil
}

returnfalse,nil
}

func main(){
// 使用字符串定义模型
    m, _ := model.NewModelFromString(`
        [request_definition]
        r = sub, obj, act

        [policy_definition]
        p = sub, obj, act

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

        [matchers]
        m = PolicyFunc(r.sub, r.obj, r.act)
    `
)

// 初始化一个Casbin的Enforcer
    enforcer, _ := casbin.NewEnforcer(m)

// 添加自定义策略函数
    enforcer.AddFunction('PolicyFunc', policyFunc)

// 创建用户和资源
    userAlice :=&User{Name:'Alice',Age:20,Role:'admin'}
    userBob :=&User{Name:'Bob',Age:16,Role:'member'}
    resourceData1 :=&Resource{Name:'data1',Location:'USA'}

// 检查访问权限
    ok, _ := enforcer.Enforce(userAlice, resourceData1,'read')
    fmt.Printf('Alice can read data1: %v\n', ok)

    ok, _ = enforcer.Enforce(userBob, resourceData1,'read')
    fmt.Printf('Bob can read data1: %v\n', ok)

    ok, _ = enforcer.Enforce(userBob, resourceData1,'write')
    fmt.Printf('Bob can write data1: %v\n', ok)
}

在上面的代码中,我们实现了一个基于Casbin的ABAC(基于属性的访问控制)权限模型。下面是对代码的详细解释:

  1. 1. 首先,我们定义了两个结构体:User 和 ResourceUser 结构体包含了用户的属性(名称、年龄、角色),Resource 结构体包含了资源的属性(名称、位置)。

  2. 2. 接下来,我们定义了一个自定义的策略函数 policyFunc。这个函数接受三个参数:用户、资源和操作。根据这些参数,函数判断用户是否有权限访问资源。在这个示例中,我们定义了以下规则:

    • · 如果用户是 admin,允许访问;

    • · 如果用户是 owner,并且资源位于 USA,允许访问;

    • · 如果用户年龄大于 18,并且操作是 read,允许访问。

  3. 3. 初始化一个Casbin的Enforcer,并使用 AddFunction 方法将自定义策略函数添加到 Enforcer 中。

  4. 4. 创建用户和资源实例:userAliceuserBob 和 resourceData1

  5. 5. 使用 Enforce 方法检查用户对资源的访问权限。在这个示例中,我们检查了 Alice 和 Bob 对 data1 资源的读写权限。

运行这个程序,你将看到以下输出:

Alice can read data1: true
Bob can read data1: false
Bob can write data1: false

Casbin综合实践

表结构定义

接下来,我们通过go的casbin库来完整实现一个RBAC权限模型,通过在API服务器中调用RBAC模型进行权限校验。

首先,创建MySQL表结构:

-- 用户表
CREATETABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50)UNIQUENOTNULL,
    password VARCHAR(50)NOTNULL
);

-- 角色表
CREATETABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)UNIQUENOTNULL
);

-- 权限表
CREATETABLE permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)UNIQUENOTNULL,
    menu VARCHAR(50)NOTNULL
);

在这里,用户表(users)存储用户信息,角色表(roles)存储角色信息,权限表(permissions)存储权限信息。role_constraints 表用于存储角色的约束信息。

当使用Casbin库存储策略到数据库时,Casbin会创建一个规则表用于存储策略信息。这个表的结构取决于具体的数据库适配器。对于Gorm适配器(gorm-adapter),Casbin将创建一个表,通常命名为casbin_rule,用于存储策略规则。实际在开发的时候,用户角色关联表(user_roles)、角色权限关联信息(role_permissions)这个三个表可以不用创建,直接复用下面的casbin_rule表。所以实际开发的时候,只需要用户表(users)、角色表(roles)、约束表(role_constraints)和规则表(casbin_rule)四张表即可。

以下是一个简化的Casbin规则表结构的例子:

CREATE TABLE casbin_rule (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ptype VARCHAR(100),
    v0 VARCHAR(100),
    v1 VARCHAR(100),
    v2 VARCHAR(100),
    v3 VARCHAR(100),
    v4 VARCHAR(100),
    v5 VARCHAR(100)
);

解释表结构中的字段:

  • · id: 规则的唯一标识,通常是一个自增的整数。

  • · ptype: 策略类型,通常是'p'表示权限策略(policy)或'g'表示角色关联策略(grouping policy)或角色继承。

  • · v0, v1, v2, v3, v4, v5: 规则的各个参数值,这些参数值的含义依赖于策略类型。

    • · 对于权限策略,通常是(sub, obj, act)三元组。

    • · 对于角色关联策略,通常是用户与角色之间的关联。

    • · 对于角色继承,通常是子角色和父角色之间的关系。

例如,一条权限策略规则可能如下,这表示用户'alice'具有对资源'data1'执行'read'操作的权限。

INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p''alice''data1''read'); 

而一条角色关联策略规则可能如下,这表示用户'alice'拥有角色'admin'。

INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g''alice''admin');

而角色继承规则可能如下,这条语句表示'user'角色继承'admin'角色。'g'表示这是一个角色继承关系,v0是父角色,v1是子角色。

INSERT INTO casbin_rule (ptype, v0, v1) VALUES ('g''admin''user');

权限模型定义

然后,初始化Casbin的RBAC模型配置文件(rbac_model.conf)如下:

# rbac_model.conf

# 定义请求的匹配规则
[request_definition]
r=sub, obj, act

# 定义策略规则
[policy_definition]
p=sub, obj, act

# 定义策略效果
[policy_effect]
e= some(where(p.eft == allow))

# 定义匹配器规则
[matchers]
m= g(r.sub, p.sub)&& keyMatch(r.obj, p.obj)&&(r.act == p.act || p.act =='*')

# 角色继承规则的默认效果
[role_definition]
g= _, _

这个文件的解释如下:

  • · [request_definition]:定义了一个请求需要的元素。在这里,请求需要三个元素:主体(sub,表示用户)、对象(obj,表示资源)和操作(act,表示对资源的操作,如读、写等)。

  • · [policy_definition]:定义了策略规则需要的元素。与请求定义相同,策略规则也需要三个元素:主体、对象和操作。

  • · [policy_effect]:定义了策略效果。在这里,策略效果是“some(where (p.eft == allow))”,表示只要有一个策略规则允许访问,请求就被允许。

  • · [matchers]:定义了匹配器规则。匹配器规则用于确定请求和策略规则是否匹配。在这里,匹配器规则包括三部分:

    • · g(r.sub, p.sub):表示请求的主体需要具有策略规则中定义的角色。

    • · keyMatch(r.obj, p.obj):表示请求的对象需要与策略规则中的对象匹配。keyMatch是Casbin内置的匹配函数,支持部分匹配和通配符。

    • · (r.act == p.act || p.act == '*'):表示请求的操作需要与策略规则中的操作匹配,或者策略规则中的操作是通配符(表示允许所有操作)。

  • · [role_definition]:定义了角色继承规则。在这里,角色继承规则是g = _, _,表示角色可以继承其他角色。这在实际应用中很有用,例如,你可以定义一个管理员角色,继承了所有普通用户角色的权限。

源码

当使用 Go 中的 HTTP 中间件时,你可以创建一个中间件函数,该函数在每个请求到达 HTTP 处理器之前执行权限校验。

rbac.go文件

package main

import(
'fmt'
'log'
'net/http'

'github.com/casbin/casbin/v2'
    gormadapter 'github.com/casbin/gorm-adapter/v3'
'gorm.io/driver/mysql'
'gorm.io/gorm'
)

// 数据库连接信息
const(
DBUsername='root'
DBPassword='123'
DBHost='localhost'
DBPort='3306'
DBName='rbac'
)

// 初始化 Casbin Enforcer
func InitCasbinEnforcer()(*casbin.Enforcer,error){
    dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName)
    db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{})
if err !=nil{
returnnil, err
}

    adapter, err := gormadapter.NewAdapterByDB(db)
if err !=nil{
returnnil, err
}

    enforcer, err := casbin.NewEnforcer('rbac_model.conf', adapter)
if err !=nil{
returnnil, err
}

    err = enforcer.LoadPolicy()
if err !=nil{
returnnil, err
}

return enforcer,nil
}

// 鉴权
func Authorizer(e *casbin.Enforcer, username string)func(next http.Handler) http.Handler{
returnfunc(next http.Handler) http.Handler{
        fn :=func(w http.ResponseWriter, r *http.Request){
            role :=GetRole(username)
if role ==''{
                role ='anonymous'
}

// casbin rule enforcing
            res, err := e.Enforce(role, r.URL.Path, r.Method)
if err !=nil{
                http.Error(w,'[1] Internal Server Error', http.StatusInternalServerError)
return
}
if res {
next.ServeHTTP(w, r)
}else{
                http.Error(w, fmt.Sprintf('[2] Access Forbidden(%s,%s,%s)', role, r.URL.Path, r.Method), http.StatusForbidden)
return
}
}

return http.HandlerFunc(fn)
}
}

// 获取用户拥有的角色
func GetRole(username string)(role string){
// 初始化数据库连接
    dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName)
    db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{})
if err !=nil{
return
}

var arr []string
    db.Raw('select v1 from casbin_rule where v0=? and ptype=?', username,'g').Scan(&arr)
iflen(arr)>0{
        role = arr[0]
}

return
}

// 获取用户拥有的所有权限(包括继承角色的)
func GetPermissions(username string, enforcer *casbin.Enforcer)(permissions []string){
    arr, _ := enforcer.GetImplicitPermissionsForUser(username)

// arr结构:[['admin','/*','*'],['anonymous','/login','*'],['member','/logout','*'],['member','/member/*','*']]
// 数组组成是角色、资源、操作
    visited :=make(map[string]bool)
for _, policy :=range arr {
if _, ok := visited[policy[1]];!ok {
            visited[policy[1]]=true
            permissions =append(permissions, policy[1])
}
}
return
}

// 获取用户拥有的菜单按钮权限
func GetMenus(username string, enforcer *casbin.Enforcer)(menus []string){
// 获取用户拥有的所有权限(包括继承角色的)
    arr, _ := enforcer.GetImplicitPermissionsForUser(username)
var permissions []string

// arr结构:[['admin','/*','*'],['anonymous','/login','*'],['member','/logout','*'],['member','/member/*','*']]
// 数组组成是角色、资源、操作
for _, policy :=range arr {
        permissions =append(permissions, policy[1])
}

// 初始化数据库连接
    dsn := fmt.Sprintf('%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local',DBUsername,DBPassword,DBHost,DBPort,DBName)
    db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{})
if err !=nil{
return
}

iflen(permissions)==0{
return
}
    db.Raw('select distinct menu from permissions where name in ?', permissions).Scan(&menus)

return
}

func logoutHandler() http.HandlerFunc{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        w.Write([]byte('Logout success\n'))
})
}

func loginHandler() http.HandlerFunc{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        w.Write([]byte('Login success\n'))
})
}

func currentMemberHandler() http.HandlerFunc{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        w.Write([]byte(fmt.Sprintf('Get current member success:%s\n', r.URL.Path)))
})
}

func adminHandler() http.HandlerFunc{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        w.Write([]byte('I'm an Admin!\n'))

})
}

func main(){
// 初始化 Casbin Enforcer
    enforcer, err :=InitCasbinEnforcer()
if err !=nil{
        log.Fatal('Failed to initialize Casbin enforcer:', err)
}

    username :='alice'// 假设当前用户
// 获取用户所有角色(包括角色继承的)
    roles, _ := enforcer.GetImplicitRolesForUser(username)
    log.Println(fmt.Sprintf('%s has these roles:%v', username, roles))

// 获取用户所拥有的权限
    permissions :=GetPermissions(username, enforcer)
    log.Println(fmt.Sprintf('%s has these permissions:%v', username, permissions))

// 获取用户所拥有的菜单权限
    menus :=GetMenus(username, enforcer)
    log.Println(fmt.Sprintf('%s has these menus:%v', username, menus))

    mux := http.NewServeMux()
    mux.HandleFunc('/login', loginHandler())
    mux.HandleFunc('/logout', logoutHandler())
    mux.HandleFunc('/member/lisi', currentMemberHandler())
    mux.HandleFunc('/member/zhangsan', currentMemberHandler())
    mux.HandleFunc('/admin/stuff', adminHandler())

// 启动API服务器
    log.Println('Server is running on :8080')
    log.Fatal(http.ListenAndServe(':8080',Authorizer(enforcer, username)(mux)))

}

测试

为了测试RBAC模型,我们初始化一些测试数据。

首先是初始化一些用户、角色和权限表。

-- 初始化用户
INSERTINTO users(username,passowrd)VALUES('tom','pwd1');
INSERTINTO users(username,passowrd)VALUES('alice','pwd1');
INSERTINTO users(username,passowrd)VALUES('bob','pwd1');

-- 初始化角色
INSERTINTO roles(name)VALUES('admin');
INSERTINTO roles(name)VALUES('anonymous');
INSERTINTO roles(name)VALUES('member');

-- 初始化权限表
INSERTINTO permissions(name, menu)VALUES('/login','用户模块');
INSERTINTO permissions(name, menu)VALUES('/logout','用户模块');
INSERTINTO permissions(name, menu)VALUES('/member/*','成员模块');
INSERTINTO permissions(name, menu)VALUES('/admin/staff','管理员模块');

因为我们要关联接口对应的菜单关系,所以在permissions表里维护接口菜单关系时,接口名称要具体到完整路径,不能用类似/*这种模糊匹配。

然后在设计策略的时候,角色关联的接口权限也要具体到完整接口路径,这个后面可以和前面的permissions表里根据接口地址获取到对应的菜单列表,前端就可以根据菜单列表做控制展示。

下面的策略规则表示如下:

  • · admin角色拥有所有接口权限

  • · anonymous角色只有登录接口权限

  • · member角色可以登录、退出以及访问/member/路径的接口权限

p, admin,/*, *
p, admin, /login, *
p, admin, /logout, *
p, admin, /member/*, *
p, admin, /admin/staff, *

p, anonymous, /login, *

p, member, /login, *
p, member, /logout, *
p, member, /member/*, *

g, tom, admin
g, alice, member
g, bob, member

g, member, anonymous
g, admin, member
INSERT INTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/*','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/login','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/logout','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/member/*','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','admin','/admin/staff','*');

INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','anonymous','/login','*');

INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/login','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/logout','*');
INSERTINTO casbin_rule (ptype, v0, v1, v2)VALUES('p','member','/member/*','*');

然后我们给3个用户初始化对应的角色,并且让member角色继承anonymous角色,也就是说member角色也包含anonymous角色的权限,同样的admin角色继承了member角色,也就间接继承了member角色和anonymous角色的所有权限。

INSERT INTO casbin_rule (ptype, v0, v1)VALUES('g','tom','admin');
INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','alice','member');
INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','bob','anonymous');

INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','member','anonymous');
INSERTINTO casbin_rule (ptype, v0, v1)VALUES('g','admin','member');

前面代码,我们假设请求的当前用户是alice,启动前面的代码,我们可以看到输出如下,可以看到alice拥有的角色member,同时因为member是继承anonymous角色,所以也间接拥有anonymous角色。最终可以看到alice拥有member和anonymous两个角色的接口权限。

go run rbac.go

2023/xx/xx 17:20:52 alice has these roles:[member anonymous]
2023/xx/xx 17:20:52 alice has these permissions:[/login /logout /member/*]
2023/xx/xx 17:20:52 alice has these menus:[用户模块 成员模块]

实际去请求下面4个接口,返回是通过的,跟策略一样。

请求:curl -d '{}' http://localhost:8080/login
响应:Login success
请求:curl -d '{}' http://localhost:8080/logout
响应:Logout success
请求:curl -d '{}' http://localhost:8080/member/lisi
响应:Get current member success:/member/lisi
请求:curl -d '{}' http://localhost:8080/member/zhangsan
响应:Get current member success:/member/zhangsan

当请求admin角色的接口时,就收到无权限的提示,符合预期设置的策略规则。

请求:curl -d '{}' http://localhost:8080/admin/staff
响应:[2] Access Forbidden(role:member,/admin/staff,POST)

假设当前用户是bob,他是anonymous角色,执行go run rbac.go输出如下:

2023/xx/xx 17:24:08 bob has these roles:[anonymous]
2023/xx/xx 17:24:08 bob has these permissions:[/login]
2023/xx/xx 17:24:08 bob has these menus:[用户模块]

假设当前用户是tom,他是admin角色,执行go run rbac.go输出如下:

2023/xx/xx 17:24:48 tom has these roles:[admin member anonymous]
2023/xx/xx 17:24:48 tom has these permissions:[/login /logout /member/* /admin/staff]
2023/xx/xx 17:24:48 tom has these menus:[管理员模块 用户模块 成员模块]

参考

[1] https://www./infs767/infs767fall03/lecture01-2.pdf

[2] https://www./infs767/infs767fall03/

[3] https://www.cnblogs.com/wang_yb/archive/2018/11/20/9987397.html

[4] https:///pdf/1903.09756.pdf

[5] https:///zh/docs/tutorials

[6] https://narendraj9./posts/generalized-authz.html

[7] https://www./jos/article/abstract/5624

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多