Skip to content

Commit

Permalink
feat: New permission to kick users (#30535)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <[email protected]>
  • Loading branch information
sampaiodiego and ggazzo authored Oct 6, 2023
1 parent bdc9d8c commit a8718ed
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-carpets-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Add new permission to allow kick users from rooms without being a member
74 changes: 40 additions & 34 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,7 @@ import { getLoggedInUser } from '../helpers/getLoggedInUser';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams';

// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property
async function findPrivateGroupByIdOrName({
params,
checkedArchived = true,
userId,
}: {
params:
| {
roomId?: string;
}
| {
roomName?: string;
};
userId: string;
checkedArchived?: boolean;
}): Promise<{
rid: string;
open: boolean;
ro: boolean;
t: string;
name: string;
broadcast: boolean;
}> {
async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise<IRoom> {
if (
(!('roomId' in params) && !('roomName' in params)) ||
('roomId' in params && !(params as { roomId?: string }).roomId && 'roomName' in params && !(params as { roomName?: string }).roomName)
Expand All @@ -68,17 +46,48 @@ async function findPrivateGroupByIdOrName({
broadcast: 1,
},
};
let room: IRoom | null = null;
if ('roomId' in params) {
room = await Rooms.findOneById(params.roomId || '', roomOptions);
} else if ('roomName' in params) {
room = await Rooms.findOneByName(params.roomName || '', roomOptions);
}

const room = await (() => {
if ('roomId' in params) {
return Rooms.findOneById(params.roomId || '', roomOptions);
}
if ('roomName' in params) {
return Rooms.findOneByName(params.roomName || '', roomOptions);
}
})();

if (!room || room.t !== 'p') {
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
}

return room;
}

// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property
async function findPrivateGroupByIdOrName({
params,
checkedArchived = true,
userId,
}: {
params:
| {
roomId?: string;
}
| {
roomName?: string;
};
userId: string;
checkedArchived?: boolean;
}): Promise<{
rid: string;
open: boolean;
ro: boolean;
t: string;
name: string;
broadcast: boolean;
}> {
const room = await getRoomFromParams(params);

const user = await Users.findOneById(userId, { projections: { username: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
Expand Down Expand Up @@ -585,17 +594,14 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
const findResult = await findPrivateGroupByIdOrName({
params: this.bodyParams,
userId: this.userId,
});
const room = await getRoomFromParams(this.bodyParams);

const user = await getUserFromParams(this.bodyParams);
if (!user?.username) {
return API.v1.failure('Invalid user');
}

await removeUserFromRoomMethod(this.userId, { rid: findResult.rid, username: user.username });
await removeUserFromRoomMethod(this.userId, { rid: room._id, username: user.username });

return API.v1.success();
},
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/app/authorization/server/constant/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const permissions = [
{ _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'] },
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,10 @@
"Jump_to_message": "Jump to message",
"Jump_to_recent_messages": "Jump to recent messages",
"Just_invited_people_can_access_this_channel": "Just invited people can access this channel.",
"kick-user-from-any-c-room": "Kick User from Any Public Channel",
"kick-user-from-any-c-room_description": "Permission to kick a user from any public channel",
"kick-user-from-any-p-room": "Kick User from Any Private Channel",
"kick-user-from-any-p-room_description": "Permission to kick a user from any private channel",
"Katex_Dollar_Syntax": "Allow Dollar Syntax",
"Katex_Dollar_Syntax_Description": "Allow using $$katex block$$ and $inline katex$ syntaxes",
"Katex_Enabled": "Katex Enabled",
Expand Down
27 changes: 21 additions & 6 deletions apps/meteor/server/lib/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,24 @@ export async function migrateDatabase(targetVersion: 'latest' | number, subcomma
return true;
}

export const onFreshInstall =
(await getControl()).version !== 0
? async (): Promise<void> => {
/* noop */
}
: (fn: () => unknown): unknown => fn();
export async function onServerVersionChange(cb: () => Promise<void>): Promise<void> {
const result = await Migrations.findOneAndUpdate(
{
_id: 'upgrade',
},
{
$set: {
hash: Info.commit.hash,
},
},
{
upsert: true,
},
);

if (result.value?.hash === Info.commit.hash) {
return;
}

await cb();
}
28 changes: 19 additions & 9 deletions apps/meteor/server/methods/removeUserFromRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { getUsersInRole } from '../../app/authorization/server';
import { canAccessRoomAsync, getUsersInRole } from '../../app/authorization/server';
import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission';
import { hasRoleAsync } from '../../app/authorization/server/functions/hasRole';
import { RoomMemberActions } from '../../definition/IRoomTypeConfig';
Expand Down Expand Up @@ -35,22 +35,32 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri
});
}

const removedUser = await Users.findOneByUsernameIgnoringCase(data.username);

const fromUser = await Users.findOneById(fromId);
if (!fromUser) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'removeUserFromRoom',
});
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, {
projection: { _id: 1 },
});
if (!subscription) {
throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', {
method: 'removeUserFromRoom',
// did this way so a ctrl-f would find the permission being used
const kickAnyUserPermission = room.t === 'c' ? 'kick-user-from-any-c-room' : 'kick-user-from-any-p-room';

const canKickAnyUser = await hasPermissionAsync(fromId, kickAnyUserPermission);
if (!canKickAnyUser && !(await canAccessRoomAsync(room, fromUser))) {
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
}

const removedUser = await Users.findOneByUsernameIgnoringCase(data.username);

if (!canKickAnyUser) {
const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, {
projection: { _id: 1 },
});
if (!subscription) {
throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', {
method: 'removeUserFromRoom',
});
}
}

if (await hasRoleAsync(removedUser._id, 'owner', room._id)) {
Expand Down
6 changes: 4 additions & 2 deletions apps/meteor/server/startup/migrations/xrun.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions';
import { migrateDatabase, onFreshInstall } from '../../lib/migrations';
import { migrateDatabase, onServerVersionChange } from '../../lib/migrations';

const { MIGRATION_VERSION = 'latest' } = process.env;

const [version, ...subcommands] = MIGRATION_VERSION.split(',');

await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands);
await onFreshInstall(upsertPermissions);

// if the server is starting with a different version we update the permissions
await onServerVersionChange(() => upsertPermissions());
1 change: 1 addition & 0 deletions packages/core-typings/src/migrations/IControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type IControl = {
_id: string;
version: number;
locked: boolean;
hash?: string;
buildAt?: string | Date;
lockedAt?: string | Date;
};

0 comments on commit a8718ed

Please sign in to comment.