snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
41
apps/meteor/server/lib/authorization/README.md
Normal file
41
apps/meteor/server/lib/authorization/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
Supports role or permission based authorization, and defines the association between them.
|
||||
|
||||
A user is associated with role(s), and a role is associated with permission(s). This package depends on alanning:roles for the role/user association, while the role/permission association is handled internally. Thus, the underlying alanning:roles has no concept of a permission or the association between a role and permission.
|
||||
|
||||
Authorization checks can be done based on a role or permission. However, permission based checks are preferred because they loosely associate an action with a role. For example:
|
||||
|
||||
```
|
||||
# permission based check
|
||||
if hasPermission(userId, 'edit-message') ...
|
||||
# action is loosely associated to role via permission. Thus action can be revoked
|
||||
# at runtime by removing the permission for user's role instead of modifying the action code.
|
||||
|
||||
# role based check
|
||||
if hasRole(userId, 'admin')
|
||||
if hasAnyRole(userId, ['admin','site-moderator','moderator'])
|
||||
# action is statically associated with the role
|
||||
# action code has to be modified to add/remove role authorization
|
||||
|
||||
```
|
||||
|
||||
Usage:
|
||||
```
|
||||
# assign user to admin role. Permissions scoped globally
|
||||
RocketChat.authz.addUserRoles(userId, ['admin'])
|
||||
|
||||
# assign user to moderator role. Permissions scoped to the specified room
|
||||
# user can moderate (e.g. edit channel name, delete private group message) for only one room specified by the roomId
|
||||
RocketChat.authz.addUserRoles(userId, ['moderator'], roomId )
|
||||
|
||||
# check if user can modify message for any room
|
||||
RocketChat.authz.hasPermission(userId, 'edit-message')
|
||||
|
||||
# check if user can modify message for the specified room. Also returns true if user
|
||||
# has 'edit-message' at global scope.
|
||||
RocketChat.authz.hasPermission(userId, 'edit-message', roomId)
|
||||
```
|
||||
|
||||
Notes:
|
||||
1. Roles are statically defined. UI needs to be implemented to dynamically assign permission(s) to a Role
|
||||
2. 'admin', 'moderator', 'user' role identifiers should NOT be changed (unless you update the associated code) because they are referenced when creating users and creating rooms.
|
||||
3. edit, delete message permissions are at either the global or room scope. i.e. role with edit-message with GLOBAL scope can edit ANY message regardless of the room type. However, role with edit-message with room scope can only edit messages for the room. The global scope is associated with the admin role while the "room-scoped" permission is assigned to the room "moderator" (room creator). If we want a middle ground that allows for edit-message for only channel/group/direct, then we need to create individual edit-c-message, edit-p-message, edit-d-message permissions.
|
||||
12
apps/meteor/server/lib/authorization/canAccessRoom.ts
Normal file
12
apps/meteor/server/lib/authorization/canAccessRoom.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Authorization } from '@rocket.chat/core-services';
|
||||
|
||||
export const canAccessRoomAsync = Authorization.canAccessRoom;
|
||||
export const canAccessRoomIdAsync = Authorization.canAccessRoomId;
|
||||
export const canAccessRoomIdsAsync = Authorization.canAccessRoomIds;
|
||||
export const roomAccessAttributes = {
|
||||
_id: 1,
|
||||
t: 1,
|
||||
teamId: 1,
|
||||
prid: 1,
|
||||
abacAttributes: 1,
|
||||
};
|
||||
77
apps/meteor/server/lib/authorization/canDeleteMessage.ts
Normal file
77
apps/meteor/server/lib/authorization/canDeleteMessage.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { IUser, IRoom } from '@rocket.chat/core-typings';
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { canAccessRoomAsync } from './canAccessRoom';
|
||||
import { hasPermissionAsync } from './hasPermission';
|
||||
import { getValue } from '../../settings/raw';
|
||||
|
||||
const elapsedTime = (ts: Date): number => {
|
||||
const dif = Date.now() - ts.getTime();
|
||||
return Math.round(dif / 1000 / 60);
|
||||
};
|
||||
|
||||
export const canDeleteMessageAsync = async (
|
||||
deletingUser: Pick<IUser, '_id' | 'username'>,
|
||||
{ u, rid, ts }: { u: Pick<IUser, '_id' | 'username'>; rid: string; ts?: Date },
|
||||
): Promise<boolean> => {
|
||||
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 'ro' | 'unmuted' | 't' | 'teamId' | 'prid'>>(rid, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
ro: 1,
|
||||
unmuted: 1,
|
||||
t: 1,
|
||||
teamId: 1,
|
||||
prid: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomAsync(room, { _id: deletingUser._id }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const forceDelete = await hasPermissionAsync(deletingUser._id, 'force-delete-message', rid);
|
||||
|
||||
if (forceDelete) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ts) {
|
||||
return false;
|
||||
}
|
||||
const deleteAllowed = await getValue('Message_AllowDeleting');
|
||||
|
||||
if (!deleteAllowed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedToDeleteAny = await hasPermissionAsync(deletingUser._id, 'delete-message', rid);
|
||||
|
||||
const allowed =
|
||||
allowedToDeleteAny || (deletingUser._id === u._id && (await hasPermissionAsync(deletingUser._id, 'delete-own-message', rid)));
|
||||
if (!allowed) {
|
||||
return false;
|
||||
}
|
||||
const bypassBlockTimeLimit = await hasPermissionAsync(deletingUser._id, 'bypass-time-limit-edit-and-delete', rid);
|
||||
|
||||
if (!bypassBlockTimeLimit) {
|
||||
const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes');
|
||||
|
||||
if (blockDeleteInMinutes) {
|
||||
const timeElapsedForMessage = elapsedTime(ts);
|
||||
return timeElapsedForMessage <= blockDeleteInMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
if (room.ro === true && !(await hasPermissionAsync(deletingUser._id, 'post-readonly', rid))) {
|
||||
// Unless the user was manually unmuted
|
||||
if (deletingUser.username && !(room.unmuted || []).includes(deletingUser.username)) {
|
||||
throw new Error("You can't delete messages because the room is readonly.");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
66
apps/meteor/server/lib/authorization/canSendMessage.ts
Normal file
66
apps/meteor/server/lib/authorization/canSendMessage.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import { Subscriptions, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { canAccessRoomAsync } from './canAccessRoom';
|
||||
import { hasPermissionAsync } from './hasPermission';
|
||||
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
|
||||
import { roomCoordinator } from '../rooms/roomCoordinator';
|
||||
|
||||
const subscriptionOptions = {
|
||||
projection: {
|
||||
blocked: 1,
|
||||
blocker: 1,
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: remove option uid and username and type
|
||||
export async function validateRoomMessagePermissionsAsync(
|
||||
room: IRoom | null,
|
||||
args: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser,
|
||||
extraData?: Record<string, any>,
|
||||
): Promise<void> {
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
if (room.archived) {
|
||||
throw new Error('room_is_archived');
|
||||
}
|
||||
if (args.type !== 'app' && !(await canAccessRoomAsync(room, 'uid' in args ? { _id: args.uid } : args, extraData))) {
|
||||
throw new Error('error-not-allowed');
|
||||
}
|
||||
|
||||
if (
|
||||
await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, 'uid' in args ? args.uid : args._id)
|
||||
) {
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, 'uid' in args ? args.uid : args._id, subscriptionOptions);
|
||||
if (subscription && (subscription.blocked || subscription.blocker)) {
|
||||
throw new Error('room_is_blocked');
|
||||
}
|
||||
}
|
||||
|
||||
if (room.ro === true && !(await hasPermissionAsync('uid' in args ? args.uid : args._id, 'post-readonly', room._id))) {
|
||||
// Unless the user was manually unmuted
|
||||
if (args.username && !(room.unmuted || []).includes(args.username)) {
|
||||
throw new Error("You can't send messages because the room is readonly.");
|
||||
}
|
||||
}
|
||||
|
||||
if (args.username && room?.muted?.includes(args.username)) {
|
||||
throw new Error('You_have_been_muted');
|
||||
}
|
||||
}
|
||||
// TODO: remove option uid and username and type
|
||||
export async function canSendMessageAsync(
|
||||
rid: IRoom['_id'],
|
||||
user: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser,
|
||||
extraData?: Record<string, any>,
|
||||
): Promise<IRoom> {
|
||||
const room = await Rooms.findOneById(rid);
|
||||
if (!room) {
|
||||
throw new Error('error-invalid-room');
|
||||
}
|
||||
|
||||
await validateRoomMessagePermissionsAsync(room, user, extraData);
|
||||
return room;
|
||||
}
|
||||
242
apps/meteor/server/lib/authorization/constant/permissions.ts
Normal file
242
apps/meteor/server/lib/authorization/constant/permissions.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
// Note:
|
||||
// 1.if we need to create a role that can only edit channel message, but not edit group message
|
||||
// then we can define edit-<type>-message instead of edit-message
|
||||
// 2. admin, moderator, and user roles should not be deleted as they are referenced in the code.
|
||||
export const permissions = [
|
||||
{ _id: 'access-permissions', roles: ['admin'] },
|
||||
{ _id: 'access-marketplace', roles: ['admin', 'user'] },
|
||||
{ _id: 'access-setting-permissions', roles: ['admin'] },
|
||||
{ _id: 'add-oauth-service', roles: ['admin'] },
|
||||
{ _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'add-user-to-any-c-room', roles: ['admin'] },
|
||||
{ _id: 'add-user-to-any-p-room', roles: [] },
|
||||
{ _id: 'kick-user-from-any-c-room', roles: ['admin'] },
|
||||
{ _id: 'kick-user-from-any-p-room', roles: [] },
|
||||
{ _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] },
|
||||
{ _id: 'archive-room', roles: ['admin', 'owner'] },
|
||||
{ _id: 'assign-admin-role', roles: ['admin'] },
|
||||
{ _id: 'assign-roles', roles: ['admin'] },
|
||||
{ _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'bulk-register-user', roles: ['admin'] },
|
||||
{ _id: 'change-livechat-room-visitor', roles: ['admin', 'livechat-manager', 'livechat-agent'] },
|
||||
{ _id: 'create-c', roles: ['admin', 'user', 'federated-external', 'bot', 'app'] },
|
||||
{ _id: 'create-d', roles: ['admin', 'user', 'federated-external', 'bot', 'app'] },
|
||||
{ _id: 'create-p', roles: ['admin', 'user', 'federated-external', 'bot', 'app'] },
|
||||
{ _id: 'create-personal-access-tokens', roles: ['admin', 'user'] },
|
||||
{ _id: 'create-user', roles: ['admin'] },
|
||||
{ _id: 'clean-channel-history', roles: ['admin'] },
|
||||
{ _id: 'delete-c', roles: ['admin', 'owner'] },
|
||||
{ _id: 'delete-d', roles: ['admin'] },
|
||||
{ _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'delete-own-message', roles: ['admin', 'user', 'federated-external'] },
|
||||
{ _id: 'delete-p', roles: ['admin', 'owner'] },
|
||||
{ _id: 'delete-user', roles: ['admin'] },
|
||||
{ _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'edit-other-user-active-status', roles: ['admin'] },
|
||||
{ _id: 'edit-other-user-info', roles: ['admin'] },
|
||||
{ _id: 'edit-other-user-password', roles: ['admin'] },
|
||||
{ _id: 'edit-other-user-avatar', roles: ['admin'] },
|
||||
{ _id: 'edit-other-user-e2ee', roles: ['admin'] },
|
||||
{ _id: 'edit-other-user-totp', roles: ['admin'] },
|
||||
{ _id: 'edit-privileged-setting', roles: ['admin'] },
|
||||
{ _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'edit-room-retention-policy', roles: ['admin'] },
|
||||
{ _id: 'force-delete-message', roles: ['admin', 'owner'] },
|
||||
{ _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] },
|
||||
{ _id: 'leave-c', roles: ['admin', 'user', 'federated-external', 'bot', 'anonymous', 'app'] },
|
||||
{ _id: 'leave-p', roles: ['admin', 'user', 'federated-external', 'bot', 'anonymous', 'app'] },
|
||||
{ _id: 'logout-other-user', roles: ['admin'] },
|
||||
{ _id: 'manage-assets', roles: ['admin'] },
|
||||
{ _id: 'manage-email-inbox', roles: ['admin'] },
|
||||
{ _id: 'manage-emoji', roles: ['admin'] },
|
||||
{ _id: 'manage-user-status', roles: ['admin'] },
|
||||
{ _id: 'manage-outgoing-integrations', roles: ['admin'] },
|
||||
{ _id: 'manage-incoming-integrations', roles: ['admin'] },
|
||||
{ _id: 'manage-own-outgoing-integrations', roles: ['admin'] },
|
||||
{ _id: 'manage-own-incoming-integrations', roles: ['admin'] },
|
||||
{ _id: 'manage-oauth-apps', roles: ['admin'] },
|
||||
{ _id: 'manage-selected-settings', roles: ['admin'] },
|
||||
{ _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user', 'federated-external'] },
|
||||
{ _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user', 'federated-external'] },
|
||||
{ _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'run-import', roles: ['admin'] },
|
||||
{ _id: 'run-migration', roles: ['admin'] },
|
||||
{ _id: 'set-moderator', roles: ['admin', 'owner'] },
|
||||
{ _id: 'set-owner', roles: ['admin', 'owner'] },
|
||||
{ _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] },
|
||||
{ _id: 'set-leader', roles: ['admin', 'owner'] },
|
||||
{ _id: 'start-discussion', roles: ['admin', 'user', 'federated-external', 'guest', 'app'] },
|
||||
{ _id: 'start-discussion-other-user', roles: ['admin', 'user', 'federated-external', 'owner', 'app'] },
|
||||
{ _id: 'unarchive-room', roles: ['admin'] },
|
||||
{ _id: 'view-c-room', roles: ['admin', 'user', 'federated-external', 'bot', 'app', 'anonymous'] },
|
||||
{ _id: 'user-generate-access-token', roles: ['admin'] },
|
||||
{ _id: 'view-d-room', roles: ['admin', 'user', 'federated-external', 'bot', 'app', 'guest'] },
|
||||
{ _id: 'view-device-management', roles: ['admin'] },
|
||||
{ _id: 'view-engagement-dashboard', roles: ['admin'] },
|
||||
{ _id: 'view-full-other-user-info', roles: ['admin'] },
|
||||
{ _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] },
|
||||
{ _id: 'view-join-code', roles: ['admin'] },
|
||||
{ _id: 'view-logs', roles: ['admin'] },
|
||||
{ _id: 'view-other-user-channels', roles: ['admin'] },
|
||||
{ _id: 'view-p-room', roles: ['admin', 'user', 'federated-external', 'anonymous', 'guest'] },
|
||||
{ _id: 'view-privileged-setting', roles: ['admin'] },
|
||||
{ _id: 'view-room-administration', roles: ['admin'] },
|
||||
{ _id: 'view-statistics', roles: ['admin'] },
|
||||
{ _id: 'view-user-administration', roles: ['admin'] },
|
||||
{ _id: 'preview-c-room', roles: ['admin', 'user', 'federated-external', 'anonymous'] },
|
||||
{ _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user', 'federated-external'] },
|
||||
{ _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'call-management', roles: ['admin', 'owner', 'moderator', 'user'] },
|
||||
{ _id: 'create-invite-links', roles: ['admin', 'owner', 'moderator'] },
|
||||
{
|
||||
_id: 'view-l-room',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'create-livechat-contact',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'update-livechat-contact',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-contact',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'delete-livechat-contact',
|
||||
roles: ['livechat-manager', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-contact-history',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{ _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'view-omnichannel-contact-center',
|
||||
roles: ['livechat-manager', 'livechat-agent', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{ _id: 'edit-omnichannel-contact', roles: ['livechat-manager', 'livechat-agent', 'admin'] },
|
||||
{ _id: 'view-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'close-livechat-room',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{ _id: 'close-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'on-hold-livechat-room',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'on-hold-others-livechat-room',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{ _id: 'save-others-livechat-room-info', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'remove-closed-livechat-rooms',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{ _id: 'view-livechat-analytics', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'view-livechat-queue',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{ _id: 'transfer-livechat-guest', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{ _id: 'manage-livechat-managers', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'manage-livechat-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'manage-livechat-departments',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{ _id: 'view-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] },
|
||||
{
|
||||
_id: 'add-livechat-department-agents',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-real-time-monitoring',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{ _id: 'view-livechat-triggers', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'view-livechat-customfields', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] },
|
||||
{
|
||||
_id: 'view-livechat-business-hours',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-room-closed-same-department',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-room-closed-by-another-agent',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'view-livechat-room-customfields',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{
|
||||
_id: 'edit-livechat-room-customfields',
|
||||
roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'],
|
||||
},
|
||||
{ _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'mail-messages', roles: ['admin'] },
|
||||
{ _id: 'toggle-room-e2e-encryption', roles: ['owner', 'admin'] },
|
||||
{ _id: 'message-impersonate', roles: ['bot', 'app'] },
|
||||
{ _id: 'create-team', roles: ['admin', 'user'] },
|
||||
{ _id: 'delete-team', roles: ['admin', 'owner'] },
|
||||
{ _id: 'convert-team', roles: ['admin', 'owner'] },
|
||||
{ _id: 'edit-team', roles: ['admin', 'owner'] },
|
||||
{ _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'move-room-to-team', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'create-team-channel', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'create-team-group', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'delete-team-channel', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'delete-team-group', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'view-all-team-channels', roles: ['admin', 'owner'] },
|
||||
{ _id: 'view-all-teams', roles: ['admin'] },
|
||||
{ _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] },
|
||||
|
||||
// New Media calls permissions
|
||||
{ _id: 'allow-internal-voice-calls', roles: ['admin', 'user'] },
|
||||
{ _id: 'allow-external-voice-calls', roles: ['admin', 'user'] },
|
||||
|
||||
{ _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] },
|
||||
{ _id: 'manage-apps', roles: ['admin'] },
|
||||
{ _id: 'post-readonly', roles: ['admin', 'owner', 'moderator'] },
|
||||
{ _id: 'set-readonly', roles: ['admin', 'owner'] },
|
||||
{ _id: 'set-react-when-readonly', roles: ['admin', 'owner'] },
|
||||
{ _id: 'manage-cloud', roles: ['admin'] },
|
||||
{ _id: 'manage-sounds', roles: ['admin'] },
|
||||
{ _id: 'access-mailer', roles: ['admin'] },
|
||||
{ _id: 'pin-message', roles: ['owner', 'moderator', 'admin'] },
|
||||
{ _id: 'mobile-upload-file', roles: ['user', 'federated-external', 'admin'] },
|
||||
{ _id: 'send-mail', roles: ['admin'] },
|
||||
{ _id: 'view-federation-data', roles: ['admin'] },
|
||||
{ _id: 'access-federation', roles: ['admin', 'user', 'federated-external'] },
|
||||
{ _id: 'add-all-to-room', roles: ['admin'] },
|
||||
{ _id: 'get-server-info', roles: ['admin'] },
|
||||
{ _id: 'register-on-cloud', roles: ['admin'] },
|
||||
{ _id: 'test-admin-options', roles: ['admin'] },
|
||||
{ _id: 'test-push-notifications', roles: ['admin', 'user'] },
|
||||
{ _id: 'sync-auth-services-users', roles: ['admin'] },
|
||||
{ _id: 'restart-server', roles: ['admin'] },
|
||||
{ _id: 'remove-slackbridge-links', roles: ['admin'] },
|
||||
{ _id: 'view-import-operations', roles: ['admin'] },
|
||||
{ _id: 'clear-oembed-cache', roles: ['admin'] },
|
||||
{ _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] },
|
||||
{ _id: 'view-moderation-console', roles: ['admin'] },
|
||||
{ _id: 'manage-moderation-actions', roles: ['admin'] },
|
||||
{ _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] },
|
||||
{ _id: 'export-messages-as-pdf', roles: ['admin', 'user'] },
|
||||
];
|
||||
7
apps/meteor/server/lib/authorization/getRoles.ts
Normal file
7
apps/meteor/server/lib/authorization/getRoles.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { IRole } from '@rocket.chat/core-typings';
|
||||
import { Roles } from '@rocket.chat/models';
|
||||
|
||||
export const getRoles = async (): Promise<IRole[]> => Roles.find().toArray();
|
||||
|
||||
export const getRoleIds = async (): Promise<IRole['_id'][]> =>
|
||||
(await Roles.find({}, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id);
|
||||
51
apps/meteor/server/lib/authorization/getUsersInRole.ts
Normal file
51
apps/meteor/server/lib/authorization/getUsersInRole.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { IRole, IUser } from '@rocket.chat/core-typings';
|
||||
import type { FindPaginated } from '@rocket.chat/model-typings';
|
||||
import { Roles, Subscriptions, Users } from '@rocket.chat/models';
|
||||
import { compact } from 'lodash';
|
||||
import type { Document, FindCursor, FindOptions } from 'mongodb';
|
||||
|
||||
export function getUsersInRole(roleId: IRole['_id'], scope?: string): Promise<FindCursor<IUser>>;
|
||||
|
||||
export function getUsersInRole(roleId: IRole['_id'], scope: string | undefined, options: FindOptions<IUser>): Promise<FindCursor<IUser>>;
|
||||
|
||||
export function getUsersInRole<P extends Document = IUser>(
|
||||
roleId: IRole['_id'],
|
||||
scope: string | undefined,
|
||||
options: FindOptions<P extends IUser ? IUser : P>,
|
||||
): Promise<FindCursor<P extends IUser ? IUser : P>>;
|
||||
|
||||
export function getUsersInRole<P = IUser>(
|
||||
roleId: IRole['_id'],
|
||||
scope: string | undefined,
|
||||
options?: any | undefined,
|
||||
): Promise<FindCursor<IUser | P>> {
|
||||
// TODO move the code from Roles.findUsersInRole to here and change all places to use this function
|
||||
return Roles.findUsersInRole(roleId, scope, options);
|
||||
}
|
||||
|
||||
export async function getUsersInRolePaginated(
|
||||
roleId: IRole['_id'],
|
||||
scope: string | undefined,
|
||||
options?: any | undefined,
|
||||
): Promise<FindPaginated<FindCursor<IUser>>> {
|
||||
if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
|
||||
throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.');
|
||||
}
|
||||
|
||||
const role = await Roles.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });
|
||||
if (!role) {
|
||||
throw new Error('role not found');
|
||||
}
|
||||
|
||||
switch (role.scope) {
|
||||
case 'Subscriptions':
|
||||
const subscriptions = await Subscriptions.findByRolesAndRoomId({ roles: role._id, rid: scope }, { projection: { 'u._id': 1 } })
|
||||
.map((subscription) => subscription.u?._id)
|
||||
.toArray();
|
||||
|
||||
return Users.findPaginated({ _id: { $in: compact(subscriptions) } }, options || {});
|
||||
case 'Users':
|
||||
default:
|
||||
return Users.findPaginatedUsersInRoles([role._id], options);
|
||||
}
|
||||
}
|
||||
24
apps/meteor/server/lib/authorization/hasPermission.ts
Normal file
24
apps/meteor/server/lib/authorization/hasPermission.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Authorization } from '@rocket.chat/core-services';
|
||||
import type { UserWithRoles } from '@rocket.chat/core-services';
|
||||
import type { IUser, IPermission, IRoom } from '@rocket.chat/core-typings';
|
||||
|
||||
// Forward only the fields the permission check needs, so a full user document
|
||||
// (with services, e2e keys, etc.) isn't serialized to the authorization service.
|
||||
const toSubject = (user: IUser['_id'] | UserWithRoles): IUser['_id'] | UserWithRoles =>
|
||||
typeof user === 'string' ? user : { _id: user._id, roles: user.roles };
|
||||
|
||||
export const hasAllPermissionAsync = async (
|
||||
user: IUser['_id'] | UserWithRoles,
|
||||
permissions: IPermission['_id'][],
|
||||
scope?: IRoom['_id'],
|
||||
): Promise<boolean> => Authorization.hasAllPermission(toSubject(user), permissions, scope);
|
||||
export const hasPermissionAsync = async (
|
||||
user: IUser['_id'] | UserWithRoles,
|
||||
permissionId: IPermission['_id'],
|
||||
scope?: IRoom['_id'],
|
||||
): Promise<boolean> => Authorization.hasPermission(toSubject(user), permissionId, scope);
|
||||
export const hasAtLeastOnePermissionAsync = async (
|
||||
user: IUser['_id'] | UserWithRoles,
|
||||
permissions: IPermission['_id'][],
|
||||
scope?: IRoom['_id'],
|
||||
): Promise<boolean> => Authorization.hasAtLeastOnePermission(toSubject(user), permissions, scope);
|
||||
31
apps/meteor/server/lib/authorization/hasRole.ts
Normal file
31
apps/meteor/server/lib/authorization/hasRole.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { IRole, IUser, IRoom, ISubscription } from '@rocket.chat/core-typings';
|
||||
import { Roles } from '@rocket.chat/models';
|
||||
|
||||
/**
|
||||
* @deprecated use `Authorization.hasAnyRole` instead
|
||||
*/
|
||||
export const hasAnyRoleAsync = async (
|
||||
userId: IUser['_id'],
|
||||
roleIds: IRole['_id'][],
|
||||
scope?: IRoom['_id'] | undefined,
|
||||
): Promise<boolean> => {
|
||||
if (!Array.isArray(roleIds)) {
|
||||
throw new Error('error-invalid-arguments');
|
||||
}
|
||||
|
||||
if (!userId || userId === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Roles.isUserInRoles(userId, roleIds, scope);
|
||||
};
|
||||
|
||||
export const hasRoleAsync = async (userId: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id'] | undefined): Promise<boolean> => {
|
||||
if (Array.isArray(roleId)) {
|
||||
throw new Error('error-invalid-arguments');
|
||||
}
|
||||
|
||||
return hasAnyRoleAsync(userId, [roleId], scope);
|
||||
};
|
||||
|
||||
export const subscriptionHasRole = (sub: ISubscription, role: IRole['_id']): boolean | undefined => sub.roles?.includes(role);
|
||||
7
apps/meteor/server/lib/authorization/index.ts
Normal file
7
apps/meteor/server/lib/authorization/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { roomAccessAttributes, canAccessRoomAsync } from './canAccessRoom';
|
||||
import { getRoles } from './getRoles';
|
||||
import { getUsersInRole } from './getUsersInRole';
|
||||
import { subscriptionHasRole } from './hasRole';
|
||||
import './streamer/permissions';
|
||||
|
||||
export { getRoles, getUsersInRole, subscriptionHasRole, canAccessRoomAsync, roomAccessAttributes };
|
||||
14
apps/meteor/server/lib/authorization/isABACManagedRoom.ts
Normal file
14
apps/meteor/server/lib/authorization/isABACManagedRoom.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const isABACManagedRoom = (room: Pick<IRoom, 't' | 'abacAttributes'>): boolean =>
|
||||
room.t === 'p' && settings.get<boolean>('ABAC_Enabled') && Array.isArray(room.abacAttributes) && room.abacAttributes.length > 0;
|
||||
|
||||
export const stripABACManagedFieldsForAdmin = <T extends Pick<IRoom, 't' | 'abacAttributes'>>(room: T): T => {
|
||||
if (!isABACManagedRoom(room)) {
|
||||
return room;
|
||||
}
|
||||
const { announcement, topic, description, ...rest } = room as T & Pick<IRoom, 'announcement' | 'topic' | 'description'>;
|
||||
return rest as T;
|
||||
};
|
||||
83
apps/meteor/server/lib/authorization/permissionRole.ts
Normal file
83
apps/meteor/server/lib/authorization/permissionRole.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { License } from '@rocket.chat/core-services';
|
||||
import { Permissions, Roles } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from './hasPermission';
|
||||
import { CONSTANTS, AuthorizationUtils } from '../../../app/authorization/lib';
|
||||
import { notifyOnPermissionChangedById } from '../notifyListener';
|
||||
|
||||
export const addPermissionToRoleMethod = async (uid: string, permissionId: string, role: string): Promise<void> => {
|
||||
if (role === 'guest' && !AuthorizationUtils.hasRestrictionsToRole(role) && (await License.hasValidLicense())) {
|
||||
AuthorizationUtils.addRolePermissionWhiteList(role, await License.getGuestPermissions());
|
||||
}
|
||||
|
||||
if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', {
|
||||
method: 'authorization:addPermissionToRole',
|
||||
action: 'Adding_permission',
|
||||
});
|
||||
}
|
||||
|
||||
const permission = await Permissions.findOneById(permissionId);
|
||||
|
||||
if (!permission) {
|
||||
throw new Meteor.Error('error-invalid-permission', 'Permission does not exist', {
|
||||
method: 'authorization:addPermissionToRole',
|
||||
action: 'Adding_permission',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await Roles.findOneById(role, { projection: { _id: 1 } }))) {
|
||||
throw new Meteor.Error('error-invalid-role', 'Role does not exist', {
|
||||
method: 'authorization:addPermissionToRole',
|
||||
action: 'Adding_permission',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!(await hasPermissionAsync(uid, 'access-permissions')) ||
|
||||
(permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions')))
|
||||
) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', {
|
||||
method: 'authorization:addPermissionToRole',
|
||||
action: 'Adding_permission',
|
||||
});
|
||||
}
|
||||
|
||||
if (permission.groupPermissionId) {
|
||||
await Permissions.addRole(permission.groupPermissionId, role);
|
||||
void notifyOnPermissionChangedById(permission.groupPermissionId);
|
||||
}
|
||||
|
||||
await Permissions.addRole(permission._id, role);
|
||||
|
||||
void notifyOnPermissionChangedById(permission._id);
|
||||
};
|
||||
|
||||
export const removeRoleFromPermissionMethod = async (uid: string, permissionId: string, role: string): Promise<void> => {
|
||||
const permission = await Permissions.findOneById(permissionId);
|
||||
|
||||
if (!permission) {
|
||||
throw new Meteor.Error('error-permission-not-found', 'Permission not found', {
|
||||
method: 'authorization:removeRoleFromPermission',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!(await hasPermissionAsync(uid, 'access-permissions')) ||
|
||||
(permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions')))
|
||||
) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Removing permission is not allowed', {
|
||||
method: 'authorization:removeRoleFromPermission',
|
||||
action: 'Removing_permission',
|
||||
});
|
||||
}
|
||||
|
||||
if (permission.groupPermissionId) {
|
||||
await Permissions.removeRole(permission.groupPermissionId, role);
|
||||
void notifyOnPermissionChangedById(permission.groupPermissionId);
|
||||
}
|
||||
|
||||
await Permissions.removeRole(permission._id, role);
|
||||
void notifyOnPermissionChangedById(permission._id);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { IPermission, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import type { WithId } from 'mongodb';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'permissions/get'(
|
||||
updatedSince?: Date,
|
||||
): Promise<IPermission[] | { update: IPermission[]; remove: WithId<RocketChatRecordDeleted<IPermission>>[] }>;
|
||||
}
|
||||
}
|
||||
|
||||
export const permissionsGetMethod = async (
|
||||
updatedAt?: Date,
|
||||
): Promise<IPermission[] | { update: IPermission[]; remove: WithId<RocketChatRecordDeleted<IPermission>>[] }> => {
|
||||
const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray();
|
||||
|
||||
if (updatedAt instanceof Date) {
|
||||
return {
|
||||
update: records,
|
||||
remove: await Permissions.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(),
|
||||
};
|
||||
}
|
||||
|
||||
return records;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'permissions/get'(updatedAt?: Date) {
|
||||
check(updatedAt, Match.Maybe(Date));
|
||||
// TODO: should we return this for non logged users?
|
||||
// TODO: we could cache this collection
|
||||
|
||||
return permissionsGetMethod(updatedAt);
|
||||
},
|
||||
});
|
||||
123
apps/meteor/server/lib/authorization/upsertPermissions.ts
Normal file
123
apps/meteor/server/lib/authorization/upsertPermissions.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
import type { IPermission, ISetting } from '@rocket.chat/core-typings';
|
||||
import { Permissions, Settings } from '@rocket.chat/models';
|
||||
|
||||
import { permissions } from './constant/permissions';
|
||||
import { getSettingPermissionId, CONSTANTS } from '../../../app/authorization/lib';
|
||||
import { settings } from '../../settings';
|
||||
import { createOrUpdateProtectedRoleAsync } from '../roles/createOrUpdateProtectedRole';
|
||||
|
||||
export const upsertPermissions = async (): Promise<void> => {
|
||||
for (const permission of permissions) {
|
||||
await Permissions.create(permission._id, permission.roles);
|
||||
}
|
||||
|
||||
const defaultRoles = [
|
||||
{ name: 'admin', scope: 'Users', description: 'Admin' },
|
||||
{ name: 'moderator', scope: 'Subscriptions', description: 'Moderator' },
|
||||
{ name: 'leader', scope: 'Subscriptions', description: 'Leader' },
|
||||
{ name: 'owner', scope: 'Subscriptions', description: 'Owner' },
|
||||
{ name: 'user', scope: 'Users', description: '' },
|
||||
{ name: 'federated-external', scope: 'Users', description: '' },
|
||||
{ name: 'bot', scope: 'Users', description: '' },
|
||||
{ name: 'app', scope: 'Users', description: '' },
|
||||
{ name: 'guest', scope: 'Users', description: '' },
|
||||
{ name: 'anonymous', scope: 'Users', description: '' },
|
||||
{ name: 'livechat-agent', scope: 'Users', description: 'Livechat Agent' },
|
||||
{ name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' },
|
||||
] as const;
|
||||
|
||||
for (const role of defaultRoles) {
|
||||
await createOrUpdateProtectedRoleAsync(role.name, role);
|
||||
}
|
||||
|
||||
const getPreviousPermissions = async function (settingId?: string): Promise<Record<string, IPermission>> {
|
||||
const previousSettingPermissions: {
|
||||
[key: string]: IPermission;
|
||||
} = {};
|
||||
|
||||
await Permissions.findByLevel('settings', settingId).forEach((permission: IPermission) => {
|
||||
previousSettingPermissions[permission._id] = permission;
|
||||
});
|
||||
return previousSettingPermissions;
|
||||
};
|
||||
|
||||
const createSettingPermission = async function (
|
||||
setting: ISetting,
|
||||
previousSettingPermissions: {
|
||||
[key: string]: IPermission;
|
||||
},
|
||||
): Promise<void> {
|
||||
const permissionId = getSettingPermissionId(setting._id);
|
||||
const permission: Omit<IPermission, '_id' | '_updatedAt'> = {
|
||||
level: CONSTANTS.SETTINGS_LEVEL as 'settings' | undefined,
|
||||
// copy those setting-properties which are needed to properly publish the setting-based permissions
|
||||
settingId: setting._id,
|
||||
// TODO: migrate settings with group and section with null to undefined
|
||||
...(setting.group && { group: setting.group }),
|
||||
...(setting.section && { section: setting.section }),
|
||||
sorter: setting.sorter,
|
||||
roles: [],
|
||||
};
|
||||
// copy previously assigned roles if available
|
||||
if (previousSettingPermissions[permissionId]?.roles) {
|
||||
permission.roles = previousSettingPermissions[permissionId].roles;
|
||||
}
|
||||
if (setting.group) {
|
||||
permission.groupPermissionId = getSettingPermissionId(setting.group);
|
||||
}
|
||||
if (setting.section) {
|
||||
permission.sectionPermissionId = getSettingPermissionId(setting.section);
|
||||
}
|
||||
|
||||
const existent = await Permissions.findOne(
|
||||
{
|
||||
_id: permissionId,
|
||||
...permission,
|
||||
},
|
||||
{ projection: { _id: 1 } },
|
||||
);
|
||||
|
||||
if (!existent) {
|
||||
try {
|
||||
await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true });
|
||||
} catch (e) {
|
||||
if (!(e as Error).message.includes('E11000')) {
|
||||
// E11000 refers to a MongoDB error that can occur when using unique indexes for upserts
|
||||
// https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes
|
||||
await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete previousSettingPermissions[permissionId];
|
||||
};
|
||||
|
||||
const createPermissionsForExistingSettings = async function (): Promise<void> {
|
||||
const previousSettingPermissions = await getPreviousPermissions();
|
||||
|
||||
const settings = await Settings.findNotHidden().toArray();
|
||||
for await (const setting of settings) {
|
||||
await createSettingPermission(setting, previousSettingPermissions);
|
||||
}
|
||||
|
||||
// remove permissions for non-existent settings
|
||||
for (const obsoletePermission of Object.keys(previousSettingPermissions)) {
|
||||
if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) {
|
||||
await Permissions.deleteOne({ _id: obsoletePermission });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// for each setting which already exists, create a permission to allow changing just this one setting
|
||||
await createPermissionsForExistingSettings();
|
||||
|
||||
// register a callback for settings for be create in higher-level-packages
|
||||
settings.on('*', async ([settingId]) => {
|
||||
const previousSettingPermissions = await getPreviousPermissions(settingId);
|
||||
const setting = await Settings.findOneById(settingId);
|
||||
if (setting && !setting.hidden) {
|
||||
await createSettingPermission(setting, previousSettingPermissions);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user