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,40 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
import { addOAuthService } from '../../lib/oauth/addOAuthService';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
addOAuthService(name: string): void;
}
}
export const addOAuthServiceMethod = async (userId: string, name: string): Promise<void> => {
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-action-not-allowed', 'Adding OAuth Services is not allowed', {
method: 'addOAuthService',
action: 'Adding_OAuth_Services',
});
}
await addOAuthService(name);
};
Meteor.methods<ServerMethods>({
async addOAuthService(name) {
methodDeprecationLogger.method('addOAuthService', '9.0.0', '/v1/settings.addCustomOAuth');
check(name, String);
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addOAuthService' });
}
return addOAuthServiceMethod(userId, name);
},
});

View File

@@ -0,0 +1,23 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { addPermissionToRoleMethod } from '../../lib/authorization/permissionRole';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'authorization:addPermissionToRole'(permissionId: string, role: string): void;
}
}
Meteor.methods<ServerMethods>({
async 'authorization:addPermissionToRole'(permissionId, role) {
methodDeprecationLogger.method('authorization:addPermissionToRole', '9.0.0', '/v1/permissions.addRole');
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'authorization:addPermissionToRole' });
}
await addPermissionToRoleMethod(uid, permissionId, role);
},
});

View File

@@ -0,0 +1,17 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { addSamlService } from '../../lib/saml/lib/settings';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
addSamlService(name: string): void;
}
}
Meteor.methods<ServerMethods>({
addSamlService(name) {
addSamlService(name);
},
});

View File

@@ -0,0 +1,73 @@
import { api } from '@rocket.chat/core-services';
import type { IRole, IUser } from '@rocket.chat/core-typings';
import { Roles, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { addUserRolesAsync } from '../../lib/roles/addUserRoles';
import { settings } from '../../settings';
export const addUserToRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise<boolean> => {
if (!(await hasPermissionAsync(userId, 'access-permissions'))) {
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', {
method: 'authorization:addUserToRole',
action: 'Accessing_permissions',
});
}
if (!roleId || typeof roleId.valueOf() !== 'string' || !username || typeof username.valueOf() !== 'string') {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
method: 'authorization:addUserToRole',
});
}
const role = await Roles.findOneById<Pick<IRole, '_id'>>(roleId, { projection: { _id: 1 } });
if (!role) {
throw new Meteor.Error('error-invalid-role', 'Invalid Role', {
method: 'authorization:addUserToRole',
});
}
if (role._id === 'admin' && !(await hasPermissionAsync(userId, 'assign-admin-role'))) {
throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', {
method: 'authorization:addUserToRole',
action: 'Assign_admin',
});
}
const user = await Users.findOneByUsernameIgnoringCase(username, {
projection: {
_id: 1,
},
});
if (!user?._id) {
throw new Meteor.Error('error-user-not-found', 'User not found', {
method: 'authorization:addUserToRole',
});
}
// verify if user can be added to given scope
if (scope && !(await Roles.canAddUserToRole(user._id, role._id, scope))) {
throw new Meteor.Error('error-invalid-user', 'User is not part of given room', {
method: 'authorization:addUserToRole',
});
}
const add = await addUserRolesAsync(user._id, [role._id], scope);
if (settings.get('UI_DisplayRoles')) {
void api.broadcast('user.roleUpdate', {
type: 'added',
_id: role._id,
u: {
_id: user._id,
username,
},
scope,
});
}
return add;
};

View File

@@ -0,0 +1,28 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
import { runAfterVerifyEmail } from '../../lib/users/runAfterVerifyEmail';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
afterVerifyEmail(): void;
}
}
Meteor.methods<ServerMethods>({
async afterVerifyEmail() {
methodDeprecationLogger.method('afterVerifyEmail', '9.0.0', '/v1/users.verifyEmail');
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'afterVerifyEmail',
});
}
await runAfterVerifyEmail(userId);
},
});

View File

@@ -0,0 +1,19 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { codesRemainingTotp } from '../../lib/2fa/functions/totp';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'2fa:checkCodesRemaining': () => { remaining: number };
}
}
Meteor.methods<ServerMethods>({
async '2fa:checkCodesRemaining'() {
methodDeprecationLogger.method('2fa:checkCodesRemaining', '9.0.0', '/v1/users.totpCodesRemaining');
return codesRemainingTotp(Meteor.userId());
},
});

View File

@@ -0,0 +1,13 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
Meteor.methods<ServerMethods>({
checkRegistrationSecretURL(hash) {
check(hash, String);
return hash === settings.get('Accounts_RegistrationForm_SecretURL');
},
});

View File

