PingCode Wiki 权限设计之ACL

2021年 Wiki 加入了很多强硬的特性,其中包括协同编辑 、页面权限表情符号等,这些功能给用户带来了更好的体验。作为 Wiki 使用者兼开发者,今日来聊聊年终上线的页面权限,同时总结一下开发阶段涉及到的技术、遇到的问题以及解决方案,关于权限本人之前已经写过一篇Worktile 权限的文章了,Worktile 权限着重讲了 RBAC(基于角色的权限控制方案)的设计与实现,本文基于 Wiki 页面权限选择的另一个主流权限设计的方案:ACL

本文大致分为三部分:

    1. ACL 介绍
    1. 介绍我们的权限以及为什么选择它
    1. 设计实现

一、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 的权限架构,基本架构见下图。 image.png

1. 基础权限 RBAC——角色对应的权限

1.1 权限配置

image.png

1.2 查看权限

image.png

2. 页面权限ACL——针对用户和用户组的权限

页面权限同样是作为 RBAC 的补充,更加细粒度的权限划分,即给某部分人或某部分用户组分配对应权限。

2.1 页面权限设置入口

右上角菜单——「更多-权限设置」 image.png

2.2 独立权限与共享权限

独立权限——「权限设置-知识库成员」 image.png 共享权限——「权限设置-非知识库成员」 image.png 从我们的需求(针对用户和用户组部分人群)来看是符合 ACL 解决方案的。

三、设计与实现

到这里的同学相信已经对 Wiki 权限功能有了大致的了解,下面开始详细介绍一下开发中的设计细节,关键点、难点以及方案的选用取与舍。

1. 关系模型与数据库设计

了解基本需求,本人根据以往的开发经验简单的定了一个初步方案,见下图方案A,后来对照原型和设计图调整了一些细节,产生了方案B,见下图。

A方案

image.png B 方案(最终方案)

image.png 两种方案的区别:

A 方案是关系型数据库的常见设计,以用户/用户组为主导映射模块和权限,对应关系为:多对多 用户/用户组(user/group) ——> 资源(pageId)

B 方案,以模块为主导映射用户/用户组,一对多:资源(pageId)——> 用户/用户组(scopes)

考虑到权限是全量保存的,正是由于这种交互方式所以调整出了 B 方案,而且服务端数据库是 mongodb,支持数组的特性,非常的契合。

2. 对应的程序实现

资源或模块(pageId)——> 用户/用户组(scopes),实体对应关系

image.png

字段 类型 描述
scopes Array 权限设置详细
permission_type Enum 权限类型
principals ScopeDesignation[] 指定详细
type ScopeDesignationType 指定类型
id UID ID 人/用户组/部门的唯一标识
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 中间件逻辑

中间件涉及的场景也比较多,比如单页面详情查看,拖拽移动以及跨知识库移动,复制,删除等等,核心的验证逻辑如下图。

image.png

4.3 页面树展示及权限计算(难点)

4.3.1 展示逻辑

