Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ API.v1.addRoute(
}

const sent = await applyAirGappedRestrictionsValidation(() =>
executeSendMessage(this.userId, this.bodyParams.message as Pick<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
executeSendMessage(this.user, this.bodyParams.message as Pick<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
);
const [message] = await normalizeMessagesForUser([sent], this.userId);

Expand Down
24 changes: 13 additions & 11 deletions apps/meteor/app/authorization/server/functions/canSendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ const subscriptionOptions = {
},
};

// TODO: remove option uid and username and type
export async function validateRoomMessagePermissionsAsync(
room: IRoom | null,
{ uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] },
args: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser,
extraData?: Record<string, any>,
): Promise<void> {
if (!room) {
Expand All @@ -25,40 +26,41 @@ export async function validateRoomMessagePermissionsAsync(
if (room.archived) {
throw new Error('room_is_archived');
}

if (type !== 'app' && !(await canAccessRoomAsync(room, { _id: uid }, extraData))) {
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)) {
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, subscriptionOptions);
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, 'post-readonly', room._id))) {
if (room.ro === true && !(await hasPermissionAsync('uid' in args ? args.uid : args._id, 'post-readonly', room._id))) {
// Unless the user was manually unmuted
if (username && !(room.unmuted || []).includes(username)) {
if (args.username && !(room.unmuted || []).includes(args.username)) {
throw new Error("You can't send messages because the room is readonly.");
}
}

if (username && room?.muted?.includes(username)) {
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'],
{ uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] },
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, { uid, username, type }, extraData);
await validateRoomMessagePermissionsAsync(room, user, extraData);
return room;
}
14 changes: 7 additions & 7 deletions apps/meteor/app/lib/server/methods/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { RateLimiter } from '../lib';
* @returns
*/
export async function executeSendMessage(
uid: IUser['_id'],
uid: IUser['_id'] | IUser,
message: AtLeast<IMessage, 'rid'>,
extraInfo?: { ts?: Date; previewUrls?: string[] },
) {
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function executeSendMessage(
}
}