@@ -0,0 +1,32 @@
import { Authorization, MeteorError, User } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { Accounts } from 'meteor/accounts-base';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
createToken(userId: string): { userId: string; authToken: string };
}
}
const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env;
export async function generateAccessToken(userId: string, secret: string, caller: IUser) {
if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) {
throw new MeteorError('error-not-authorized', 'Not authorized');
}
if (caller._id !== userId && !(await Authorization.hasPermission(caller, 'user-generate-access-token'))) {
throw new MeteorError('error-not-authorized', 'Not authorized');
}
const token = Accounts._generateStampedLoginToken();
await Accounts._insertLoginToken(userId, token);
await User.ensureLoginTokensLimit(userId);
return {
userId,
authToken: token.token,
};
}

View File

@@ -0,0 +1,81 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { CROWD } from '../../lib/auth-providers/crowd/crowd';
import { logger } from '../../lib/auth-providers/crowd/logger';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { settings } from '../../settings';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
crowd_test_connection(): { message: TranslationKey; params: string[] };
crowd_sync_users(): { message: string; params: string[] };
}
}
Meteor.methods<ServerMethods>({
async crowd_test_connection() {
const user = await Meteor.userAsync();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'crowd_test_connection',
});
}
if (!(await hasPermissionAsync(user, 'test-admin-options'))) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
method: 'crowd_test_connection',
});
}
if (settings.get('CROWD_Enable') !== true) {
throw new Meteor.Error('crowd_disabled');
}
try {
const crowd = new CROWD();
await crowd.checkConnection();
return {
message: 'Crowd_Connection_successful' as const,
params: [],
};
} catch (err) {
logger.error({
msg: 'Invalid crowd connection details, check the url and application username/password and make sure this server is allowed to speak to crowd',
err,
});
throw new Meteor.Error('Invalid connection details', '', { method: 'crowd_test_connection' });
}
},
async crowd_sync_users() {
const user = await Meteor.userAsync();
if (settings.get('CROWD_Enable') !== true) {
throw new Meteor.Error('crowd_disabled');
}
if (!user || !(await hasPermissionAsync(user, 'sync-auth-services-users'))) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
method: 'crowd_sync_users',
});
}
try {
const crowd = new CROWD();
const startTime = Date.now();
await crowd.sync();
const stopTime = Date.now();
const actual = Math.ceil((stopTime - startTime) / 1000);
return {
message: `User data synced in ${actual} seconds`,
params: [],
};
} catch (err) {
logger.error({ msg: 'Error syncing user data. ', err });
throw new Meteor.Error('Error syncing user data', '', { method: 'crowd_sync_users' });
}
},
});

View File

@@ -0,0 +1,43 @@
import type { IOAuthApps } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { OAuthAccessTokens, OAuthApps, OAuthAuthCodes } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
deleteOAuthApp(applicationId: IOAuthApps['_id']): boolean;
}
}
export const deleteOAuthApp = async (userId: string, applicationId: IOAuthApps['_id']): Promise<boolean> => {
if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' });
}
const application = await OAuthApps.findOneAndDeleteById(applicationId, { projection: { clientId: 1 } });
if (!application) {
throw new Meteor.Error('error-application-not-found', 'Application not found', {
method: 'deleteOAuthApp',
});
}
await OAuthAccessTokens.deleteMany({ clientId: application.clientId });
await OAuthAuthCodes.deleteMany({ clientId: application.clientId });
return true;
};
Meteor.methods<ServerMethods>({
async deleteOAuthApp(applicationId) {
methodDeprecationLogger.method('deleteOAuthApp', '9.0.0', '/v1/oauth-apps.delete');
if (!this.userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOAuthApp' });
}
return deleteOAuthApp(this.userId, applicationId);
},
});

View File

@@ -0,0 +1,19 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { disableTotp } from '../../lib/2fa/functions/totp';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'2fa:disable': (code: string) => Promise<boolean>;
}
}
Meteor.methods<ServerMethods>({
async '2fa:disable'(code) {
methodDeprecationLogger.method('2fa:disable', '9.0.0', '/v1/users.disableTotp');
return disableTotp(Meteor.userId(), code);
},
});

View File

@@ -0,0 +1,19 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { enableTotp } from '../../lib/2fa/functions/totp';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'2fa:enable': () => Promise<{ secret: string; url: string }>;
}
}
Meteor.methods<ServerMethods>({
async '2fa:enable'() {
methodDeprecationLogger.method('2fa:enable', '9.0.0', '/v1/users.enableTotp');
return enableTotp(Meteor.userId());
},
});

View File

