一、ABAC经典设计案例分析
我们先来说说个经典的设计案例,这个设计案例会让你进一步的理解ABAC的能力
其实这个案例在最后的参考文章里,你也可以自己看,我这里是结合着我的理解在讲
AWS IAM权限策略设计
我们先来看一个json
// 看不懂没关系,先混个脸熟 { "Action":[ "EditUser", "GetUser" ], "Resource":"user:1、2、3、4", "Condition":{ "DateLessThan":{ "acs:CurrentTime":"2021-03-22T17:00:00+08:00" } } }
我们在上篇文章中说过,权限管控的本质是为了控制一个访问实体可以做什么 ,RBAC通过角色聚合权限,很容易的做到了这些,但是做过权限系统的人都知道,很多时候还有一些其他条件的影响,以及这个人可以访问什么数据的限制,如果要做到这些,则需要在角色或者权限上面加上各种属性,然后又去根据各种条件过滤,这样子带来的复杂性是呈指数上升的,并且不够通用。我们再看看上面的json中,这个json我们可以把它视作一个访问策略,在ABAC中叫做policy(上文中已经说过),在这个policy中,Action元素其实就是一系列api code的聚合,Resource元素就是可以访问的资源的集合,condition元素指的是额外的限定条件,所以这个json表示的是,和这个policy绑定的人可以在2021年的3月22日之前去对id为1,2,3,4的四个用户执行这个编辑和查看操作(对应EditUser和GetUser),这样,就做到了增加条件限制以及限定访问的数据。
这个是AWS的IAM访问策略的设计,也只是其中一个很简单的例子,但也是一个非常经典的设计案例,它的设计思想是源自于XMACL,XMACL的文档在结尾的文档中(不推荐学ABAC的看这玩意,太复杂了)。
事实上亚马逊的这个policy中的元素是非常多的,远远不止这么几个,而且每个元素也是比我例子中的复杂的,比如我下面这个policy,它是表示获取AlexaForBusiness这个资源的所有权限以及和其相关的AWS的权限:
// 看不懂也没关系,因为只是为了让你明白思想 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "a4b:*", "kms:DescribeKey" ], "Resource": "*" }, { "Action": [ "iam:CreateServiceLinkedRole" ], "Effect": "Allow", "Resource": "*", "Condition": { "StringLike": { "iam:AWSServiceName": [ "*a4b.amazonaws.com" ] } } }, { "Effect": "Allow", "Action": [ "iam:DeleteServiceLinkedRole", "iam:GetServiceLinkedRoleDeletionStatus" ], "Resource": "arn:aws-cn:iam::*:role/aws-service-role/*a4b.amazonaws.com/AWSServiceRoleForAlexaForBusiness*" }, { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DeleteSecret", "secretsmanager:UpdateSecret" ], "Resource": "arn:aws-cn:secretsmanager:*:*:secret:A4B*" }, { "Effect": "Allow", "Action": "secretsmanager:CreateSecret", "Resource": "*", "Condition": { "StringLike": { "secretsmanager:Name": "A4B*" } } } ] }
再次强调,看不懂没关系,别被这个json看晕了,这个策略为啥包含这么多东西,一般是只有设计者才知道的,而我们需要搞懂的是如何去设计。
二、权限系统设计方案
1、ABAC设计本质
我们从之前的文章中能够明白,ABAC设计的目的是为了能够满足控制请求者在某些条件下是否对请求数据具备某个操作(API) 的能力,目前来看,ABAC还暂时没有什么标准建模(policy不是由实体组成),一般都是policy的语法设计,这个指的就是用一个json或者一个xml来描述一个policy ,这个policy会和用户或者其他实体绑定,然后这个用户或者是实体就具备了在某些条件下对某些数据的操作能力。
为什么ABAC没有标准建模呢?作者的经验是认为,ABAC主要是语法设计,因此会非常灵活,这种非常灵活的关系不适合用模型和模型 之间的约束关系来表示,因此目前经典的设计都是用xml或者json来表示一个policy。
2、业务背景概述及设计难点分析
现在的业务场景是要做一个为一系列SaaS服务的提供权限管控的权限系统,同时包括控制每个用户渲染的菜单的能力,这要求,我们的权限系统必须足够通用,并且还能够在各种各样复杂的业务场景下进行访问控制(即鉴权服务)。
最后这句话里有个非常难的点就是鉴权,因为我们知道,不同的业务,在鉴权的时候往往会受到业务特性的影响,但是我们的权限系统不可能把所有的这些业务特性都集成进来,一方面这样做权限系统就不是通用的了,再一方面,如果这样做了,那么每有个业务方接入的话,那就必须适配新的业务方的业务逻辑,然后还有就是复杂度会随着时间而呈指数增长,接入方多的话,这样的系统简直会没办法维护。
这时候我们的ABAC便派上了用场。
3、ABAC的设计思路
面对这样的场景,我们该怎么设计ABAC呢?我们来看看我们的语法设计:
{ "effect":"Allow", "actions":[ "getUser", "updateUser", "get*" ], "resources":[ "contextField:contextValue:resourceField:resourceValue" ], "condition":[ { "operation-name":{ "condition-key":[ "condition value" ] } } ] }
其中action部分还是api,resource主要是各种业务条件以及业务数据的约束,condition部分目前主要是各种时间地理位置的约束,我们可以看出,condition部分完全可以由权限系统自己来闭环,而resource部分是受业务影响的,所以我们重点看看resource部分的语法:
为了防止你懵逼,我还是解释一下为什么condition部分权限系统自己就可以闭环? 这是因为我们权限系统的设计condition部分就是一些公共的条件,比如时间,如果是要求当前请求只能是早上八点到晚上的九点才 可以访问,那么你完全可以获取请求的当前时间,然后再和policy中的condition value去做比对,以下是一个约束时间的json: { "effect":"Allow", "actions":[ "getOrder" ], "resources":[ "contextField:contextValue:resourceField:resourceValue" ], "condition":[ { "DateLessThan":{ "CurrentTime":[ "2021-03-30 17:00:00" // 必须满足时间小于2021-03-30 17:00:00 ] } } ] }
然后我们看看resource的语法设计,这里我先抛出一个业务场景,现在有个客服SaaS系统,采用的是租户模式,每个租户下有很多商家,每个商家又有很多客服人员,这就意味着每个租户有个主商家,系统中有些事情是只有主商家才可以做的。这时候我们的json可以怎么设计呢?我们来看看如下例子:
{ "effect":"Allow", "actions":[ "getOrder" ], "resources":[ "isMain:1:orderId:12345" // 并且必须是主商家,且只能访问orderId为123456的订单 ], "condition":[ { "DateLessThan":{ "CurrentTime":[ "2021-03-30 17:00:00" // 必须满足时间小于2021-03-30 17:00:00 ] } } ] }
我们重点来看看resource中 “isMain:1:orderId:12345” 这个玩意,isMain就是一个业务字段,含义是是否为主商家,1的意思为主商家,所以这个json的意思就是用户所在商户必须是主商家并且访问时间必须是早于2021-03-30 17:00:00才可以获取id为12345的订单(getOrder是获取订单),当用户和我们这个策略绑定之后呢,便可以这样去对用户鉴权了。
但是聪明的你肯定注意到了,权限系统怎么知道当前访问者所在商家是否是主商家呢?这是个超级重点的问题,这里有很多权限系统就走入一个坑,这个坑是什么呢?就是它们会去调用业务系统去查,这个做法是大错特错的,我们不能去依赖业务系统去做权限管控的,否则每来一个业务系统对接,我们还得去开发,另外就是复杂度会随着业务系统的变多而呈指数增长,这是无法接受的。那么我们是怎么做的呢?我们观察到,一般这类属性都是很重要的属性,其实在前端页面发起请求之前,就已经查看过商家的信息了,这时候让业务系统前端把这些在policy中的业务属性带在请求头中就行,权限系统此时就可以根据这些数据和policy来做比对,从而完成鉴权,并且,这样子做,业务系统的改造量是非常小的。
你一定发现了一些通用的设计技巧但又不知道是啥对吗?大方的我再次告诉你,policy就像是我们和权限系统做的约定,resource 部分就是告诉权限系统我会用这些业务字段来鉴权,然后我请求的时候再把这些值带上,权限系统来计算即可
4、RBAC 和 ABAC的优势及劣势
我们在前面文章中说过,RBAC在应对复杂场景的时候会显得很吃力,但是它有没有好处呢?答案是当然有,它的好处显而易见就是简单且容易理解,我们很容易想象,一般不是开发者的这种用户创建一个角色是多么的容易,但是你如果让他去写一个策略的json,那他怕是要骂娘,因为一般人根本都不知道json是干嘛的,所以就更不可能会写json。
ABAC的优点就是RBAC的缺点,它可以胜任在复杂场景下的鉴权工作,但是不知道读者们注意到了上面的json中没有菜单信息 这一点了没,往具体的说就是如果你的系统使用的是个很纯粹的ABAC权限模型,那么,你的系统是不具备管控用户可以渲染的菜单的能力的,这时候肯定有人说,我可以通过一个人的所绑定的策略(ABAC中policy一般都是分配给人的)计算出他能访问的所有API然后去渲染API所绑定的菜单呀,这个想法其实是大错特错的,一方面,因为policy中有的条件是动态的,比如时间,如果纯粹靠计算来的话,那么这一刻和下一刻看到的菜单很有可能是不一样的(因为时间可能也是一种条件),另一方面就是,API和菜单往往不是一一对应关系,也就是说,API上面可能绑定了多个菜单,这时候就会有粒度的问题。
5、RBAC 和 ABAC取舍以及结合
结合上面的业务,我们来分析下我们的需求,我们需要做到的有如下的几个点:
- 向各种各样的业务去提供一个统一的、满足各类业务要求的鉴权服务
- 控制业务系统的菜单渲染能力
- 不能向用户展现太大的复杂度
- 要让原本的业务系统使用我们的权限系统之后业务改造成本足够低
- 不能与业务系统产生过多交互
为了同时满足上面的要求,我们结合使用了RBAC和ABAC的能力,大致建模如下:
注意哈,这个不是数据库建模,而是一个DDD的建模,具体的我就再不能解释了,再解释就涉及到商业机密了,大家可以参考这个设计,也可以找我来进一步交流。