Univer
Univer Sheet
功能
权限控制

权限控制

📊 Univer Sheet

Univer 提供了权限控制的能力,通常可用于限制用户对工作簿、工作表、选区的操作。当用户进行没有权限的操作时,可终止代码执行并提示用户缺失权限。例如你可以在一片选区内设置区域保护,就可以设置其他协作者在该保护范围内是否可以使用编辑、阅读、复制、筛选等等。

注意:Univer提供的是可拓展的基础能力,并非定制化的能力,如果有持久化或者组织架构等更丰富和更定制化的需求,需要自行实现权限规则的存储和组织架构接入,这里需要编写自定义插件来实现。 所以会出现设置权限后去查看权限列表时权限信息为空、查看用户信息时返回为空等情况,这是因为这些信息都需要走接口请求拉取,是自定义的逻辑 需要自己额外补充,可以看下面的第三方接入。

基础示例

工作簿权限

目前工作簿的整体权限是直接通过 API 来修改权限点位,我们提供了 Facade API,你可以通过调用这个 API 来设置工作簿不同功能的权限。

拿编辑权限举例(其他功能只需替换权限点位即可, 具体的权限点位列举在文章最下面, 需要从 @univerjs/sheets 中引入),你可以通过下面的代码来设置整体工作簿不可编辑:

import { FUniver } from "@univerjs/facade";
 
const univerAPI = FUniver.newAPI(univer);

然后你就可以通过调用 univerAPI 的方法来获取 FPermission 对象,从而操作具体的权限设置:

const permission = univerAPI.getPermission();
// unitId 是工作簿的 id, WorkbookEditablePermission 是权限点位, import { WorkbookEditablePermission } from '@univerjs/sheets' , false 表示该权限不可使用
permission.setWorkbookPermissionPoint(unitId, WorkbookEditablePermission, false)

工作表权限代码示例

工作表和选区相关的权限设置通过API和命令模式两种方式来执行,这里以工作表编辑权限举例,其他工作表功能只需替换权限点位即可,具体的权限点位列举在文章最下面。

API 方式

const permission = univerAPI.getPermission();
// 创建工作表权限,unitId 是工作簿的 id, subUnitId 是工作表的 id,此时生成的权限是一个基础权限,它的作用是用于渲染权限区域
const permissionId = await permission.addWorksheetBasePermission(unitId, subUnitId);
permission.setWorksheetPermissionPoint(unitId, subUnitId, WorksheetEditPermission, false);

命令模式

const accessor = univer.__getInjector();
const commandService = accessor.get(ICommandService);
// import { getSheetCommandTarget } from '@univerjs/sheets';
const target = getSheetCommandTarget(univerInstanceService);
if (!target) {
  return;
}
const { unitId, subUnitId } = target;
commandService.executeCommand(AddWorksheetProtectionMutation.id, {
  unitId,
  subUnitId,
  rule: {
    permissionId: "2sxcza1",
    name: "sheet",
    unitType: 2,
    unitId,
    subUnitId,
  },
});
 
const permissionService = accessor.get(IPermissionService);
permissionService.updatePermissionPoint(new WorksheetEditPermission(unitId, subUnitId).id, false);

该命令中的参数:permissionId 需要自己生成的唯一 Id 用来存储权限,unitType 是工作表的类型,你可以使用仓库中的 UnitObject 枚举来替换上面的魔法数,unitId 是工作簿的id,subUnitId 是工作表的 id。

删除工作表权限

删除已经存在的工作表权限可以使用命令模式驱动 DeleteWorksheetProtectionMutation,参数是 unitIdsubUnitId

DeleteWorksheetProtectionMutationParams {
  unitId: string;
  subUnitId: string;
}

也可以使用API来删除工作表的所有权限设置

const permission = univerAPI.getPermission();
permission.removeWorksheetPermission(unitId, subUnitId);

自定义选区权限代码示例

选区的权限同样可以通过 API 和命令模式两种方式来执行,这里以选区编辑权限举例,其他选区功能只需替换权限点位即可,具体的权限点位列举在文章最下面。

API 方式

const permission = univerAPI.getPermission();
const res = await permission.addRangeBasePermission(unitId, subUnitId, ranges);
// 这里的返回区别于工作表权限,因为一个子表中可能会有多个区域保护,所以 ruleId 是用来存储该权限规则的唯一 id,permissionId 是用来拼接权限点位的。
const { permissionId, ruleId } = res;
permission.setRangeProtectionPermissionPoint(unitId, subUnitId, permissionId, RangeProtectionPermissionEditPoint, false);

