年关将近,终于有了一点时间。研究了下小米的运维权限体系,结合自己过往的经验,整理了下运维权限系统相关的设计思路。欢迎各位行家拍砖。
概述
访问控制,是针对越权使用资源的防御措施。企业环境中的访问控制策略一般有三种:自主型访问控制方法、强制型访问控制方法和基于角色的访问控制方法(Role-Based Access Control, RBAC)。其中,自主式太弱,强制式太强,RBAC 是目前公认的、解决企业统一资源访问控制的有效方法。
RBAC 以角色为中心,进行访问策略的控制。例如,在一个医院系统中,医生角色可以诊断、开据处方、指示实验室化验等;研究员角色只能收集用于研究的匿名临床信息。NIST(The National Institute of Standards and Technology,美国国家标准与技术研究院) 标准 RBAC 有 4 个模型,分别是基本模型 RBAC0(Core RBAC)、角色分级模型 RBAC1(Hierarchal RBAC)、角色限制模型 RBAC2(Constraint RBAC) 和统一模型 RBAC3(Combines RBAC)[1]。
RBAC0
RBAC0 定义了 RBAC 控制系统的最小元素集合,包括用户 (user)、角色 (role)、权限 (permission)、资源 (resouce)、操作 (operation),模型如下,
在 RBAC0 模型中,主要关系有两种,为角色分配权限 PA(Permission Assignment)、分配角色给用户 UA(Users Assignment)。PA 实现权限和角色之间的关联关系,UA 实现用户和角色之间的关联关系。
在 RBAC0 模型中,资源的定义,需要注意以下两个问题 [2]:
- RBAC0 中的资源,指的是资源类别 (resouce class)、而不是某个特定资源的实例 (resouce instance),权限管理系统负责 资源类别之权限 的管理,而应用系统负责 特定资源实例之权限 的管理。因为,对资源实例的访问控制,通常带有较多个性化的业务逻辑、不适合放在高度抽象的权限管理系统中。
- RBAC0 中的资源,是一级扁平化的。在生产环境下,资源可能具有层次关系和包含关系,需要将这种层级关系 降为 一级扁平结构。
RBAC 的其他模型,都是在 RBAC0 的基础上,演化而来的。
RBAC1
RBAC1 引入了,角色间的继承关系,被继承的是 一个个的权限点。角色间的继承关系,可分为一般继承关系和受限继承关系。一般继承关系仅要求角色继承关系是一个绝对偏序关系、允许角色间的多继承 (类似有向无环图)。而受限继承关系则进一步要求角色继承关系是一个树结构。
在 RBAC1 模型中,角色继承常常借助外部已有系统来实现。
RBAC2
RBAC2 模型中添加了责任分离关系。RBAC2 的约束规定了权限被赋予角色时, 或角色被赋予用户时, 以及当用户在某一时刻激活一个角色时所应遵循的强制性规则。约束与用户-角色-权限
关系一起,决定了 RBAC2 模型中用户的访问权限。
RBAC3
RBAC3 包含了 RBAC1 和 RBAC2,既提供了角色继承,又提供了责任分离关系。
以业务树为核心的运维体系,其权限系统一般会采用 RBAC3 模型,且角色继承借助业务树来完成、约束也借助服务树来实现。
运维权限系统
本文适合,以业务树为核心的,运维体系。对于不以业务树为核心的运维体系,本文不一定可用。
业务树
业务树,以树状的上下层级模式,展示公司内部的各个业务。业务树是公司业务的高度抽象,是各运维子系统与公司业务发生耦合的纽带,是各运维子系统彼此交互的统一协议平台。一个典型的业务树,结构如下图所示,
模型设计
权限系统的设计,取决于其滋生的业务环境。根据运维系统的业务特性,运维权限系统特点如下:
- 资源类型少,操作少,因此,可以穷举所有的权限点。甚至,增加角色时,可以从所有权限点中进行勾选
- 权限点较少,运维业务场景相对固定且较少,因此,可以预定义绝大部分角色。甚至可以,不允许增删角色、只允许通过继承和覆盖来定制角色权限
- 借助业务树的层级关系,实现角色的向上继承、向上覆盖
- 权限系统的约束关系,大部分与业务规范、安全控制有关, 更适合在代码中实现
结合上述特点,采用 RBAC3 模型,设计出运维权限系统的核心表结构,如下图,
上述表结构设计,有如下关键点,
- 用户 user、角色 role、权限点 permission 是权限模型中的元素,用户角色关系表
r_user_role
记录了 UA 的结果,角色权限表r_role_permission
记录了 PA 的结果 - 资源 resource 和操作 operation,合并到权限点表 permission 中。因为,资源和操作组成的权限点相对较少、相对固定,没有必要将资源和操作拆出来、独立成表
- 角色表 role 中有一个 tag 字段。tag 是业务树节点的有序字符串描述,易于存储、同时又保留了业务树的层级信息,可以辅助实现角色的继承、覆盖等逻辑,可以适配不同公司的业务树结构
角色
运维系统,主要面向不同层次的开发和运维人员。因此,常用角色包括,
- | 开发人员 | 运维人员 |
---|---|---|
普通成员 | dev.member | sre.member |
管理员 | dev.admin | sre.admin |
… | … | … |
其中,dev.member、dev.admin、sre.member、sre.admin 可以是 权限系统预定义的 角色。
权限点
运维系统的权限点,以不同的运维子系统为核心,包括但不限于,
资源 | 操作 |
---|---|
监控系统. 策略 | C,R,U,D |
监控系统. 策略. 报警历史 | R,D |
监控系统. 绘图 | C,R,U,D |
部署系统. 任务 | C,R,U,D,X |
预算系统. 申请 | C,R,U,D,X,A |
… | … |
同一运维子系统,可以抽象出多个资源;每个资源,对应了不同的操作。资源和操作结合起来,就是权限点,如 “监控系统. 策略: C” 就是一个权限点,表示 “创建 监控策略” 的权限。
权限点是有限的、基本稳定的,不会发生大面积的更新。
给角色分配权限
PA。一般的,我们会说,把 权限” 监控系统. 策略: C”,授予 普通开发人员 “dev.member”。考虑到角色的继承、覆盖关系,我们会说,把 权限” 监控系统. 策略: C”,授予,业务节点cop.xiaomi_owt.inf
上的 普通开发人员”dev.member”,接口定义如下,
@app.route("/tag/<string:tag>/role/<string:role>/permission/<string:permission>/pa", methods=['POST'])
def pa_tag_role_permission(tag, role, permission):
rid = get_or_create_role(tag, role)
permissionId = get_or_create_permission(permission)
assign_permission_to_role(rid, permissionId)
对应的数据库操作,如下:
insert into `role` values (null, 'cop.xiaomi_owt.inf', 'dev.member') on duplicate key update id=last_insert_id(id);
# rid=`role`.lastInsertId()
insert into `permission` values (null, '监控系统.策略', 'C') on duplicate key update id=last_insert_id(id);
# permissionId=`permission`. lastInsertId()
insert into `r_role_permission` values (null, rid, permissionId);
为用户分配角色
UA。考虑到角色继承,我们会说,将 业务节点cop.xiaomi_owt.inf
上的 普通开发人员 “dev.member” 角色,授予,用户 “niean”,接口定义如下,
@app.route("/tag/<string:tag>/role/<string:role>/user/<string:user>/ua", methods=['POST'])
def ua_tag_role_user(tag, role, user):
rid = get_role(tag, role)
uid = get_user(user)
assign_user_to_role(rid, uid)
对应的数据库操作,为,
# rid=`role`.getIdOfRole('cop.xiaomi_owt.inf', 'dev.member')
# uid=`user`.getIdOfUser('niean')
insert into `r_user_role` values (null, rid, uid);
角色继承
角色继承,依赖业务树的层级结构,向上继承。继承来的角色,其权限点维持不变。
例如,用户”niean” 在业务树节点cop.xiaomi_owt.inf
上 具有 普通开发人员”dev.member” 的角色。那么用户”niean” 在业务树节点cop.xiaomi_owt.inf_pdl.falcon
上 也具有普通开发人员”dev.member” 的角色,且具有相同的权限点。
我们的数据表”role” 中,保存的是扁平的业务树节点信息,那,如何判断业务节点的上下级关系呢?字符串包含! 如,字符串cop.xiaomi_owt.inf_pdl.falcon
包含cop.xiaomi_owt.inf
,所以前者是后者的子节点。又如,cop.xiaomi_owt.miui
不包含cop.xiaomi_owt.inf
,所以前者不是后者的子节点。业务树的层级关系,蜕化为字符串的包含操作,是不是很爽,哈哈。
角色覆盖
角色覆盖,依赖业务树的层级结构,向上覆盖。考虑到安全性,角色覆盖时,对应的权限点只减少、不增加。这是运维规范的一部分,有利于提高安全性。
例如,业务树节点cop.xiaomi_owt.inf
上 绑定了一个 普通开发人员”dev.member” 的角色。根据业务需要,业务树节点cop.xiaomi_owt.inf_pdl.falcon
上的 普通开发人员”dev.member” 角色,不允许发起部署任务 (“部署系统. 任务: X”),于是,我们需要在业务树节点cop.xiaomi_owt.inf_pdl.falcon
上 重新定义 普通开发人员”dev.member” 的 权限点、减少权限点” 部署系统. 任务: X”。
角色覆盖 | dev.member 角色的权限点 |
---|---|
cop.xiaomi_owt.inf |
监控系统. 绘图: R, 部署系统. 任务: R, 部署系统. 任务: X, … |
cop.xiaomi_owt.inf_pdl.falcon |
监控系统. 绘图: R, 部署系统. 任务: R, … |
通常,我们在业务树的根节点cop.xiaomi
,授予,各角色以最大权限;同时,在业务树子节点,根据业务需要,删除一些角色特定的权限点。这样,既可以 让用户方便地 在各业务树子节点 通过继承获取权限,又可以 让管理者方便地 在各业务树节点 通过覆盖回收特定权限。
验权
典型的验权场景,如下。
@app.route("/permission/user/<string:user>/tag/<string:tag>/permission/<string:permission>/check", methods=['GET'])
def check_user_tag_permission(user, tag, permission):
if user's roles on tag have permisson:
return True
if user's roles on tag's parents have permisson:
return True
return False
如果 用户 在 业务树节点及其父节点 上的 角色 拥有 权限点,则验权成功;否则,验权失败。
总结
运维权限系统,业务场景相对简单,模型也较固定。更多的工作,需要在使用过程中逐步积累一些运维规范、安全策略、工程优化等等——从这个角度来说,权限系统是一种最佳实践的沉淀。