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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View 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.

View 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,
};

View 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;
};

View 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;
}

View 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'] },
];

View 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);

View 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);
}
}

View 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);

View 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);

View 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 };

View 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;
};

View 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);
};

View File

@@ -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);
},
});

View 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);
}
});
};