会议上,讨论页面树的展示的可选方案如下:

  • 展示全部层级页面,根据权限可见内容(存在标题私密性问题,层级过多后,会看到多个不可见的标题,抛弃
  • 只展示有权限的页面,把子页面提出来(层级结构打乱了,​抛弃
  • 落地方案:只展示有权限的页面,当父页面不可见时,子页面可见时,把父页面展示出来,如果没有可见的子页面时父页面也不可见(方案二)---父页面不可见的标题用灰色展示(点击效果保留),点击后右侧无权限页面
  • 直接隐藏父与子(已实现方案一,仅能通过通知查看,入口少,改变继承关系后,不太适用,抛弃

最终确定的是一个折中方案,如果子页面有权限,那么会把父页面也展示出来,保证树结构的完整性,若当前页面和子页面都没有权限,那么就过滤掉。如下图

image.png

4.3.2 权限计算(技术难点)

难点:

  1. 页面树涉及到继承
  2. 全量的计算(弄不好需要递归处理),所以程序计算复杂度要考虑
  3. 过滤逻辑(子有查看权限,父一定展示)

解决手段

  1. 数据库存储 parent 和 parent_ids 字段,parent_ids 按照树的层级拍平排序,比如树的层级是 A->B->C,parent_ids 储存的是 [A,B,C]。
  2. 把列表数据分割成两组,一组是所有的页面,另一组是所有作为了父页面的,具体怎么查询只需要验证parent_id 是否存在即可,属于数据库操作。
  3. 把 List 数据结构转换成 Map 或 Set,减少迭代次数来降低复杂度。

具体处理逻辑

准备工作:

  1. 从数据库中取出当前知识库的页面列表数据和页面对应的 ACL 权限列表
  2. 将页面列表拍平转换为Map(为了下面减少复杂度)
  3. 将权限列表拍平转换为 Map(目的同上)
  4. 取出 RBAC 的知识库权限
  5. 用来处理子页面可见,父页面不可见,而需要展示的父页面集合 Set(在下面会详细讲解具体用处)
  6. 最终要 页面列表(含权限值)

如下图(为了方便后续的讲解,特标注序号)

image.png 处理流程,如下图: image.png

  1. 迭代页面列表

  2. 取出一个页面,找对应权限

  3. 如果找到了设置的对应权限,那么合并 RBAC 权限放入最终的结果集,没有找到执行下面逻辑

    1. 通过当前页面的 parent_ids 按倒序迭代(上面解决手段已讲解 parent_ids 的排列顺序)
    2. 如果没有父级或所有父级均没有权限,则继续最外层迭代,反之取出最近有权限的父级页面,并且将父级页面与当前页面之间的页面放入 ⑥ (Set)中,这块大家可能有疑问,举个例子:假设有3个页面,A、B、C,C 的父级是 B,B 的父级是 A,当前迭代的是 C,C 没有权限,B也没有权限,A有权限,那么最终将 A、B 推入 Set 中,C 由于继承逻辑 也拥有权限。
  4. 将 Set 中的父级页面补充到最终的结果集

  5. 继续迭代,直至完毕

最后发现 ⑥ 是为了补充缺失的一部分数据:子页面可见,父页面不可见,但要展示的父页面

大家可能从上面环节还有一个疑问就是为什么要合并 RBAC 知识库权限,请接着看。

4.4 权限相关特殊处理

上面谈到了已经验证出页面权限,但还是合并了知识库权限,是因为产品逻辑中有以下规定:

  1. 知识库没有编辑权限,页面权限有,那么最终有编辑权限,所以要覆盖知识库的编辑权限。
  2. 拥有只读权限的成员不能执行的操作:
  • 不能创建子页面
  • 不能删除当前页面
  • 不能移动当前页面
  • 不能移动至只读的页面下(对目标页面来说实际上是添加页面)
  • 不能复制页面
  • 不能复制至只读页面下
  • 不能发布页面
  • 不能重命名页面
  • 不能进入编辑页面

注意的是切勿漏掉场景。

4.4.1 个人对后续的设想

针对于这些特殊处理,个人感觉终究不是解决方案,而且后续新加或移除权限,都要兼顾多处,技术角度维护起来困难,也容易漏掉,所以设想页面也加入更多权限点,让用户来主动勾选所需要和禁止的权限,技术上就可以统一一套逻辑。

五、写在最后

上面只是列举了一些通用和些许复杂的场景,实际开发中还有很多的细节,所以开发期间很忙,经历了头脑风暴,也有走误区的及时调整和反思,这些都是宝贵的经验。

最后谈谈开发至今的一些感悟:

  • 团队之间及时沟通,充分理解需求,建立统一认知,避免把烟囱做成水井的糗事,也减少互相甩锅的情况,对团队有良好的作用,值得一提的是Wiki团队秉承着这一优良习惯,与 Wiki 这款产品的价值观相契合 。
  • 技术人员要积极考虑需求,优先站在用户和产品角度思考,其次再考虑技术,切勿过于执着技术,却忽略了用户和产品的初衷。
  • 技术人员对每一行代码负责,别把技术债留给自己或别人,代码终将会回馈团队和你自身。
  • 要做好细节,作为还是技术都要做好细节,“刚”到可能是细节决定的成败。

到这里结束了,非常感谢每个看到这里的同学 ,有任何疑问欢迎讨论交流。

Guess you like

Origin juejin.im/post/7063295421585555492