命令模式

const accessor = univer.__getInjector();
const commandService = accessor.get(ICommandService);
const sheetSelectionManagerService = accessor.get(SheetsSelectionsService);
// import { getSheetCommandTarget } from '@univerjs/sheets';
const target = getSheetCommandTarget(univerInstanceService);
if (!target) {
  return;
}
const { unitId, subUnitId } = target;
const ranges = sheetSelectionManagerService.getCurrentSelections().map(selection => selection.range);
commandService.executeCommand(AddRangeProtectionMutation.id, {
  unitId,
  subUnitId,
  rules: [{
    permissionId: "3xtfxG1",
    name: "sheet1",
    unitType: 3,
    unitId,
    subUnitId,
    ranges,
    id: 'rule1'
  }],
});
 
const permissionService = accessor.get(IPermissionService);
// 这里 RangeProtectionPermissionEditPoint 的第三个参数就是上面生成的 permissionId,false 表示不可编辑
permissionService.updatePermissionPoint(new RangeProtectionPermissionEditPoint(unitId, subUnitId, "3xtfxG1").id, false);

这里面的参数和上面的一样,增加了 ranges 参数,这个参数是你选中的区域, 还有一个 id 参数, 这个参数是用来标识这个规则的唯一 id,用来删除这个规则。

删除区域保护权限

删除已经存在的自定义区域保护可以通过命令模式驱动 DeleteRangeProtectionMutation,参数是 unitIdsubUnitId 以及想批量删除的 ruleIds

DeleteRangeProtectionMutationParams {
  unitId: string;
  subUnitId: string;
  ruleIds: string[];
}

也可以使用API来删除选区的所有权限设置

const permission = univerAPI.getPermission();
permission.removeRangeProtection(unitId, subUnitId, ruleIds);

拓展使用

这里以WorkbookEditablePermission举例,在自己的插件中增加权限校验,其他点位类似

import { WorkbookEditablePermission } from '@univerjs/sheets';
import { IPermissionService } from '@univerjs/core';
 
class YourService {
  constructor(@IPermissionService private _permissionService: IPermissionService) {
 
  }
 
  setWorkbookNotEditable() {
    this._permissionService.updatePermissionPoint(new WorkbookEditablePermission('unitId').id, false);
  }
 
  setWorkbookEditable() {
    this._permissionService.updatePermissionPoint(new WorkbookEditablePermission('unitId').id, true);
  }
}

你也可以拓展修改别得权限点位来实现对不同功能的权限控制,具体点位列表请参考文章底部。

第三方插件如何扩展权限点位

import { IPermissionService, IPermissionPoint } from '@univerjs/core';
 
export class CustomPermissionPoint implements IPermissionPoint {
  type = UnitObject.Unkonwn; // your type
  subType = UnitAction.View; // your subType
  status = PermissionStatus.INIT;
  value = true; // 权限的初始值
  id: string;
  constructor(unitId: string, subUnitId: string, customId: string) {
    // 这里自行拼凑一个 id,id 属性需要保证在整个 IPermissionService 中是唯一的凭证。
    this.id = `${unitId}.${subUnitId}.${customId}`;
  }
}
 
class YourService {
  constructor(@IPermissionService private _permissionService: IPermissionService) {
    this._init()
  }
 
  _init() {
    this._permissionService.addPermissionPoint(new CustomPermissionPoint('unitId', 'subUnitId', 'my-id'));
  }
}
 
// 在其他地方如何使用
class ConsumeService {
  constructor(@IPermissionService private _permissionService: IPermissionService) {
  }
 
  doSomething() {
   const point = this._permissionService.getPermissionPoint(new CustomPermissionPoint('unitId', 'subUnitId', 'my-id').id);
   console.log(point.value);
  }
 
  bindEvent() {
    // 这将获得一个 Rx 对象,使得你能够监听当前权限的变化作出一些改变
    const pount$ = this._permissionService.getPermissionPoint$(new CustomPermissionPoint('unitId', 'subUnitId', 'my-id').id);
    console.log(pount$);
  }
}

第三方权限服务接入(进阶用法)

🚨

已经开始接入自定义权限,可以直接操作更细粒度的权限点位,建议不要和 Permission Facade Api 混用。

权限的判断逻辑通常是由一个外置服务来单独处理,这部分一般会带有一个通信流程,在纯前端 sdk 实现中,我们使用 AuthzIoLocalService (opens in a new tab) 来承载这部分逻辑。