const user = await Users.findOneById(uid);
const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid;
if (!user?.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
Expand All @@ -95,7 +95,7 @@ export async function executeSendMessage(
check(rid, String);

try {
const room = await canSendMessageAsync(rid, { uid, username: user.username, type: user.type });
const room = await canSendMessageAsync(rid, user);

if (room.encrypted && settings.get<boolean>('E2E_Enable') && !settings.get<boolean>('E2E_Allow_Unencrypted_Messages')) {
if (message.t !== 'e2e') {
Expand All @@ -112,7 +112,7 @@ export async function executeSendMessage(

const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message;
const errorContext: TOptions = err.details ?? {};
void api.broadcast('notify.ephemeralMessage', uid, message.rid, {
void api.broadcast('notify.ephemeralMessage', user._id, message.rid, {
msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }),
});

Expand Down Expand Up @@ -151,8 +151,8 @@ Meteor.methods<ServerMethods>({
sentByEmail: Match.Maybe(Boolean),
});

const uid = Meteor.userId();
if (!uid) {
const user = (await Meteor.userAsync()) as IUser;
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'sendMessage',
});
Expand All @@ -163,7 +163,7 @@ Meteor.methods<ServerMethods>({
}

try {
return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, { previewUrls }));
return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls }));
} catch (error: any) {
if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) {
throw new Meteor.Error(error.error || error.message, error.reason, {
Expand Down
36 changes: 30 additions & 6 deletions apps/meteor/server/services/authorization/canAccessRoom.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Authorization, License, Abac, Settings } from '@rocket.chat/core-services';
import type { RoomAccessValidator } from '@rocket.chat/core-services';
import { TeamType, AbacAccessOperation, AbacObjectType } from '@rocket.chat/core-typings';
import type { IUser, ITeam } from '@rocket.chat/core-typings';
import { Subscriptions, Rooms, TeamMember, Team } from '@rocket.chat/models';
import type { IUser, ITeam, IRoom } from '@rocket.chat/core-typings';
import { Subscriptions, Rooms, TeamMember, Team, Users } from '@rocket.chat/models';

import { canAccessRoomLivechat } from './canAccessRoomLivechat';

Expand All @@ -15,7 +15,13 @@ async function canAccessPublicRoom(user?: Partial<IUser>): Promise<boolean> {
return Authorization.hasPermission(user._id, 'view-c-room');
}

const roomAccessValidators: RoomAccessValidator[] = [
type RoomAccessValidatorConverted = (
room?: Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>,
user?: IUser,
extraData?: Record<string, any>,
) => Promise<boolean>;

const roomAccessValidators: RoomAccessValidatorConverted[] = [
async function _validateAccessToPublicRoomsInTeams(room, user): Promise<boolean> {
if (!room) {
return false;
Expand Down Expand Up @@ -56,8 +62,8 @@ const roomAccessValidators: RoomAccessValidator[] = [
}

const [canViewJoined, canViewT] = await Promise.all([
Authorization.hasPermission(user._id, 'view-joined-room'),
Authorization.hasPermission(user._id, `view-${room.t}-room`),
Authorization.hasPermission(user, 'view-joined-room'),
Authorization.hasPermission(user, `view-${room.t}-room`),
]);

// When there's no ABAC setting, license or values on the room, fallback to previous behavior
Expand Down Expand Up @@ -89,14 +95,32 @@ const roomAccessValidators: RoomAccessValidator[] = [
canAccessRoomLivechat,
];

const isPartialUser = (user: IUser | Pick<IUser, '_id'> | undefined): user is Pick<IUser, '_id'> => {
return Boolean(user && Object.keys(user).length === 1 && '_id' in user);
};

export const canAccessRoom: RoomAccessValidator = async (room, user, extraData): Promise<boolean> => {
// TODO livechat can send both as null, so they we need to validate nevertheless
// if (!room || !user) {
// return false;
// }

// TODO: remove this after migrations
// if user only contains _id, convert it to a full IUser object

if (isPartialUser(user)) {
user = (await Users.findOneById(user._id)) || undefined;
if (!user) {
throw new Error('User not found');
}

if (process.env.NODE_ENV === 'development') {
console.log('User converted to full IUser object');
}
}

for await (const roomAccessValidator of roomAccessValidators) {
if (await roomAccessValidator(room, user, extraData)) {
if (await roomAccessValidator(room, user as IUser, extraData)) {
return true;
}
}
Expand Down
27 changes: 15 additions & 12 deletions apps/meteor/server/services/authorization/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ export class Authorization extends ServiceClass implements IAuthorization {
}
}

async hasAllPermission(userId: string, permissions: string[], scope?: string): Promise<boolean> {
async hasAllPermission(userId: string | IUser, permissions: string[], scope?: string): Promise<boolean> {
if (!userId) {
return false;
}
return this.all(userId, permissions, scope);
}

async hasPermission(userId: string, permissionId: string, scope?: string): Promise<boolean> {
async hasPermission(userId: string | IUser, permissionId: string, scope?: string): Promise<boolean> {
if (!userId) {
return false;
}
return this.all(userId, [permissionId], scope);
}

async hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise<boolean> {
async hasAtLeastOnePermission(userId: string | IUser, permissions: string[], scope?: string): Promise<boolean> {
if (!userId) {
return false;
}
Expand All @@ -85,7 +85,7 @@ export class Authorization extends ServiceClass implements IAuthorization {
return canReadRoom(...args);
}

async canAccessRoomId(rid: IRoom['_id'], uid: IUser['_id']): Promise<boolean> {
async canAccessRoomId(rid: IRoom['_id'], user: IUser['_id']): Promise<boolean> {
const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>>(rid, {
projection: {
_id: 1,
Expand All @@ -100,7 +100,7 @@ export class Authorization extends ServiceClass implements IAuthorization {
return false;
}

return this.canAccessRoom(room, { _id: uid });
return this.canAccessRoom(room, { _id: user });
}

async addRoleRestrictions(role: IRole['_id'], permissions: string[]): Promise<void> {
Expand Down Expand Up @@ -160,17 +160,20 @@ export class Authorization extends ServiceClass implements IAuthorization {
return !!result;
}

private async getRoles(uid: string, scope?: IRoom['_id']): Promise<string[]> {
const { roles: userRoles = [] } = (await Users.findOneById(uid, { projection: { roles: 1 } })) || {};
private async getRoles(user: string | IUser, scope?: IRoom['_id']): Promise<string[]> {
const { roles: userRoles = [] } = typeof user === 'string' ? (await Users.findOneById(user, { projection: { roles: 1 } })) || {} : user;
const { roles: subscriptionsRoles = [] } =
(scope &&
(await Subscriptions.findOne<Pick<ISubscription, 'roles'>>({ 'rid': scope, 'u._id': uid }, { projection: { roles: 1 } }))) ||
(await Subscriptions.findOne<Pick<ISubscription, 'roles'>>(
{ 'rid': scope, 'u._id': typeof user === 'string' ? user : user._id },
{ projection: { roles: 1 } },
))) ||
{};
return [...userRoles, ...subscriptionsRoles].sort((a, b) => a.localeCompare(b));
}

private async atLeastOne(uid: string, permissions: string[] = [], scope?: string): Promise<boolean> {
const sortedRoles = await this.getRolesCached(uid, scope);
private async atLeastOne(user: string | IUser, permissions: string[] = [], scope?: string): Promise<boolean> {
const sortedRoles = await this.getRolesCached(user, scope);
for await (const permission of permissions) {
if (await this.rolesHasPermissionCached(permission, sortedRoles)) {
return true;
Expand All @@ -180,8 +183,8 @@ export class Authorization extends ServiceClass implements IAuthorization {
return false;
}

private async all(uid: string, permissions: string[] = [], scope?: string): Promise<boolean> {
const sortedRoles = await this.getRolesCached(uid, scope);
private async all(user: string | IUser, permissions: string[] = [], scope?: string): Promise<boolean> {
const sortedRoles = await this.getRolesCached(user, scope);
for await (const permission of permissions) {
if (!(await this.rolesHasPermissionCached(permission, sortedRoles))) {
return false;
Expand Down
8 changes: 7 additions & 1 deletion packages/core-services/src/types/IAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings';

export type RoomAccessValidator = (
room?: Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>,
user?: Pick<IUser, '_id'>,
user?: IUser | Pick<IUser, '_id'>,
extraData?: Record<string, any>,
) => Promise<boolean>;

export interface IAuthorization {
hasAllPermission(user: IUser, permissions: string[], scope?: string): Promise<boolean>;
// @deprecated
hasAllPermission(userId: string, permissions: string[], scope?: string): Promise<boolean>;
hasPermission(user: IUser, permissionId: string, scope?: string): Promise<boolean>;
// @deprecated
hasPermission(userId: string, permissionId: string, scope?: string): Promise<boolean>;
hasAtLeastOnePermission(user: IUser, permissions: string[], scope?: string): Promise<boolean>;
// @deprecated
hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise<boolean>;
canAccessRoom: RoomAccessValidator;
canReadRoom: RoomAccessValidator;
Expand Down
Loading