@@ -0,0 +1,29 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import type { IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { afterLogoutCleanUpCallback } from '../../lib/callbacks/afterLogoutCleanUpCallback';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
logoutCleanUp(user: IUser): Promise<void>;
}
}
Meteor.methods<ServerMethods>({
async logoutCleanUp(user) {
methodDeprecationLogger.method('logoutCleanUp', '9.0.0', '/v1/users.logout');
check(user, Object);
setImmediate(() => {
void afterLogoutCleanUpCallback.run(user);
});
// App IPostUserLogout event hook
await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedOut, user);
},
});

View File

@@ -0,0 +1,36 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
import { refreshLoginServices } from '../../lib/refreshLoginServices';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
refreshOAuthService(): Promise<void>;
}
}
Meteor.methods<ServerMethods>({
async refreshOAuthService() {
methodDeprecationLogger.method('refreshOAuthService', '9.0.0', '/v1/settings.refreshOAuthServices');
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'refreshOAuthService',
});
}
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
method: 'refreshOAuthService',
action: 'Refreshing_OAuth_Services',
});
}
await refreshLoginServices();
},
});

View File

@@ -0,0 +1,19 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { regenerateTotpCodes } from '../../lib/2fa/functions/totp';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'2fa:regenerateCodes': (userToken: string) => { codes: string[] } | undefined;
}
}
Meteor.methods<ServerMethods>({
async '2fa:regenerateCodes'(userToken) {
methodDeprecationLogger.method('2fa:regenerateCodes', '9.0.0', '/v1/users.regenerateTotpCodes');
return regenerateTotpCodes(Meteor.userId(), userToken);
},
});

View File

@@ -0,0 +1,86 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Settings } from '@rocket.chat/models';
import { capitalize } from '@rocket.chat/tools';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
import { notifyOnSettingChangedById } from '../../lib/notifyListener';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
removeOAuthService(name: string): Promise<void>;
}
}
export const removeCustomOAuthSettings = async (name: string): Promise<void> => {
const normalized = capitalize(name.toLowerCase().replace(/[^a-z0-9_]/g, ''));
if (!normalized) {
throw new Meteor.Error('error-invalid-name', 'Invalid OAuth service name', { method: 'removeOAuthService' });
}
const settingsIds = [
`Accounts_OAuth_Custom-${normalized}`,
`Accounts_OAuth_Custom-${normalized}-url`,
`Accounts_OAuth_Custom-${normalized}-token_path`,
`Accounts_OAuth_Custom-${normalized}-identity_path`,
`Accounts_OAuth_Custom-${normalized}-authorize_path`,
`Accounts_OAuth_Custom-${normalized}-scope`,
`Accounts_OAuth_Custom-${normalized}-access_token_param`,
`Accounts_OAuth_Custom-${normalized}-token_sent_via`,
`Accounts_OAuth_Custom-${normalized}-identity_token_sent_via`,
`Accounts_OAuth_Custom-${normalized}-id`,
`Accounts_OAuth_Custom-${normalized}-secret`,
`Accounts_OAuth_Custom-${normalized}-button_label_text`,
`Accounts_OAuth_Custom-${normalized}-button_label_color`,
`Accounts_OAuth_Custom-${normalized}-button_color`,
`Accounts_OAuth_Custom-${normalized}-login_style`,
`Accounts_OAuth_Custom-${normalized}-key_field`,
`Accounts_OAuth_Custom-${normalized}-username_field`,
`Accounts_OAuth_Custom-${normalized}-email_field`,
`Accounts_OAuth_Custom-${normalized}-name_field`,
`Accounts_OAuth_Custom-${normalized}-avatar_field`,
`Accounts_OAuth_Custom-${normalized}-roles_claim`,
`Accounts_OAuth_Custom-${normalized}-merge_roles`,
`Accounts_OAuth_Custom-${normalized}-roles_to_sync`,
`Accounts_OAuth_Custom-${normalized}-merge_users`,
`Accounts_OAuth_Custom-${normalized}-show_button`,
`Accounts_OAuth_Custom-${normalized}-groups_claim`,
`Accounts_OAuth_Custom-${normalized}-channels_admin`,
`Accounts_OAuth_Custom-${normalized}-map_channels`,
`Accounts_OAuth_Custom-${normalized}-groups_channel_map`,
`Accounts_OAuth_Custom-${normalized}-merge_users_distinct_services`,
];
const promises = settingsIds.map((id) => Settings.removeById(id));
(await Promise.all(promises)).forEach((value, index) => {
if (value?.deletedCount) {
void notifyOnSettingChangedById(settingsIds[index], 'removed');
}
});
};
Meteor.methods<ServerMethods>({
async removeOAuthService(name) {
methodDeprecationLogger.method('removeOAuthService', '9.0.0', '/v1/settings.removeCustomOAuth');
check(name, String);
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'removeOAuthService',
});
}
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
}
await removeCustomOAuthSettings(name);
},
});