在生产环境中,我们需要将这部分实现转由后端实现,前端需要基于 IAuthzIoService (opens in a new tab) 类型实现对应的请求函数,来进行运行时的替换。

Permission

下面是一个简单的示例用来展示预设 2 个角色( 所有者/阅读者) 情况下区域保护权限点位的增删. 所有者拥有保护区域的编辑/查看权限,而阅读者不能编辑也不能查看保护区域内单元格的内容.

import { IAuthzIoService, Injector, Univer, Inject, generateRandomId, IResourceManagerService, createDefaultUser, isDevRole, UserManagerService } from '@univerjs/core';
import { ObjectScope, UnitAction, UnitObject, UnitRole, UniverType } from '@univerjs/protocol';
import type { IActionInfo, IAllowedRequest, IBatchAllowedResponse, ICollaborator, ICreateRequest, ICreateRequest_SelectRangeObject, IListPermPointRequest, IPermissionPoint, IPutCollaboratorsRequest, IUnitRoleKV, IUpdatePermPointRequest } from '@univerjs/protocol';
 
class YourAuthzService implements IAuthzIoService {
    private _permissionMap: Map<string, ICreateRequest_SelectRangeObject & { objectType: UnitObject }> = new Map([]);
 
    constructor(
        @IResourceManagerService private _resourceManagerService: IResourceManagerService,
        @Inject(UserManagerService) private _userManagerService: UserManagerService
    ) {
        this._initSnapshot();
        this._initDefaultUser();
    }
 
    private _initDefaultUser() {
        const currentUser = this._userManagerService.getCurrentUser();
        const currentUserIsValid = currentUser && currentUser.userID;
        if (!currentUserIsValid) {
            this._userManagerService.setCurrentUser(createDefaultUser(UnitRole.Owner));
        }
    }
 
    private _getRole(type: UnitRole) {
        const user = this._userManagerService.getCurrentUser();
        if (!user) {
            return false;
        }
        return isDevRole(user.userID, type);
    }
 
    private _initSnapshot() {
        this._resourceManagerService.registerPluginResource({
            toJson: (_unitId: string) => {
                const obj = [...this._permissionMap.keys()].reduce((r, k) => {
                    const v = this._permissionMap.get(k);
                    r[k] = v!;
                    return r;
                }, {} as Record<string, ICreateRequest_SelectRangeObject & { objectType: UnitObject }>);
                return JSON.stringify(obj);
            },
            parseJson: (json: string) => {
                return JSON.parse(json);
            },
            pluginName: 'SHEET_AuthzIoMockService_PLUGIN',
            businesses: [UniverType.UNIVER_SHEET, UniverType.UNIVER_DOC, UniverType.UNIVER_SLIDE],
            onLoad: (_unitId, resource) => {
                for (const key in resource) {
                    this._permissionMap.set(key, resource[key]);
                }
            },
            onUnLoad: () => {
                this._permissionMap.clear();
            },
        });
    }
 
    async create(config: ICreateRequest): Promise<string> {
        const permissionId = generateRandomId(8);
        if (config.objectType === UnitObject.SelectRange && config.selectRangeObject) {
            this._permissionMap.set(permissionId, { ...config.selectRangeObject, objectType: config.objectType });
        }
        return permissionId;
    }
 
    async batchAllowed(config: IAllowedRequest[]): Promise<IBatchAllowedResponse['objectActions']> {
        const selectionRangeConfig = config.filter((c) => c.objectType === UnitObject.SelectRange);
        if (selectionRangeConfig.length) {
            const currentUser = this._userManagerService.getCurrentUser();
            const res = [] as IBatchAllowedResponse['objectActions'];
            selectionRangeConfig.forEach((c) => {
                res.push({
                    unitID: c.unitID,
                    objectID: c.objectID,
                    actions: c.actions.map((action) => {
                        if (isDevRole(currentUser.userID, UnitRole.Owner)) {
                            return { action, allowed: true };
                        }
                        return { action, allowed: false };
                    }),
                });
            });
            return res;
        }
        return Promise.resolve([]);
    }
 
    async list(config: IListPermPointRequest): Promise<IPermissionPoint[]> {
        const result: IPermissionPoint[] = [];
        config.objectIDs.forEach((objectID) => {
            const rule = this._permissionMap.get(objectID);
            if (rule) {
                const item = {
                    objectID,
                    unitID: config.unitID,
                    objectType: rule!.objectType,
                    name: rule!.name,
                    shareOn: false,
                    shareRole: UnitRole.Owner,
                    shareScope: -1,
                    scope: {
                        read: ObjectScope.AllCollaborator,
                        edit: ObjectScope.AllCollaborator,
                    },
                    creator: createDefaultUser(UnitRole.Owner),
                    strategies: [
                        {
                            action: UnitAction.View,
                            role: UnitRole.Owner,
                        },
                        {
                            action: UnitAction.Edit,
                            role: UnitRole.Owner,
                        },
                    ],
                    actions: config.actions.map((a) => {
                        return { action: a, allowed: this._getRole(UnitRole.Owner) };
                    }),
                };
                result.push(item);
            }
        });
        return result;
    }
 
