diff --git a/client/web/src/styles/antd/dark.less b/client/web/src/styles/antd/dark.less index b6f3b34f..42c1ce3c 100644 --- a/client/web/src/styles/antd/dark.less +++ b/client/web/src/styles/antd/dark.less @@ -153,6 +153,15 @@ color: rgba(255, 255, 255, 0.65); } } + + .ant-dropdown-menu-item-disabled, .ant-dropdown-menu-submenu-title-disabled { + color: rgba(255, 255, 255, 0.25); + + &:hover { + color: rgba(255, 255, 255, 0.25); + background-color: transparent; + } + } } } diff --git a/server/lib/call.ts b/server/lib/call.ts index 388dcb05..82dabc94 100644 --- a/server/lib/call.ts +++ b/server/lib/call.ts @@ -74,5 +74,23 @@ export function call(ctx: TcContext) { groupId, }); }, + /** + * 检查群组成员权限 + */ + async checkUserPermissions( + groupId: string, + userId: string, + permissions: string[] + ): Promise { + const userAllPermissions: string[] = await ctx.call( + 'group.getUserAllPermissions', + { + groupId, + userId, + } + ); + + return permissions.map((p) => (userAllPermissions ?? []).includes(p)); + }, }; } diff --git a/server/lib/role.ts b/server/lib/role.ts new file mode 100644 index 00000000..dd5ad71b --- /dev/null +++ b/server/lib/role.ts @@ -0,0 +1,17 @@ +export const PERMISSION = { + /** + * 非插件的权限点都叫core + */ + core: { + owner: '__group_owner__', // 保留字段, 用于标识群组所有者 + message: 'core.message', + invite: 'core.invite', + unlimitedInvite: 'core.unlimitedInvite', + groupDetail: 'core.groupDetail', + managePanel: 'core.managePanel', + manageInvite: 'core.manageInvite', + manageRoles: 'core.manageRoles', + }, +}; + +export const allPermission = [...Object.values(PERMISSION.core)]; diff --git a/server/models/group/group.ts b/server/models/group/group.ts index 23ee0e4d..438d6a75 100644 --- a/server/models/group/group.ts +++ b/server/models/group/group.ts @@ -10,6 +10,7 @@ import { import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'; import _ from 'lodash'; import { Types } from 'mongoose'; +import { allPermission } from '../../lib/role'; import { User } from '../user/user'; export enum GroupPanelType { @@ -196,19 +197,13 @@ export class Group extends TimeStamps implements Base { this: ReturnModelType, groupId: string, roleId: string, - roleName: string, - operatorUserId: string + roleName: string ): Promise { const group = await this.findById(groupId); if (!group) { throw new Error('Not Found Group'); } - // 首先判断是否有修改权限的权限 - if (String(group.owner) !== operatorUserId) { - throw new Error('No Permission'); - } - const modifyRole = group.roles.find((role) => String(role._id) === roleId); if (!modifyRole) { throw new Error('Not Found Role'); @@ -227,19 +222,13 @@ export class Group extends TimeStamps implements Base { this: ReturnModelType, groupId: string, roleId: string, - permissions: string[], - operatorUserId: string + permissions: string[] ): Promise { const group = await this.findById(groupId); if (!group) { throw new Error('Not Found Group'); } - // 首先判断是否有修改权限的权限 - if (String(group.owner) !== operatorUserId) { - throw new Error('No Permission'); - } - const modifyRole = group.roles.find((role) => String(role._id) === roleId); if (!modifyRole) { throw new Error('Not Found Role'); @@ -264,6 +253,11 @@ export class Group extends TimeStamps implements Base { throw new Error('Not Found Group'); } + if (String(group.owner) === userId) { + // 群组管理者有所有权限 + return [...allPermission]; + } + const member = group.members.find( (member) => String(member.userId) === userId ); diff --git a/server/services/core/group/group.service.ts b/server/services/core/group/group.service.ts index ee8062ed..f9f43e97 100644 --- a/server/services/core/group/group.service.ts +++ b/server/services/core/group/group.service.ts @@ -22,6 +22,7 @@ import { } from 'tailchat-server-sdk'; import { call } from '../../../lib/call'; import moment from 'moment'; +import { PERMISSION } from '../../../lib/role'; interface GroupService extends TcService, @@ -320,14 +321,30 @@ class GroupService extends TcService { throw new EntityError(t('该数据不允许修改')); } - const group = await this.adapter.model.findById(groupId).exec(); - if (String(group.owner) !== userId) { - throw new NoPermissionError(); + const [isGroupOwner, hasRolePermission] = await call( + ctx + ).checkUserPermissions(groupId, userId, [ + PERMISSION.core.owner, + PERMISSION.core.manageRoles, + ]); + + if (fieldName === 'fallbackPermissions') { + if (!hasRolePermission) { + throw new NoPermissionError(t('没有操作权限')); + } + } else if (!isGroupOwner) { + throw new NoPermissionError(t('不是群组管理员无法编辑')); } + const group = await this.adapter.model.findById(groupId).exec(); + group[fieldName] = fieldValue; await group.save(); + if (fieldName === 'fallbackPermissions') { + await this.cleanGroupAllUserPermissionCache(groupId); + } + this.notifyGroupInfoUpdate(ctx, group); } @@ -472,15 +489,12 @@ class GroupService extends TcService { const { groupId, memberIds, roles } = ctx.params; const { t, userId } = ctx.meta; - const isOwner: boolean = await this.actions['isGroupOwner']( - { - groupId, - }, - { - parentCtx: ctx, - } + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.manageRoles] ); - if (!isOwner) { + if (!hasPermission) { throw new NoPermissionError(t('没有操作权限')); } @@ -520,15 +534,12 @@ class GroupService extends TcService { const { groupId, memberIds, roles } = ctx.params; const { t, userId } = ctx.meta; - const isOwner: boolean = await this.actions['isGroupOwner']( - { - groupId, - }, - { - parentCtx: ctx, - } + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.manageRoles] ); - if (!isOwner) { + if (!hasPermission) { throw new NoPermissionError(t('没有操作权限')); } @@ -570,17 +581,15 @@ class GroupService extends TcService { ) { const { groupId, name, type, parentId, provider, pluginPanelName, meta } = ctx.params; - const { t } = ctx.meta; - const isOwner: boolean = await this.actions['isGroupOwner']( - { - groupId, - }, - { - parentCtx: ctx, - } + const { t, userId } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] ); - if (!isOwner) { + if (!hasPermission) { throw new NoPermissionError(t('没有操作权限')); } @@ -626,16 +635,14 @@ class GroupService extends TcService { ) { const { groupId, panelId, name, provider, pluginPanelName, meta } = ctx.params; - const { t } = ctx.meta; - const isOwner: boolean = await this.actions['isGroupOwner']( - { - groupId, - }, - { - parentCtx: ctx, - } + const { t, userId } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] ); - if (!isOwner) { + if (!hasPermission) { throw new NoPermissionError(t('没有操作权限')); } @@ -674,16 +681,14 @@ class GroupService extends TcService { */ async deleteGroupPanel(ctx: TcContext<{ groupId: string; panelId: string }>) { const { groupId, panelId } = ctx.params; - const { t } = ctx.meta; - const isOwner: boolean = await this.actions['isGroupOwner']( - { - groupId, - }, - { - parentCtx: ctx, - } + const { t, userId } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] ); - if (!isOwner) { + if (!hasPermission) { throw new NoPermissionError(t('没有操作权限')); } @@ -746,19 +751,27 @@ class GroupService extends TcService { ctx: TcContext<{ groupId: string; roleName: string; permissions: string[] }> ) { const { groupId, roleName, permissions } = ctx.params; - const userId = ctx.meta.userId; + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有操作权限')); + } const group = await this.adapter.model .findOneAndUpdate( { _id: new Types.ObjectId(groupId), - owner: new Types.ObjectId(userId), }, { $push: { roles: { name: roleName, - permissions: [], + permissions, }, }, }, @@ -777,13 +790,21 @@ class GroupService extends TcService { */ async deleteGroupRole(ctx: TcContext<{ groupId: string; roleId: string }>) { const { groupId, roleId } = ctx.params; - const userId = ctx.meta.userId; + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有操作权限')); + } const group = await this.adapter.model .findOneAndUpdate( { _id: new Types.ObjectId(groupId), - owner: new Types.ObjectId(userId), }, { $pull: { @@ -813,13 +834,21 @@ class GroupService extends TcService { }> ) { const { groupId, roleId, roleName } = ctx.params; - const userId = ctx.meta.userId; + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有操作权限')); + } const group = await this.adapter.model.updateGroupRoleName( groupId, roleId, - roleName, - userId + roleName ); const json = await this.notifyGroupInfoUpdate(ctx, group); @@ -837,13 +866,21 @@ class GroupService extends TcService { }> ) { const { groupId, roleId, permissions } = ctx.params; - const userId = ctx.meta.userId; + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.managePanel] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有操作权限')); + } const group = await this.adapter.model.updateGroupRolePermission( groupId, roleId, - permissions, - userId + permissions ); const json = await this.notifyGroupInfoUpdate(ctx, group); @@ -871,6 +908,7 @@ class GroupService extends TcService { /** * 获取群组成员权限(对内) + * 带有groupId和userId的缓存 */ async getUserAllPermissions( ctx: TcContext<{ @@ -963,6 +1001,14 @@ class GroupService extends TcService { private cleanGroupUserPermission(groupId: string, userId: string) { this.cleanActionCache('getUserAllPermissions', [groupId, userId]); } + + /** + * 清理群组所有用户缓存 + * @param groupId 群组id + */ + private cleanGroupAllUserPermissionCache(groupId: string) { + this.cleanActionCache('getUserAllPermissions', [groupId]); + } } export default GroupService; diff --git a/server/services/core/group/invite.service.ts b/server/services/core/group/invite.service.ts index f797e269..1b7c99c1 100644 --- a/server/services/core/group/invite.service.ts +++ b/server/services/core/group/invite.service.ts @@ -12,6 +12,7 @@ import { TcDbService, PureContext, } from 'tailchat-server-sdk'; +import { PERMISSION } from '../../../lib/role'; interface GroupService extends TcService, @@ -62,21 +63,21 @@ class GroupService extends TcService { inviteType: 'normal' | 'permanent'; }> ): Promise { - const groupId = ctx.params.groupId; - const inviteType = ctx.params.inviteType; - const userId = ctx.meta.userId; - const t = ctx.meta.t; - - // TODO: 基于RBAC判定群组权限 - // 先视为仅群组所有者可以创建群组邀请 - const isGroupOwner = await ctx.call( - 'group.isGroupOwner', - { - groupId, - } - ); - if (isGroupOwner !== true) { - throw new NoPermissionError(t('不是群组所有者, 没有分享权限')); + const { groupId, inviteType } = ctx.params; + const { userId, t } = ctx.meta; + + const [hasNormalPermission, hasUnlimitedPermission] = await call( + ctx + ).checkUserPermissions(groupId, userId, [ + PERMISSION.core.invite, + PERMISSION.core.unlimitedInvite, + ]); + + if ( + (inviteType === 'normal' && !hasNormalPermission) || + (inviteType === 'permanent' && !hasUnlimitedPermission) + ) { + throw new NoPermissionError(t('没有创建邀请码权限')); } const invite = await this.adapter.model.createGroupInvite( @@ -96,18 +97,15 @@ class GroupService extends TcService { }> ) { const groupId = ctx.params.groupId; - const t = ctx.meta.t; + const { t, userId } = ctx.meta; - // TODO: 基于RBAC判定群组权限 - // 先视为仅群组所有者可以创建群组邀请 - const isGroupOwner = await ctx.call( - 'group.isGroupOwner', - { - groupId, - } + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.manageInvite] ); - if (isGroupOwner !== true) { - throw new NoPermissionError(t('不是群组所有者, 没有查看权限')); + if (!hasPermission) { + throw new NoPermissionError(t('没有查看权限')); } const list = await this.adapter.model.find({ @@ -170,18 +168,15 @@ class GroupService extends TcService { async deleteInvite(ctx: TcContext<{ groupId: string; inviteId: string }>) { const groupId = ctx.params.groupId; const inviteId = ctx.params.inviteId; - const t = ctx.meta.t; + const { t, userId } = ctx.meta; - // TODO: 基于RBAC判定群组权限 - // 先视为仅群组所有者可以创建群组邀请 - const isGroupOwner = await ctx.call( - 'group.isGroupOwner', - { - groupId, - } + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION.core.manageInvite] ); - if (isGroupOwner !== true) { - throw new NoPermissionError(t('不是群组所有者, 没有查看权限')); + if (!hasPermission) { + throw new NoPermissionError(t('没有删除权限')); } await this.adapter.model.deleteOne({