View File

@@ -0,0 +1,23 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { removeRoleFromPermissionMethod } from '../../lib/authorization/permissionRole';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'authorization:removeRoleFromPermission'(permissionId: string, role: string): void;
}
}
Meteor.methods<ServerMethods>({
async 'authorization:removeRoleFromPermission'(permissionId, role) {
methodDeprecationLogger.method('authorization:removeRoleFromPermission', '9.0.0', '/v1/permissions.removeRole');
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'authorization:removeRoleFromPermission' });
}
await removeRoleFromPermissionMethod(uid, permissionId, role);
},
});

View File

@@ -0,0 +1,77 @@
import { api } from '@rocket.chat/core-services';
import type { IRole, IUser } from '@rocket.chat/core-typings';
import { Roles, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { removeUserFromRolesAsync } from '../../lib/roles/removeUserFromRoles';
import { settings } from '../../settings';
export const removeUserFromRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise<boolean> => {
if (!(await hasPermissionAsync(userId, 'access-permissions'))) {
throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', {
method: 'authorization:removeUserFromRole',
action: 'Accessing_permissions',
});
}
if (!roleId || typeof roleId.valueOf() !== 'string' || !username || typeof username.valueOf() !== 'string') {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
method: 'authorization:removeUserFromRole',
});
}
const role = await Roles.findOneById<Pick<IRole, '_id'>>(roleId, { projection: { _id: 1 } });
if (!role) {
throw new Meteor.Error('error-invalid-role', 'Invalid Role', {
method: 'authorization:removeUserFromRole',
});
}
const user = await Users.findOneByUsernameIgnoringCase(username, {
projection: {
_id: 1,
roles: 1,
},
});
if (!user?._id) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'authorization:removeUserFromRole',
});
}
// prevent removing last user from admin role
if (role._id === 'admin') {
const adminCount = await Users.countDocuments({
roles: {
$in: ['admin'],
},
});
const userIsAdmin = user.roles?.indexOf('admin') > -1;
if (adminCount === 1 && userIsAdmin) {
throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', {
method: 'removeUserFromRole',
action: 'Remove_last_admin',
});
}
}
const remove = await removeUserFromRolesAsync(user._id, [role._id], scope);
const event = {
type: 'removed',
_id: role._id,
u: {
_id: user._id,
username,
},
scope,
} as const;
if (settings.get('UI_DisplayRoles')) {
void api.broadcast('user.roleUpdate', event);
}
void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: userId });
return remove;
};

View File

@@ -0,0 +1,78 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import type { IServiceProviderOptions } from '../../lib/saml/definition/IServiceProviderOptions';
import { SAMLServiceProvider } from '../../lib/saml/lib/ServiceProvider';
import { SAMLUtils } from '../../lib/saml/lib/Utils';
/**
* Fetch SAML provider configs for given 'provider'.
*/
function getSamlServiceProviderOptions(provider: string): IServiceProviderOptions {
if (!provider) {
throw new Meteor.Error('no-saml-provider', 'SAML internal error', {
method: 'getSamlServiceProviderOptions',
});
}
const providers = SAMLUtils.serviceProviders;
const samlProvider = function (element: IServiceProviderOptions): boolean {
return element.provider === provider;
};
return providers.filter(samlProvider)[0];
}
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
samlLogout(provider: string): string | undefined;
}
}
Meteor.methods<ServerMethods>({
async samlLogout(provider: string) {
const userId = Meteor.userId();
// Make sure the user is logged in before we initiate SAML Logout
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'samlLogout' });
}
const providerConfig = getSamlServiceProviderOptions(provider);
SAMLUtils.log({ msg: 'Logout request', providerConfig });
// This query should respect upcoming array of SAML logins
const user = await Users.getSAMLByIdAndSAMLProvider(userId, provider);
if (!user?.services?.saml) {
return;
}
const { nameID, idpSession } = user.services.saml;
SAMLUtils.log({ msg: `NameID for user ${Meteor.userId()} found`, nameID });
const _saml = new SAMLServiceProvider(providerConfig);
if (!nameID || !idpSession) {
SAMLUtils.log({ msg: 'No NameID or idpSession found for user', userId });
return;
}
const request = _saml.generateLogoutRequest({
nameID: nameID || idpSession,
sessionIndex: idpSession,
});
SAMLUtils.log({ request, msg: '----Logout Request---' });
// request.request: actual XML SAML Request
// request.id: comminucation id which will be mentioned in the ResponseTo field of SAMLResponse
await Users.setSamlInResponseTo(userId, request.id);
const result = await _saml.requestToUrl(request.request, 'logout');
SAMLUtils.log({ msg: 'SAML Logout Request URL generated', result });
return result;
},
});