    async listCollaborators(): Promise<ICollaborator[]> {
        // List the existing collaborators
        return [];
    }
 
    async allowed(_config: IAllowedRequest): Promise<IActionInfo[]> {
        // Because this is a mockService for handling permissions, we will not write real logic in it. We will only return an empty array to ensure that the permissions originally set by the user are not modified.
        // If you want to achieve persistence of permissions, you can modify the logic here.
        return Promise.resolve([]);
    }
 
    async listRoles(): Promise<{ roles: IUnitRoleKV[]; actions: UnitAction[] }> {
        return {
            roles: [],
            actions: [],
        };
    }
 
    async update(config: IUpdatePermPointRequest): Promise<void> {
        // Update bit information
    }
 
    async updateCollaborator(): Promise<void> {
        // Update collaborator information
        return undefined;
    }
 
    async createCollaborator(): Promise<void> {
        // Create new collaborator information
        return undefined;
    }
 
    async deleteCollaborator(): Promise<void> {
        return undefined;
    }
 
    async putCollaborators(config: IPutCollaboratorsRequest): Promise<void> {
        return undefined;
    }
}
 
export class YourPlugin extends Plugin {
  override onStarting(injector: Injector): void {
    injector.add([IAuthzIoService, { useClass: YouAuthzService }]);
  }
}
 
// 通过将 override 选项设置为 [[IAuthzIoService, null]],可以告诉 Univer 不要注册内置的 IAuthzIoService。
// 这样,Univer 将使用你在 YourAuthzService 中提供的服务作为权限服务的实现。
const univer = new Univer({
  override: [[IAuthzIoService, null]],
});
 
univer.registerPlugin(YourPlugin);

权限点位列表

不同的功能权限是由不同的权限点位来控制的,修改该点位的值可以控制对应功能的权限。下面是一些常用的权限点位,具体的点位代码,请参阅这里 (opens in a new tab)

如果 workbook 的权限控制和 worksheet / range 有交叉,那么必须全部为 true 才能使用。例如一个单元格的编辑权限必须 workbookworksheet 的编辑权限都为 true 才能编辑。

Workbook 工作簿

PermissionDescription
WorkbookEditablePermission能否编辑
WorkbookPrintPermission能否打印
WorkbookCommentPermission能否评论
WorkbookViewPermission能否查看
WorkbookCopyPermission能否复制
WorkbookExportPermission能否导出
WorkbookManageCollaboratorPermission能否管理协作者
WorkbookCreateSheetPermission能否创建工作表
WorkbookDeleteSheetPermission能否删除工作表
WorkbookRenameSheetPermission能否重命名工作表
WorkbookHideSheetPermission能否隐藏工作表
WorkbookDuplicateSheetPermission能否复制工作表
WorkbookSharePermission能否分享
WorkbookMoveSheetPermission能否移动工作表
WorksheetViewHistoryPermission能否查看历史记录
WorksheetRecoverHistoryPermission能否恢复历史记录

Worksheet 工作表

PermissionDescription
WorksheetCopyPermission能否复制
WorksheetDeleteColumnPermission能否删除列
WorksheetDeleteRowPermission能否删除行
WorksheetFilterPermission能否过滤
WorksheetInsertColumnPermission能否插入列
WorksheetInsertHyperlinkPermission能否使用超链接
WorksheetInsertRowPermission能否插入行
WorksheetPivotTablePermission能否使用透视表
WorksheetSetCellStylePermission能否编辑单元格样式
WorksheetSetCellValuePermission能否编辑单元格值
WorksheetSetColumnStylePermission能否设置列样式
WorksheetSetRowStylePermission能否设置行样式
WorksheetSortPermission能否排序
WorksheetViewPermission能否查看
WorksheetEditPermission能否编辑

Range 区域保护

PermissionDescription
RangeProtectionPermissionViewPoint能否查看保护区域的内容
RangeProtectionPermissionEditPoint能否编辑保护区域

Copyright © 2021-2024 DreamNum Co,Ltd. All Rights Reserved.