Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/odd-gorillas-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes issue when trying to create an unencrypted discussion when a parent channel is encrypted
24 changes: 18 additions & 6 deletions apps/meteor/app/2fa/server/twoFactorRequired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@ import { Meteor } from 'meteor/meteor';
import type { ITwoFactorOptions } from './code/index';
import { checkCodeForUser } from './code/index';

export function twoFactorRequired<TFunction extends (this: Meteor.MethodThisType, ...args: any[]) => any>(
fn: TFunction,
export type AuthenticatedContext = {
userId: string;
token: string;
connection: {
id: string;
clientAddress: string;
httpHeaders: Record<string, string>;
};
twoFactorChecked?: boolean;
};

export const twoFactorRequired = <TFunction extends (this: any, ...args: any) => Promise<any>>(
fn: ThisParameterType<TFunction> extends AuthenticatedContext
? TFunction
: (this: AuthenticatedContext, ...args: Parameters<TFunction>) => ReturnType<TFunction>,
options?: ITwoFactorOptions,
): (this: Meteor.MethodThisType, ...args: Parameters<TFunction>) => Promise<ReturnType<TFunction>> {
return async function (this: Meteor.MethodThisType, ...args: Parameters<TFunction>): Promise<ReturnType<TFunction>> {
) =>
async function (this, ...args) {
if (!this.userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'twoFactorRequired' });
}
Expand Down Expand Up @@ -35,5 +48,4 @@ export function twoFactorRequired<TFunction extends (this: Meteor.MethodThisType
}

return fn.apply(this, args);
};
}
} as (this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) => ReturnType<TFunction>;
72 changes: 51 additions & 21 deletions apps/meteor/app/api/server/ApiClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,18 +496,16 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
public async processTwoFactor({
userId,
request,
invocation,
options,
connection,
}: {
userId: string;
request: Request;
invocation: { twoFactorChecked?: boolean };
options?: Options;
connection: IMethodConnection;
}): Promise<void> {
}): Promise<boolean> {
if (options && (!('twoFactorRequired' in options) || !options.twoFactorRequired)) {
return;
return false;
}
const code = request.headers.get('x-2fa-code') ? String(request.headers.get('x-2fa-code')) : undefined;
const method = request.headers.get('x-2fa-method') ? String(request.headers.get('x-2fa-method')) : undefined;
Expand All @@ -520,7 +518,7 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
connection,
});

invocation.twoFactorChecked = true;
return true;
}

public getFullRouteName(route: string, method: string): string {
Expand Down Expand Up @@ -902,30 +900,28 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
}
}

const invocation = new DDPCommon.MethodInvocation({
connection,
isSimulation: false,
userId: this.userId,
});

Accounts._accountData[connection.id] = {
connection,
};

Accounts._setAccountData(connection.id, 'loginToken', this.token!);

this.userId &&
if (
this.userId &&
(await api.processTwoFactor({
userId: this.userId,
request: this.request,
invocation: invocation as unknown as Record<string, any>,
options: _options,
connection: connection as unknown as IMethodConnection,
}));
}))
) {
this.twoFactorChecked = true;
}

this.parseJsonQuery = () => api.parseJsonQuery(this);

result = (await DDP._CurrentInvocation.withValue(invocation as any, async () => originalAction.apply(this))) || api.success();
if (options.applyMeteorContext) {
const invocation = APIClass.createMeteorInvocation(connection, this.userId, this.token);
result = await invocation
.applyInvocation(() => originalAction.apply(this))
.finally(() => invocation[Symbol.asyncDispose]());
} else {
result = await originalAction.apply(this);
}
} catch (e: any) {
result = ((e: any) => {
switch (e.error) {
Expand Down Expand Up @@ -1209,4 +1205,38 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
},
);
}

static createMeteorInvocation(
connection: {
id: string;
close: () => void;
clientAddress: string;
httpHeaders: Record<string, any>;
},
userId?: string,
token?: string,
) {
const invocation = new DDPCommon.MethodInvocation({
connection,
isSimulation: false,
userId,
});

Accounts._accountData[connection.id] = {
connection,
};
if (token) {
Accounts._setAccountData(connection.id, 'loginToken', token);
}

return {
invocation,
applyInvocation: <F extends () => Promise<any>>(action: F): ReturnType<F> => {
return DDP._CurrentInvocation.withValue(invocation as any, async () => action()) as ReturnType<F>;
},
[Symbol.asyncDispose]() {
return Promise.resolve();
},
};
}
}
3 changes: 3 additions & 0 deletions apps/meteor/app/api/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export type SharedOptions<TMethod extends string> = (
version: DeprecationLoggerNextPlannedVersion;
alternatives?: PathPattern[];
};
applyMeteorContext?: boolean;
};

