这篇文章讲述。
权限可以全部转化为访问控制。
访问控制就是访问控制矩阵的记录和查询。
用户组、RBAC、ABAC、PBAC就是帮我们管理好访问控制矩阵。
想要访问控制的管理层度越细,则method+url映射到表达元素的方式需要越精细。
映射方式由粗犷到精细,依次出现 —— url映射、method+url、url路径参数、语义化api。
RBAC如何管理需求,做好功能设计。
ABAC又如何,
PBAC的语义设计和url的语义化细节如何。
最后,某些点位实在不想把权限转化为访问控制,又该如何操作。
设计部分
一、 需求管理
To B的产品,业务上往往会涉及到权限。
这些业务上的要求为:
- 一些人能做某些事,而另一些人无法做某些事。
- 同样的视角,一些人所见数据,另一些人所见数据,各不相同。
- 一些人在控制台的视角所见页面,跟另一些人不一样。
用比喻的手法来说明,
- 需求一, 有人能进门,有人不能进这个门。
- 需求二, 不同的人进门后所见所得,不一致。
- 需求三, 有人能看见门,另一些人连门都看不见。
当面临这些业务需求时,直觉上,我们的想法是: 是时候做一些权限相关的开发工作了。
为此,我们衍生出了一系列的业务、技术概念,如角色、用户组、RBAC、ABAC等,帮助我们去做我们直觉上的权限工作。
但其实第一步,应该做好需求上的管理。
比如需求二,完全可以设计不同门,门后放置不同的风景。想让谁见到怎样的风景,就让这批人进入怎样的门。这样就从实现需求二,转而变为了实现需求一即可。
实际上的例子,学校后台管理系统中,校长可以进入"校长办公室" 页面,而老师不允许进入。
又比如需求三,不想让人看见的门,前置另一道门。当人可以通过这道前置的门时,才可以见到后面的门。那么就从实现需求三,变为实现需求一即可。
实际的例子,依然是学校后台管理系统,老师可以登录,进入系统,而学生却无法登入。
这里下一个定论,一切的权限相关的需求,皆可以设计为访问控制。
访问控制,即需求一的需求。
换言之,只要实现了访问控制,就可以通过一些设计,表达出权限相关的全部业务需求。
Q: 如果那个学校后台管理系统,不想让老师看见"校长办公室", "校长秘密通道", 这一系列的tab页,我该如何设计,才能转化为访问控制?
A: 将原来的学校后台管理系统,分为老师端和校长端,分属不同的web app,即可转化为访问控制。这样会涉及到成本问题,所以后文对需求二和需求三的设计,再做一番讨论。
二、 访问控制判断设计
为完成访问控制的判断,先剖析请求行为。
一次http请求中,具有对应的http method,和http url。
而一次访问,还具有另一个上下文。
即这次访问发起的主体。
这个主体通过身份验证获取到,塞入上下文之中。
如图所示,权限鉴定之时,已经具有method、url、principal的上下文。
通过method和url,我们唯一定位到这次访问要去往的地方。
即与我们上文所比喻的门一一对应。
而principal则代表了访问控制中的人。
一次http请求是否被授权,即为一次访问控制的判断过程。
也即为通过principal、method、url去寻找判断结果的过程。
为实现这个过程,需要事先建立访问控制矩阵。
建立之后,可由人、门两个上下文,去查询矩阵记录,获知判断结果。
三、 访问控制管理
访问控制矩阵卷帙浩繁,维护和使用成本都很高。
为此,设计上一般在遵循一些原则。
比如,无记录,则视为无法通过。
无兵符,则无法点将;无玉玺,不是皇帝。
没有记录,默认不允许通过。
另外,还有概念上的设计,帮助我们去归聚人、门。
用户组这个概念,从"人"的角度出发,将"人"归聚到一起。
组织部门,人员关系是既有的认知,可利用此既有认知帮助我们完成归聚。
例如部门x的成员,份数用户组x,理所应当。
RBAC,基于角色的访问控制,角色本身,从"门"的角度出发,将"门"归聚到一起。
业务的进行和归纳,会自然演化出角色,可通过角色上的认知,帮助归聚。
例如教师这一角色,能访问管理学生,管理试卷的页面。
如上图所示,归聚之前,人员2和3,皆可访问门2和3,但相应会产生四条记录。
归聚之后,只需记录用户组x到角色y的一条记录。即可拥有同样的信息密度。
ABAC,基于属性(或标签)的访问控制,与角色一样仍然是从"门"的角度触发,将"门"归聚到一起。
只要门2、门3,具有相同的属性,那么即可归聚到一起。
并且,RBAC是ABAC的子集,可以用ABAC兼容掉RBAC。
如图所示,建立标签role,原来归聚到角色x的资源 1,2,3绑定到标签role并且值为x,
原来绑定到角色x的人,绑定到标签 role=x。ABAC就兼容掉了RBAC。
另外,权限管理上的角色概念,是为了将归聚"门"的任务与业务概念的联系变得更紧密而出现。
但这个概念本身,并不适配它的任务。
它总是容易跟业务角色联想到一起,导致遗忘了它的任务根本与人不相关。
对"门"的归聚,若要强行与一个角色概念绑定到一起,当控制粒度需要更细时,最终会被角色概念所缚累。
比如教师有不同年级的教师,若要细粒度的对不同年级的"门"进行控制,则势必会出现角色"一年级教师"、"二年级教师"、"三年级教师"。若要粒度更细,则"一班教师"、"二班教师"此类角色又要出现了,进而会产生role explosion。
考虑基于属性的方式去归聚,"门"打上属性,班级=一班,年级=二年级。
而"人"也附加在属性年级=二年级,班级=一班。
人与门的对应属性值相同,于是可以进入。
从上面那个例子上可以看出,角色只是找个概念去归聚而已。
荃者所以在鱼,得鱼而忘荃,言者所以在意,得意而忘言。
当需要更细粒度的归聚的时候,放弃角色的概念,使用ABAC,能帮你在设计上走的更为畅通。
PBAC, policy base access control,一个policy采用json格式的领域特定语言,表达出语义,归聚"门"。
policy完全不用跟业务概念绑定在一起,故是最灵活,粒度也最为细致。丰富的特定语义和占位符,可以将表述玩的花里胡哨。但也最具复杂度,离业务概念最远。
并且,ABAC兼容RBAC,而PBAC兼容一切。
四、应对变化
随着软件的迭代,业务的进行,业务上也会出现许多变化。访问控制需要适应这些相应的变化,以满足对应的业务需求。
访问控制矩阵中,"人"的维度发生变化,是最容易适应的。
人员的产生和消失,可以简单的对应到访问控制矩阵的记录的产生和消失。
加入和退出某个用户组,可以简单的对应到用户组跟人的绑定记录的产生和消失。
这是因为,无论是"人"还是用户组的概念,它们的变化跟业务的变化天然契合。
业务上不会去设计或者发展出这些概念无法覆盖的地方。
复杂的部分,在于访问控制矩阵的另一个维度。
"门"。
"门"的新增和消失,依旧可以简单对应到访问控制矩阵的记录的产生和消失。
角色、属性的变化,依然可以是某个"门"与之绑定的角色、属性的绑定记录的产生和消失。
不过如何将这种变化,与业务的变化匹配好,则并不简单。
因为,若使用角色的概念,则角色的概念并不完全匹配归聚业务。
而若使用属性,那么属性跟业务如何契合,又考验设计者的联想能力。
而且,"门"的设计实现,并不简单。
业务形形色色,想要的表达太多,欲壑难填。
故除了method+url的简单组合外,往往会出现另外的表达。
例如aws的policy,有通配符,condition等语法和属性,可以让一个policy表达出复杂的语意。
所以,有两点建议。
- 一者,设计风格固定。以使归聚概念固定。
风格固定,RBAC能满足你的业务,就用RBAC, 一开始决定要上ABAC则上ABAC,不要沉浸在类似policy这类具体的实现之中,而忘记了自己的设计风格。或者干脆一开始就没有自己的设计风格。 - 二者,做增量式的变化。以使得容易回滚和版本控制。
实现部分
五、租户,用户,用户组
to b的产品,若做私有化交付,那么可以不必考虑租户这个概念。
每一套交付,都是一套新的租户。
若是类似云服务的场景。租户的实现则有多种。
最简单的一种,是在数据存储的表结构上,增加租户id的记录。
查询数据时,从验身过后的上下文中拿到租户id,从而进行数据筛选。
若是这样的手法,那么租户的概念,隐藏在了数据访问层。
每个租户,都以为自己独占了系统。故仍可以当作私有化部署场景。
用户和用户组,则比较简单,实现上契合业务概念即可。
六、从url,method映射到"门"
url和method到"门"的映射,表达的越精细,则可以归聚到的粒度自然就越细腻。
6.1 url 与表达元素一一映射
最粗犷简单的表达为,完全抛弃http的method, 仅用url映射要去往的门。
这时,url完全等于聚合记录中的表达元素。
例如角色x,包含元素 1,2,3。而元素1,2,3的值跟url一一对应。
6.2 method+url,组合表达,帮助结合业务概念
稍微再精细一点,http的method可跟url结合在一起,组合表达。
例如,restful的设计风格,url代表资源,是一个名词,而相应的http method代表动词。
组合起来后,等于这次http访问会对这个资源做一次怎样的操作。
角色x绑定的主体,只能对此资源进行查询,
角色y绑定的主体,能对此资源进行更新,创建,删除。
这样的风格设计,对归聚并无太大帮助,因为完全可以用各种不同的url代表method+url。
但此种风格设计,将业务表达为对资源做操作,使得"门"的表达从千姿百态转化为一个统一的定式:
权限表达元素 = 资源 + 操作
通过这样的定式,业务概念、业务设计,将与权限设计较好的结合。
6.3 路径参数,精细到具体资源
精细层度再进一步,url中出现路径参数。譬如:
storage/{accountId}/myStorageList
这个url代表访问自己的存储列表。路径中{accountId}代表了这个账户本身的id。
若忽略路径参数,例如:
storage/myStorageList
依然能够实现相应的业务功能,但是对归聚的粒度,表达力就不够了。
此时,若表达元素中的url部分为:
sotrage/357/myStorageList
则会相当精细的表达出这样的"门" —— 账号id为357的存储列表。
当然,通常会在表达元素中相应增加一些语法,以降低表达元素的维护成本。
{
"method": "GET",
"url" : "sotrage/${selfAccountId}/myStorageList"
}
通过语法${selfAccountId}, 指代了绑定主体的账户id,当此表达元素被绑定到主体之上时,即可解析展开为具体的表达。
6.4 富语义api设计
路径参数,往往是id、name等某个业务实体的标识,故当使用这样的表述方式时,已经将url变作了对资源的表达。
api的设计自然转变成为对资源进行操作,那么结合restful风格,或者google api设计规范的风格,就变得较为自然。
再精细一点,可以从url中解析出来该对应资源的丰富信息。
举例,假设aws的某个url设计为:
s3/buckets/{bucketsId}
那么可以解析出ARN:
arn:aws:s3:::buckets:{bucketsId}
aws的ARN全称为亚马逊资源名称,具有固定的格式,
arn:partition:service:region:account-id:resource-type:resource-id
上文省略了region和account-id,并且partition分区可通过验身信息找到。
具体的ARN的格式,可看这里: AWS资源名称
可参考aws的ARN,做自己的实现。也可以自行设计一套富语义api,只要能解析出你对资源的表达名称即可。
最后,method,url的映射方式精细,归聚细粒度细的归聚方式,才能得到支撑。
若映射不精细,则更细粒度的PBAC、ABAC等归聚方式无法支持。
当然,越精细,对设计者、实现者的要求就越高,技术负担就越重。
一般的to B业务,都是在RBAC层面为止。
七、RBAC
访问控制矩阵过于庞大,维护代价大,很难跟着业务走,此不必赘言。
RBAC帮助我们做归聚,是以角色这个概念为出发,将实际的业务需求,关联到权限之上的。
实现RABC的要诀在于,是保持业务角色和权限角色上的匹配。
比如这样的需求:
- 做一个任务列表接口
- 管理人员,有权限能看到全部的任务。
- 普通人员,只能看到自己相关的任务。
解读此直觉上的需求,给出直觉上的设计:
- 给出任务列表接口
- 区分管理员和普通用户
- 如果为管理员,返回全部任务
- 如果为普通用户,返回关联的任务
从功能角度来讲,这个接口的功能,不够简单清晰。
从设计角度来讲,这个接口服务的群体不够单一,它既对管理员负责,也对普通用户负责。
并且,若要以RBAC的方式,做好这个接口的访问控制,不够。
重新设计为:
- 接口1,返回全部任务列表。
- 接口2,返回被分配到的任务列表。
拆分为此两个接口后,接口1只对业务角色管理员这一个群体负责,并可以对应到权限角色上的"任务管理员"。
接口2只对业务上的普通用户负责,也可对应到权限角色"任务执行者"。
业务角色管理员还会负责分配任务,将普通用户跟任务绑定到一起。分配任务这一行为,也可以归聚在权限角色"任务管理员"之中。
RBAC的应用与实现,可见于此。
使用RBAC做归聚,又会遇到RBAC的天然缺陷,即角色概念与归聚任务,并不适配。
夸张点说牛头不对马嘴。
不过未必在感受到不匹配,或者粒度不够时,就考虑上ABAC或者PBAC。
我们开篇使用了需求管理,将相应权限的直觉需求,全部转化为了访问控制。
那么其实也可以使用需求管理,做好功能设计,消除不适配感。