本文由 PingCode 龚林杰分享
本文主要介绍 PingCode 整个权限体系的设计以及实现。在介绍权限体系之前,我们还需要先明白 PingCode 的产品体系、账户体系,这样才能把权限都给介绍清楚。
子产品体系
跟 Worktile 一样,PingCode 是一些列产品的组合,他包含有很多个子产品:产品管理,项目管理,测试管理,知识管理,效能度量,协作空间,自动化,Access 等,还包含了应用市场的很多个插件,应用,甚至用户自己开发的自建应用等。
PingCode 的每个子产品都有一套自己的权限点的配置,用来控制团队中每个成员的操作行为,那么我们会为每个子产品独立设计一套权限系统吗?
账户体系
PingCode 同 Worktile 一样是一种多租户系统,是以团队为核心,用户只属于团队成员。但是在 PingCode 权限体系分为两套系统,第一套权限系统是管理整个团队的关键操作的,第二套权限系统则是管理每个子产品内部关键操作的。
一. 团队权限体系
这套系统是用于控制团队管理的权限的,比如团队成员管理,订阅管理,安全管理,每个子产品的创建和配置的管理等。
在这套系统中,有两种配置方式
通过以上添加管理员,配置权限组和权限的形式即完成了对于团队关键数据的权限控制。
二. 产品权限体系
这套权限系统就是控制 PingCode 中每个子产品内部功能的操作。
在产品管理的产品成员管理中可以给成员分配相应的角色,该成员在当前的产品中就拥有了该角色相应的权限。
以上就是 PingCode 的子产品体系和账户体系的介绍,那么技术上我们是如何进行权限设计的呢?
权限的设计与实现
功能权限是完全按照 RBAC 模型 设计的,关系为:
无论是团队权限还是产品权限,我们统一使用同样的设计方式,也就是说连数据库表都是相同的,只是在操作的地方做了相应的区分。
数据库设计
在传统关系型数据库的设计,基本都是三张表:角色表,权限表,角色权限关联表,如果校验一个或一组权限,是需要三表关联查询的。
但是 PingCode 采用的是 Mongdb + 系统常量配置的形式来处理的,所以这里我们只介绍角色表和角色的权限存储表,而权限点则是通过系统常量进行配置的。
class RoleEntity { _id: Id; name: string; application: Application; // 用于区分哪个子产品的角色 type: Type; // 区分:团队角色,产品角色 is_system: Is; // 是否为系统内置}
复制代码
class RolePermissionEntity { _id: Id; role_id: Id; application: Application; // 用于标识哪个子产品的权限 permissions: Dictionary<Is>; // 这是一个字典表,具体形式 {create_ticket: 1, edit_ticket: 0}}
复制代码
permissions 用户存储每个权限点是否具有操作权限的,0: 无权限,1:有权限
每个子产品权限点的常量配置
const permissionDictionary = { // product permissions product_basic_setting: { display_position: 1000, storage_position: 1000, admin_default: Is.yes, normal_member_default: Is.no, readonly_member_default: Is.no, group: PERMISSION_GROUPS.product.key, key: `product_basic_setting`, // 在 RolePermissionEntity 表中 permissions 字段中的一个key值 text: `基本设置` }, item1... item2...}
复制代码
只是以上的数据表还不足以支撑整个权限系统的运行,成员是否有操作的权限最终还是要判断该成员拥有什么角色。我们还是分别介绍团队权限和子产品权限两个路线。
对于这个权限体系来说很直接,就是在成员上给予其什么样的团队角色,这里说明一下,我们在技术实现上把权限组也作为角色来存储了,也就是说 role_ids 中存储有管理员角色 ID 和权限组 ID
class UserEntity { _id: Id; role_ids: Id[]; ...}
复制代码
该成员拥有角色之后,就可以进行判断其拥有什么样的权限了,请求如下:
对于每个子产品来说成员有没有权限操作子产品下的功能,也是根据成员拥有什么样的角色,他存储在产品的成员管理中
class ProductEntity { _id: Id; name: string; members: Array<{uid: string, role_ids: Id[]}>}
复制代码
判断子产品的权限点就稍微又些复杂了,请求如下:
统一实现
PingCode 拥有两套权限体系,每个子产品都有自己的权限判断,为了达到一致性,减少错误,以及代码的复用我们肯定是要统一实现的。
由于团队权限的配置是独立于所有产品之外的,而且相对比较简单,所以下面我们只介绍 PingCode 所有子产品的权限体系是如何搭建的。
通过使用 PingCode 你会发现,在 PingCode 中所有子产品配置中心的权限配置中,产品形态都是一样的,唯一不同的是每个子产品的权限点不同。这样对于我们统一配置的实现也带来的巨大的方便。我们从技术上是通过权限系统提供 SDK 的方式,定义出统一的 API PATH,让每个子产品接入 SDK ,让子产品根据要求传入特有的权限点配置。
import { ApplicationRoleService } from "../role/application-role.service";import { RoleEntity } from "../../entities";import { FindResponse } from "@atinc/eros/info";import { RouterContext } from "@atinc/chaos/router";import { PermissionGroup } from "../../constants";import { TyphonOperationContext } from "../../info/operation-context";export declare class ApplicationRoleConfigurationFacade<TRoleService extends ApplicationRoleService> { protected roleService: TRoleService; constructor(roleService: TRoleService); /** * @api {get} /apiPrefix/configuration/application-roles 获取角色列表 * @apiName getApplicationRoles * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": RoleEntity[] * } * } */ protected getApplicationRoles(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<RoleEntity[], void>>; /** * @api {get} /apiPrefix/configuration/application-roles/all-permissions 获取所有角色以及权限 * @apiName getAllApplicationRolesAndPermissions * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value":{ * roles: RoleEntity[], * groups: any[] * } * } * } */ protected getAllApplicationRolesAndPermissions(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<{ roles: RoleEntity[]; groups: any[]; }, void>>; /** * @api {get} /apiPrefix/configuration/application-roles/:roleId 获取角色详情 * @apiParam (param) {string} roleId * @apiName getApplicationRole * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": RoleEntity * } * } */ protected getApplicationRole(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<RoleEntity, void>>; /** * @api {get} /apiPrefix/configuration/application-roles/:roleId/permissions 获取角色权限 * @apiParam (param) {string} roleId * @apiName getApplicationRolePermissions * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": PermissionGroup[] * } * } */ protected getApplicationRolePermissions(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<PermissionGroup[], void>>; /** * @api {put} /apiPrefix/configuration/application-roles/:roleId/permissions 设置角色权限 * @apiParam (param) {string} roleId * @apiName setApplicationRolePermissions * @apiGroup SDK-Configuration-Role * @apiParamExample {json} Request-Example: * [{ * display_position: number; * storage_position: number; * admin_default?: Is; * group?: string; * key: string; * text?: string; * }] * * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": true * } * } */ protected setApplicationRolePermissions(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<boolean, void>>; /** * @api {put} /apiPrefix/configuration/application-roles/:roleId/default 设置默认角色 * @apiParam (param) {string} roleId * @apiName setDefaultRole * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": true * } * } */ protected setDefaultRole(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<boolean, void>>; /** * @api {del} /apiPrefix/configuration/application-roles/:roleId/default 取消默认角色 * @apiParam (param) {string} roleId * @apiName removeDefaultRole * @apiGroup SDK-Configuration-Role * @apiSuccess {Number} code 200 * @apiSuccessExample {json} Success-Response: * { * "oid": "893b67d6-244b-40d2-ae1e-6a5c3d7910a2", * "code": 200, * "data": { * "value": true * } * } */ protected removeDefaultRole(ctx: RouterContext<TyphonOperationContext>): Promise<FindResponse<boolean, void>>;}
复制代码
这样每个子产品就可以获取角色和权限,并且给角色设置权限,设置默认角色等。
同配置一样,权限判断也是通过 SDK 的方式进行,权限系统提供权限控制的基类,用户获取其所在产品的所有权限,通过权限点判断是否有操作的权限。权限判断的基类中提供了如下几个方法。
export declare class PermissionBase { // 验证团队权限 authenticateGlobalPermissionsByUser(operationContext: TyphonOperationContext, expectedPermissionPoint: string): Promise<boolean>; // 获取用户的团队权限 getCombinedGlobalPermissionByUser(operationContext: TyphonOperationContext): Promise<string>; // 验证产品权限 authenticateApplicationPermissionsByUser(operationContext: TyphonOperationContext, roleIds: Id[], expectedPermissionPoint: string): Promise<boolean>; // 获取用户的产品权限 getCombinedApplicationPermissionByUser(operationContext: TyphonOperationContext, roleIds: Id[]): Promise<string>;}
复制代码
我们调用 API 时,API 的中间件来判断权限,调用如下
public authenticateOperationCustomerMiddleware(expectedPermissionPoint: PermissionDefinitionItem) { return async (ctx: RouterContext<CompanyOperationContext>, next: INextFunction) => { let product = ctx.operationContext.product; if (!product) { const productId = this.resolveProductId(ctx); product = await this.service.getProductById(ctx.operationContext, productId); ctx.operationContext.product = product; } const me = product.members.find(member => member.uid === ctx.operationContext.uid); if (!me) { throw new WTError(WTCode.invalidInput, `member not in the product(${product._id})`); } await this.authenticateApplicationPermissionsByUser(ctx.operationContext, me.role_ids || [], expectedPermissionPoint.key); return await next(); }; }
复制代码
PingCode 所有子产品只需要集成权限系统的 SDK 实现相应的 API 和 权限判断的基类,我们就实现了权限系统的统一性了。以上就是关于 PingCode 权限系统的介绍以及如何设计和实现的。
结尾
当我们实现产品功能的时候,尽量做到统一,可复用,可扩展,这样无论产品形态怎么变,我们只需要做小的修改就能适配产品的变动,这也是我们技术上要做到的标准。
最后,推荐我们的智能化研发管理工具 PingCode 给大家。
PingCode官网
关于 PingCode
PingCode 是由国内老牌 SaaS 厂商 Worktile 打造的智能化研发管理工具,围绕企业研发管理需求推出了 Agile(敏捷开发)、Testhub(测试管理)、Wiki(知识库)、Plan(项目集)、Goals(目标管理)、Flow(自动化管理)、Access (目录管理)七大子产品以及应用市场,实现了对项目、任务、需求、缺陷、迭代规划、测试、目标管理等研发管理全流程的覆盖以及代码托管工具、CI/CD 流水线、自动化测试等众多主流开发工具的打通。
自正式发布以来,以酷狗音乐、商汤科技、电银信息、51 社保、万国数据、金鹰卡通、用友、国汽智控、智齿客服、易快报等知名企业为代表,已经有超过 13 个行业的众多企业选择 PingCode 落地研发管理。
评论