> 本文由PingCode@阿杰分享
2021年 Wiki 加入了很多强硬的特性,其中包括协同编辑 、页面权限、表情符号 等,这些功能给用户带来了更好的体验。作为 Wiki 使用者兼开发者,今日来聊聊年终上线的页面权限,同时总结一下开发阶段涉及到的技术、遇到的问题以及解决方案,关于权限本人之前已经写过一篇 Worktile 权限的文章了,Worktile 权限着重讲了 RBAC(基于角色的权限控制方案)的设计与实现,本文基于 Wiki 页面权限选择的另一个主流权限设计的方案:ACL。
本文大致分为三部分:
- ACL 介绍
- 介绍我们的权限以及为什么选择它
- 设计实现
一、ACL 介绍
1. 什么是 ACL?
ACL:Access Control List,权限控制列表,是对文件以及目录的权限控制方案。大名鼎鼎的 Linux 权限系统,它就是 ACL 的典型案例,本人在开发过程中也受到了 Linux 权限设计的一些启发。
2. ACL的使用场景
使用场景也可以换个问法:为什么要使用 ACL ?关于这个问题我们还以 Linux 作为案例:Linux 本身只提供了Owner(所有者)、Group(用户组)、Others(其他成员),也就是说其他成员或用户组是无法指定更细粒度的权限。
为了更好的解释,我们来举个简单的例子(场景):
有 4 个成员有 A、B、C、D,其中 A、B、C 是开发组G的成员,A成员创建了一个代码仓库并把团队开发的代码放置到该目录中,其中这些代码主要是关于G组的,与其他成员无关,所以A把文件目录设置了权限,权限是组内可读可写,其他人没有任何权限。
现在来分析一下各个角色,A 是仓库的 Owner,G 是 Group(含B、C),D 是与该文件无关的成员,所以是 Others。现在入职了一个用户E,因为E是新人,所以不想让E去操作代码,只允许他查看熟悉代码。
面对这种场景,试想一下如何给 E 成员设置对应权限呢?答案是 oh no,因为 E 既不能按照 G 组权限,也不能按照 Others 权限,更不能是 Owner!所以面对这种鸡肋的权限,ACL 就作为了其补充,ACL 可以支持针对某一个用户或某个用户组做独立的权限,完美解决了类似场景。
二、PingCode Wiki 权限架构
OK,了解了什么是 ACL 以及使用场景,我们来聊一下 Wiki 的权限架构,基本架构见下图。
1. 基础权限 RBAC——角色对应的权限
1.1 权限配置
1.2 查看权限
2. 页面权限ACL——针对用户和用户组的权限
页面权限同样是作为 RBAC 的补充,更加细粒度的权限划分,即给某部分人或某部分用户组分配对应权限。
2.1 页面权限设置入口
右上角菜单——「更多-权限设置」
2.2 独立权限与共享权限
独立权限——「权限设置-知识库成员」
共享权限——「权限设置-非知识库成员」
从我们的需求(针对用户和用户组部分人群)来看是符合 ACL 解决方案的。
三、设计与实现
到这里的同学相信已经对 Wiki 权限功能有了大致的了解,下面开始详细介绍一下开发中的设计细节,关键点、难点以及方案的选用取与舍。
1. 关系模型与数据库设计
了解基本需求,本人根据以往的开发经验简单的定了一个初步方案,见下图方案A,后来对照原型和设计图调整了一些细节,产生了方案B,见下图。
A方案
B 方案(最终方案)
两种方案的区别:
A 方案是关系型数据库的常见设计,以用户/用户组为主导映射模块和权限,对应关系为:多对多 用户/用户组(user/group) ——> 资源(pageId)
B 方案,以模块为主导映射用户/用户组,一对多:资源(pageId)——> 用户/用户组(scopes)
考虑到权限是全量保存的,正是由于这种交互方式所以调整出了 B 方案,而且服务端数据库是 mongodb,支持数组的特性,非常的契合。
2. 对应的程序实现
资源或模块(pageId)——> 用户/用户组(scopes),实体对应关系
scopes | Array<Schema> | 权限设置详细 |
permission_type | Enum | 权限类型 |
principals | ScopeDesignation[] | 指定详细 |
type | ScopeDesignationType | 指定类型 |
id | UIDlID | 人/用户组/部门的唯一标识 |
value | number | 权限值 |
权限值计算参考了Linux,采用的是位运算:1、2、4、8...
- 拥有的权限值累计取最大mask值:只读1 + 编辑 2,最终数据库存3。
判断是否拥有某个权限值:权限值 & 预期的权限值 = 存在/预期的权限值。
3. 约定 API
还是由于保存交互决定了添加、修改、删除是一个 API。
- 保存(添加/修改/移除):
{put}/api/wiki/spaces/:spaceId/pages/:pageId/permission$
获取:
{get} /api/wiki/spaces/:spaceId/pages/:pageId/permission/:scopeType$
4. 涉及的复杂场景
权限涉及的复杂场景非常的多,以下场景讨论和决定经历了很多波折,某些功能中途重做了两次,迭代向后推迟了一周,两次会议,共持续大约两个半小时,由于文章篇幅,只列举部分场景作为交流。
4.1 权限继承(复杂场景)
第一个难处理的点就是权限继承,确实很绕,相信很多产品都遇到过类似场景(具体是哪些这里不赘述了),本人从技术和产品的角度都参考过 Linux 的处理,但由于系统级别和应用还是有些许不同,做了一些调整,下面罗列的是我们做过的方案。
第一版:子页面完全继承父页面且不可更改- 父页面不可见,子页面不可见
- 父页面只读,子页面编辑,最终子页面是只读
- 父页面编辑,子页面只读,最终子页面是编辑
不满足的场景和逻辑的冲突点:
- 父页面允许查看,预期某些子页面允许查看,某些子页面不可查看
- 父页面允许查看但不许编辑,预期某些子页面开放编辑权限
- 父页面允许编辑,但某些子页面不允许编辑
第二版:子页面允许独立权限,默认继承父权限
页面继承逻辑:子页面一旦设置,中断与父页面的继承关系,中断后权限类型可以改
- 父级页面设置权限后,子级页面不设置权限,默认继承父级权限;
- 子级页面设置权限后,不再继承父级权限,形成自己单独的页面权限
4.2 中间件逻辑
中间件涉及的场景也比较多,比如单页面详情查看,拖拽移动以及跨知识库移动,复制,删除等等,核心的验证逻辑如下图。
4.3 页面树展示及权限计算(难点)
4.3.1 展示逻辑
会议上,讨论页面树的展示的可选方案如下:
- 展示全部层级页面,根据权限可见内容(存在标题私密性问题,层级过多后,会看到多个不可见的标题,抛弃)
- 只展示有权限的页面,把子页面提出来(层级结构打乱了,抛弃)
- 落地方案:只展示有权限的页面,当父页面不可见时,子页面可见时,把父页面展示出来,如果没有可见的子页面时父页面也不可见(方案二)---父页面不可见的标题用灰色展示(点击效果保留),点击后右侧无权限页面
- 直接隐藏父与子(已实现方案一,仅能通过通知查看,入口少,改变继承关系后,不太适用,抛弃)
最终确定的是一个折中方案,如果子页面有权限,那么会把父页面也展示出来,保证树结构的完整性,若当前页面和子页面都没有权限,那么就过滤掉。如下图
4.3.2 权限计算(技术难点)
难点:
- 页面树涉及到继承
- 全量的计算(弄不好需要递归处理),所以程序计算复杂度要考虑
- 过滤逻辑(子有查看权限,父一定展示)
解决手段
- 数据库存储 parent 和 parent_ids 字段,parent_ids 按照树的层级拍平排序,比如树的层级是 A->B->C,parent_ids 储存的是 [A,B,C]。
- 把列表数据分割成两组,一组是所有的页面,另一组是所有作为了父页面的,具体怎么查询只需要验证parent_id 是否存在即可,属于数据库操作。
- 把 List 数据结构转换成 Map 或 Set,减少迭代次数来降低复杂度。
具体处理逻辑
准备工作:
- 从数据库中取出当前知识库的页面列表数据和页面对应的 ACL 权限列表
- 将页面列表拍平转换为Map(为了下面减少复杂度)
- 将权限列表拍平转换为 Map(目的同上)
- 取出 RBAC 的知识库权限
- 用来处理子页面可见,父页面不可见,而需要展示的父页面集合 Set(在下面会详细讲解具体用处)
- 最终要 页面列表(含权限值)
如下图(为了方便后续的讲解,特标注序号)
处理流程,如下图:
- 迭代页面列表
- 取出一个页面,找对应权限
- 如果找到了设置的对应权限,那么合并 RBAC 权限放入最终的结果集,没有找到执行下面逻辑a. 通过当前页面的 parent_ids 按倒序迭代(上面解决手段已讲解 parent_ids 的排列顺序)
b. 如果没有父级或所有父级均没有权限,则继续最外层迭代,反之取出最近有权限的父级页面,并且将父级页面与当前页面之间的页面放入 ⑥ (Set)中,这块大家可能有疑问,举个例子:假设有3个页面,A、B、C,C 的父级是 B,B 的父级是 A,当前迭代的是 C,C 没有权限,B也没有权限,A有权限,那么最终将 A、B 推入 Set 中,C 由于继承逻辑 也拥有权限。 - 将 Set 中的父级页面补充到最终的结果集
- 继续迭代,直至完毕
最后发现 ⑥ 是为了补充缺失的一部分数据:子页面可见,父页面不可见,但要展示的父页面。
大家可能从上面环节还有一个疑问就是为什么要合并 RBAC 知识库权限,请接着看。
4.4 权限相关特殊处理
上面谈到了已经验证出页面权限,但还是合并了知识库权限,是因为产品逻辑中有以下规定:
- 知识库没有编辑权限,页面权限有,那么最终有编辑权限,所以要覆盖知识库的编辑权限。
- 拥有只读权限的成员不能执行的操作:
- 不能创建子页面
- 不能删除当前页面
- 不能移动当前页面
- 不能移动至只读的页面下(对目标页面来说实际上是添加页面)
- 不能复制页面
- 不能复制至只读页面下
- 不能发布页面
- 不能重命名页面
- 不能进入编辑页面
注意的是切勿漏掉场景。
4.4.1 个人对后续的设想
针对于这些特殊处理,个人感觉终究不是解决方案,而且后续新加或移除权限,都要兼顾多处,技术角度维护起来困难,也容易漏掉,所以设想页面也加入更多权限点,让用户来主动勾选所需要和禁止的权限,技术上就可以统一一套逻辑。
五、写在最后
上面只是列举了一些通用和些许复杂的场景,实际开发中还有很多的细节,所以开发期间很忙,经历了头脑风暴,也有走误区的及时调整和反思,这些都是宝贵的经验。
最后谈谈开发至今的一些感悟:
- 团队之间及时沟通,充分理解需求,建立统一认知,避免把烟囱做成水井的糗事,也减少互相甩锅的情况,对团队有良好的作用,值得一提的是Wiki团队秉承着这一优良习惯,与 Wiki 这款产品的价值观相契合 。
- 技术人员要积极考虑需求,优先站在用户和产品角度思考,其次再考虑技术,切勿过于执着技术,却忽略了用户和产品的初衷。
- 技术人员对每一行代码负责,别把技术债留给自己或别人,代码终将会回馈团队和你自身。
- 要做好细节,作为还是技术都要做好细节,“刚”到可能是细节决定的成败。
到这里结束了,非常感谢每个看到这里的同学 ,有任何疑问欢迎讨论交流。
欢迎关注 Wiki,关注 PingCode 研发中心
最后,推荐我们的智能化研发管理工具 PingCode 给大家。
关于PingCode