export type GenericRouteExecutionContext = ActionThis<any, any, any>;
Expand Down Expand Up @@ -191,6 +192,8 @@ export type ActionThis<TMethod extends Method, TPathPattern extends PathPattern,
readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never;
readonly queryFields: TOptions extends { queryFields: infer T } ? T : never;

readonly twoFactorChecked: boolean;

parseJsonQuery(): Promise<{
sort: Record<string, 1 | -1>;
/**
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/lib/eraseTeam.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('eraseTeam (TypeScript) module', () => {

await subject.eraseTeam(user, team, []);

sinon.assert.calledWith(eraseRoomStub, team.roomId, 'u1');
sinon.assert.calledWith(eraseRoomStub, team.roomId, user);
});
});

Expand Down
16 changes: 8 additions & 8 deletions apps/meteor/app/api/server/lib/eraseTeam.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import { MeteorError, Team } from '@rocket.chat/core-services';
import type { AtLeast, IRoom, ITeam, IUser } from '@rocket.chat/core-typings';
import type { IRoom, ITeam, IUser, AtLeast } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';

import { eraseRoom } from '../../../../server/lib/eraseRoom';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { deleteRoom } from '../../../lib/server/functions/deleteRoom';

type eraseRoomFnType = (rid: string, user: AtLeast<IUser, '_id' | 'username' | 'name'>) => Promise<boolean | void>;
type EraseRoomFnType = <T extends AtLeast<IUser, '_id' | 'name' | 'username'>>(rid: string, user: T) => Promise<boolean | void>;

export const eraseTeamShared = async (
user: AtLeast<IUser, '_id' | 'username' | 'name'>,
export const eraseTeamShared = async <T extends AtLeast<IUser, '_id' | 'name' | 'username'>>(
user: T,
team: ITeam,
roomsToRemove: IRoom['_id'][] = [],
eraseRoomFn: eraseRoomFnType,
eraseRoomFn: EraseRoomFnType,
) => {
const rooms: string[] = roomsToRemove.length
? (await Team.getMatchingTeamRooms(team._id, roomsToRemove)).filter((roomId) => roomId !== team.roomId)
Expand Down Expand Up @@ -41,9 +41,9 @@ export const eraseTeamShared = async (
await Team.deleteById(team._id);
};

export const eraseTeam = async (user: AtLeast<IUser, '_id' | 'username' | 'name'>, team: ITeam, roomsToRemove: IRoom['_id'][]) => {
export const eraseTeam = async (user: IUser, team: ITeam, roomsToRemove: IRoom['_id'][]) => {
await eraseTeamShared(user, team, roomsToRemove, async (rid, user) => {
return eraseRoom(rid, user._id);
return eraseRoom(rid, user);
});
};

Expand All @@ -54,7 +54,7 @@ export const eraseTeam = async (user: AtLeast<IUser, '_id' | 'username' | 'name'
*/
export const eraseTeamOnRelinquishRoomOwnerships = async (team: ITeam, roomsToRemove: IRoom['_id'][] = []): Promise<string[]> => {
const deletedRooms = new Set<string>();
await eraseTeamShared({ _id: 'rocket.cat', username: 'rocket.cat', name: 'Rocket.Cat' }, team, roomsToRemove, async (rid) => {
await eraseTeamShared({ _id: 'rocket.cat', username: 'rocket.cat', name: 'Rocket.Cat' } as IUser, team, roomsToRemove, async (rid) => {
const isDeleted = await eraseRoomLooseValidation(rid);
if (isDeleted) {
deletedRooms.add(rid);
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/lib/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid
name: 1,
t: 1,
avatarETag: 1,
encrypted: 1,
},
limit: 10,
sort: {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ API.v1.addRoute(
checkedArchived: false,
});

await eraseRoom(room._id, this.userId);
await eraseRoom(room._id, this.user);

return API.v1.success();
},
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ API.v1.addRoute(
throw new Meteor.Error('The required "mid" body param is missing.');
}

await followMessage(this.userId, { mid });
await followMessage(this.user, { mid });

return API.v1.success();
},
Expand All @@ -822,7 +822,7 @@ API.v1.addRoute(
throw new Meteor.Error('The required "mid" body param is missing.');
}

await unfollowMessage(this.userId, { mid });
await unfollowMessage(this.user, { mid });

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ API.v1.addRoute(
checkedArchived: false,
});

await eraseRoom(findResult.rid, this.userId);
await eraseRoom(findResult.rid, this.user);

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/im.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const dmDeleteAction = <Path extends string>(_path: Path): TypedAction<typeof dm
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

await eraseRoom(room._id, this.userId);
await eraseRoom(room._id, this.user);

return API.v1.success();
};
Expand Down
9 changes: 5 additions & 4 deletions apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ API.v1.addRoute(
authRequired: true,
rateLimiterOptions: false,
validateParams: isMeteorCall,
applyMeteorContext: true,
},
{
async post() {
Expand Down Expand Up @@ -515,8 +516,7 @@ API.v1.addRoute(
});
}

const result = await Meteor.callAsync(method, ...params);
return API.v1.success(mountResult({ id, result }));
return API.v1.success(mountResult({ id, result: await Meteor.callAsync(method, ...params) }));
} catch (err) {
if (!(err as any).isClientSafe && !(err as any).meteorError) {
SystemLogger.error({ msg: 'Exception while invoking method', err, method });
Expand All @@ -531,12 +531,14 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'method.callAnon/:method',
{
authRequired: false,
rateLimiterOptions: false,
validateParams: isMeteorCall,
applyMeteorContext: true,
},
{
async post() {
Expand Down Expand Up @@ -572,8 +574,7 @@ API.v1.addRoute(
});
}

const result = await Meteor.callAsync(method, ...params);
return API.v1.success(mountResult({ id, result }));
return API.v1.success(mountResult({ id, result: await Meteor.callAsync(method, ...params) }));
} catch (err) {
if (!(err as any).isClientSafe && !(err as any).meteorError) {
SystemLogger.error({ msg: 'Exception while invoking method', err, method });
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ API.v1.addRoute(
});
}

await eraseRoom(room, this.userId);
await eraseRoom(room, this.user);

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ API.v1.addRoute(

if (rooms.length) {
for await (const room of rooms) {
await eraseRoom(room, this.userId);
await eraseRoom(room, this.user);
}
}

Expand Down
17 changes: 4 additions & 13 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/persona
import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken';
import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged';
import { i18n } from '../../../../server/lib/i18n';
import { removeOtherTokens } from '../../../../server/lib/removeOtherTokens';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { registerUser } from '../../../../server/methods/registerUser';
import { requestDataDownload } from '../../../../server/methods/requestDataDownload';
Expand Down Expand Up @@ -187,13 +186,7 @@ API.v1.addRoute(
twoFactorMethod: 'password',
};

await executeSaveUserProfile.call(
this as unknown as Meteor.MethodThisType,
this.user,
userData,
this.bodyParams.customFields,
twoFactorOptions,
);
await executeSaveUserProfile.call(this, this.user, userData, this.bodyParams.customFields, twoFactorOptions);

return API.v1.success({
user: await Users.findOneById(this.userId, { projection: API.v1.defaultFieldsToExclude }),
Expand Down Expand Up @@ -1239,7 +1232,7 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
return API.v1.success(await removeOtherTokens(this.userId, this.connection.id));
return API.v1.success(await Users.removeNonLoginTokensExcept(this.userId, this.token));
},
},
);
Expand Down Expand Up @@ -1410,9 +1403,7 @@ API.v1.addRoute(
});
}

const user = await (async (): Promise<
Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'statusText' | 'roles'> | undefined | null
> => {
const user = await (async () => {
if (isUserFromParams(this.bodyParams, this.userId, this.user)) {
return Users.findOneById(this.userId);
}
Expand All @@ -1429,7 +1420,7 @@ API.v1.addRoute(
let { statusText, status } = user;

if (this.bodyParams.message || this.bodyParams.message === '') {
await setStatusText(user._id, this.bodyParams.message, { emit: false });
await setStatusText(user, this.bodyParams.message, { emit: false });
statusText = this.bodyParams.message;
}

Expand Down
Loading
Loading