Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/add-users-rest-param-validations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Migrated `users.getAvatar`, `users.deleteOwnAccount`, `users.resetAvatar`, and `users.forgotPassword` to explicit `Ajv` schema validation. Similarly, `groups.list` and `groups.listAll` were migrated to ensure strict parameter validation.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ storybook-static
development/tempo-data/

.env

# Antigravity local folders
.agent/
.optimization/
18 changes: 9 additions & 9 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Team, isMeteorError } from '@rocket.chat/core-services';
import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings';
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings';
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps, isGroupsListProps } from '@rocket.chat/rest-typings';
import { isTruthy } from '@rocket.chat/tools';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -81,12 +81,12 @@ async function findPrivateGroupByIdOrName({
userId,
}: {
params:
| {
roomId?: string;
}
| {
roomName?: string;
};
| {
roomId?: string;
}
| {
roomName?: string;
};
userId: string;
checkedArchived?: boolean;
}): Promise<{
Expand Down Expand Up @@ -653,7 +653,7 @@ API.v1.addRoute(
// List Private Groups a user has access to
API.v1.addRoute(
'groups.list',
{ authRequired: true },
{ authRequired: true, validateParams: isGroupsListProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
Expand Down Expand Up @@ -692,7 +692,7 @@ API.v1.addRoute(

API.v1.addRoute(
'groups.listAll',
{ authRequired: true, permissionsRequired: ['view-room-administration'] },
{ authRequired: true, permissionsRequired: ['view-room-administration'], validateParams: isGroupsListProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
Expand Down
63 changes: 36 additions & 27 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
ajv,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
isUsersGetAvatarProps,
isUsersDeleteOwnAccountProps,
isUsersResetAvatarProps,
isUsersForgotPasswordProps,
} from '@rocket.chat/rest-typings';
import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
Expand Down Expand Up @@ -83,7 +87,10 @@ import { findPaginatedUsersByStatus, findUsersToAutocomplete, getInclusiveFields

API.v1.addRoute(
'users.getAvatar',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersGetAvatarProps,
},
{
async get() {
const user = await getUserFromParams(this.queryParams);
Expand Down Expand Up @@ -171,9 +178,9 @@ API.v1.addRoute(
const twoFactorOptions = !userData.typedPassword
? null
: {
twoFactorCode: userData.typedPassword,
twoFactorMethod: 'password',
};
twoFactorCode: userData.typedPassword,
twoFactorMethod: 'password',
};

await executeSaveUserProfile.call(this, this.user, userData, this.bodyParams.customFields, twoFactorOptions);

Expand Down Expand Up @@ -361,19 +368,18 @@ API.v1.addRoute(

API.v1.addRoute(
'users.deleteOwnAccount',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersDeleteOwnAccountProps,
},
{
async post() {
const { password } = this.bodyParams;
if (!password) {
return API.v1.failure('Body parameter "password" is required.');
}
const { password, confirmRelinquish = false } = this.bodyParams;

if (!settings.get('Accounts_AllowDeleteOwnAccount')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

const { confirmRelinquish = false } = this.bodyParams;

await deleteUserOwnAccount(this.userId, password, confirmRelinquish);

return API.v1.success();
Expand Down Expand Up @@ -536,13 +542,18 @@ API.v1.addRoute(
const limit =
count !== 0
? [
{
$limit: count,
},
]
{
$limit: count,
},
]
: [];

const result = await Users.col
const [
{
sortedResults: users,
totalCount: [{ total } = { total: 0 }],
} = { sortedResults: [], totalCount: [] },
] = await Users.col
.aggregate<{ sortedResults: IUser[]; totalCount: { total: number }[] }>([
{
$match: nonEmptyQuery,
Expand Down Expand Up @@ -574,11 +585,6 @@ API.v1.addRoute(
])
.toArray();

const {
sortedResults: users,
totalCount: [{ total } = { total: 0 }],
} = result[0];

return API.v1.success({
users,
count: users.length,
Expand Down Expand Up @@ -729,7 +735,10 @@ API.v1.addRoute(

API.v1.addRoute(
'users.resetAvatar',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersResetAvatarProps,
},
{
async post() {
const user = await getUserFromParams(this.bodyParams);
Expand Down Expand Up @@ -900,7 +909,10 @@ API.v1.addRoute(

API.v1.addRoute(
'users.forgotPassword',
{ authRequired: false },
{
authRequired: false,
validateParams: isUsersForgotPasswordProps,
},
{
async post() {
const isPasswordResetEnabled = settings.get('Accounts_PasswordReset');
Expand All @@ -910,9 +922,6 @@ API.v1.addRoute(
}

const { email } = this.bodyParams;
if (!email) {
return API.v1.failure("The 'email' param is required");
}

await sendForgotPasswordEmail(email.toLowerCase());
return API.v1.success();
Expand Down Expand Up @@ -1558,5 +1567,5 @@ type UsersEndpoints = ExtractRoutesFromAPI<typeof usersEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends UsersEndpoints {}
interface Endpoints extends UsersEndpoints { }
}
2 changes: 1 addition & 1 deletion packages/rest-typings/src/v1/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ addFormats(ajv);
ajv.addFormat('basic_email', /^[^@]+@[^@]+$/);
ajv.addFormat(
'rfc_email',
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
);
ajv.addKeyword({
keyword: 'isNotEmpty',
Expand Down
36 changes: 34 additions & 2 deletions packages/rest-typings/src/v1/groups/GroupsListProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import type { PaginatedRequest } from '../../helpers/PaginatedRequest';
import { ajv } from '../Ajv';

export type GroupsListProps = PaginatedRequest<null>;
const groupsListPropsSchema = {};
export type GroupsListProps = PaginatedRequest<{ name?: string }>;

const groupsListPropsSchema = {
type: 'object',
properties: {
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
fields: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
name: {
type: 'string',
nullable: true,
},
},
required: [],
additionalProperties: false,
};

export const isGroupsListProps = ajv.compile<GroupsListProps>(groupsListPropsSchema);
50 changes: 49 additions & 1 deletion packages/rest-typings/src/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type { UserLogoutParamsPOST } from './users/UserLogoutParamsPOST';
import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST';
import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusParamsPOST';
import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET';
import type { UsersDeleteOwnAccountParamsPOST } from './users/UsersDeleteOwnAccountParamsPOST';
import type { UsersForgotPasswordParamsPOST } from './users/UsersForgotPasswordParamsPOST';
import type { UsersGetAvatarParamsGET } from './users/UsersGetAvatarParamsGET';
import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet';
import type { UsersListStatusParamsGET } from './users/UsersListStatusParamsGET';
import type { UsersListTeamsParamsGET } from './users/UsersListTeamsParamsGET';
Expand All @@ -18,6 +21,48 @@ import type { UsersSetPreferencesParamsPOST } from './users/UsersSetPreferencePa
import type { UsersUpdateOwnBasicInfoParamsPOST } from './users/UsersUpdateOwnBasicInfoParamsPOST';
import type { UsersUpdateParamsPOST } from './users/UsersUpdateParamsPOST';

export const isUsersGetAvatarProps = ajv.compile<UsersGetAvatarParamsGET>({
oneOf: [
{
type: 'object',
properties: { userId: { type: 'string' } },
required: ['userId'],
additionalProperties: false,
},
{
type: 'object',
properties: { username: { type: 'string' } },
required: ['username'],
additionalProperties: false,
},
{
type: 'object',
properties: { user: { type: 'string' } },
required: ['user'],
additionalProperties: false,
},
],
});

export const isUsersDeleteOwnAccountProps = ajv.compile<UsersDeleteOwnAccountParamsPOST>({
type: 'object',
properties: {
password: { type: 'string', minLength: 1 },
confirmRelinquish: { type: 'boolean' },
},
required: ['password'],
additionalProperties: false,
});

export const isUsersForgotPasswordProps = ajv.compile<UsersForgotPasswordParamsPOST>({
type: 'object',
properties: {
email: { type: 'string', minLength: 1 },
},
required: ['email'],
additionalProperties: false,
});

type UsersInfo = { userId?: IUser['_id']; username?: IUser['username'] };

const UsersInfoSchema = {
Expand Down Expand Up @@ -356,7 +401,7 @@ export type UsersEndpoints = {
};

'/v1/users.getAvatar': {
GET: (params: { userId?: string; username?: string; user?: string }) => void;
GET: (params: UsersGetAvatarParamsGET) => void;
};

'/v1/users.updateOwnBasicInfo': {
Expand All @@ -380,3 +425,6 @@ export * from './users/UserRegisterParamsPOST';
export * from './users/UserLogoutParamsPOST';
export * from './users/UsersListTeamsParamsGET';
export * from './users/UsersAutocompleteParamsGET';
export * from './users/UsersGetAvatarParamsGET';
export * from './users/UsersDeleteOwnAccountParamsPOST';
export * from './users/UsersForgotPasswordParamsPOST';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UsersDeleteOwnAccountParamsPOST = {
password: string;
confirmRelinquish?: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UsersForgotPasswordParamsPOST = {
email: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UsersGetAvatarParamsGET =
| { userId: string; username?: never; user?: never }
| { username: string; userId?: never; user?: never }
| { user: string; userId?: never; username?: never };