View File

@@ -0,0 +1,31 @@
import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
export const sendConfirmationEmail = async (to: string): Promise<boolean> => {
check(to, String);
const email = to.trim();
const user = await Users.findOneByEmailAddress(email, { projection: { emails: 1 } });
if (!user) {
return false;
}
// Do not re-send a verification mail to an address that is already verified.
const target = user.emails?.find((e) => e.address.toLowerCase() === email.toLowerCase() && !e.verified);
if (!target) {
return false;
}
try {
Accounts.sendVerificationEmail(user._id, target.address);
return true;
} catch (error: any) {
throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, {
method: 'registerUser',
message: error.message,
});
}
};

View File

@@ -0,0 +1,59 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { check } from 'meteor/check';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { Meteor } from 'meteor/meteor';
import { SystemLogger } from '../../lib/logger/system';
import { settings } from '../../settings';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
sendForgotPasswordEmail(to: string): boolean | undefined;
}
}
export const sendForgotPasswordEmail = async (to: string): Promise<boolean | undefined> => {
const email = to.trim().toLowerCase();
const user = await Users.findOneByEmailAddress(email, { projection: { _id: 1, services: 1 } });
if (!user) {
return true;
}
if (user.services && !user.services.password) {
if (!settings.get('Accounts_AllowPasswordChangeForOAuthUsers')) {
return false;
}
}
try {
Accounts.sendResetPasswordEmail(user._id, email);
return true;
} catch (err) {
SystemLogger.error({ err });
}
};
Meteor.methods<ServerMethods>({
async sendForgotPasswordEmail(to) {
check(to, String);
return sendForgotPasswordEmail(to);
},
});
DDPRateLimiter.addRule(
{
type: 'method',
name: 'sendForgotPasswordEmail',
clientAddress() {
return true;
},
},
10,
60000,
);

View File

@@ -0,0 +1,82 @@
import type { IOAuthApps } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { OAuthApps, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { parseUriList } from '../../lib/auth/oauth2-server/parseUriList';
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
updateOAuthApp(
applicationId: IOAuthApps['_id'],
application: Pick<IOAuthApps, 'name' | 'redirectUri' | 'active'>,
): Promise<IOAuthApps | null>;
}
}
export const updateOAuthApp = async (
userId: string,
applicationId: IOAuthApps['_id'],
application: Pick<IOAuthApps, 'name' | 'redirectUri' | 'active'>,
): Promise<IOAuthApps | null> => {
if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' });
}
if (!application.name || typeof application.name.valueOf() !== 'string' || application.name.trim() === '') {
throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' });
}
if (!application.redirectUri || typeof application.redirectUri.valueOf() !== 'string' || application.redirectUri.trim() === '') {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'updateOAuthApp',
});
}
if (typeof application.active !== 'boolean') {
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
method: 'updateOAuthApp',
});
}
const redirectUri = parseUriList(application.redirectUri);
if (redirectUri.length === 0) {
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
method: 'updateOAuthApp',
});
}
const updatedApplication = await OAuthApps.updateById(applicationId, {
name: application.name,
active: application.active,
redirectUri,
_updatedBy: await Users.findOneById(userId, {
projection: {
username: 1,
},
}),
});
if (updatedApplication == null) {
throw new Meteor.Error('error-application-not-found', 'Application not found', {
method: 'updateOAuthApp',
});
}
return updatedApplication;
};
Meteor.methods<ServerMethods>({
async updateOAuthApp(applicationId, application) {
methodDeprecationLogger.method('updateOAuthApp', '9.0.0', '/v1/oauth-apps.update');
if (!this.userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOAuthApp' });
}
return updateOAuthApp(this.userId, applicationId, application);
},
});

View File

@@ -0,0 +1,20 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { validateTotpTempToken } from '../../lib/2fa/functions/totp';
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'2fa:validateTempToken': (userToken: string) => { codes: string[] } | undefined;
}
}
Meteor.methods<ServerMethods>({
async '2fa:validateTempToken'(userToken) {
methodDeprecationLogger.method('2fa:validateTempToken', '9.0.0', '/v1/users.validateTotp');
const { 'x-auth-token': xAuthToken } = this.connection?.httpHeaders ?? {};
return validateTotpTempToken(Meteor.userId(), userToken, xAuthToken);
},
});