Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Audit users.update API endpoint #34494

Merged
merged 42 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
de90396
WIP
gabriellsh Dec 19, 2024
46d8f44
WIP 2
gabriellsh Dec 20, 2024
5a9cb10
Finish logging changed users
gabriellsh Dec 24, 2024
ee499ff
catch and reject
gabriellsh Dec 26, 2024
858dec6
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into logs…
gabriellsh Jan 20, 2025
aa46038
v2
gabriellsh Jan 20, 2025
300bfb0
WIP deep diff & obfuscation
gabriellsh Jan 21, 2025
88b3080
Merge remote-tracking branch 'origin' into logs/users
Jan 27, 2025
4a51de9
Fix diff comparison
gabriellsh Jan 27, 2025
a9b4da0
fix Date comparison
gabriellsh Jan 27, 2025
de1ff5c
Add tests
gabriellsh Jan 27, 2025
df56383
Merge remote-tracking branch 'origin/develop' into logs/users
gabriellsh Feb 5, 2025
fc6d6ee
Merge remote-tracking branch 'origin/develop' into logs/users
gabriellsh Feb 7, 2025
7aa9fce
Revert users.update
gabriellsh Feb 7, 2025
70b6ab3
externalize updater
gabriellsh Feb 7, 2025
fca5afe
Update IAuditUserChangedEvent type
gabriellsh Feb 7, 2025
ad50698
Update audit store and tests
gabriellsh Feb 7, 2025
a573b2c
allow getting updateFilter when updater is dirty
gabriellsh Feb 7, 2025
4722339
Grab just changed service keys
gabriellsh Feb 7, 2025
a6ce086
Audit
gabriellsh Feb 7, 2025
55b9a88
Merge remote-tracking branch 'origin/develop' into logs/users
gabriellsh Mar 17, 2025
7c0a0cd
update updater type
gabriellsh Mar 17, 2025
c6410c0
Fix disable audit flag
gabriellsh Mar 17, 2025
c23793c
fix nested updater keys
gabriellsh Mar 17, 2025
d9b1e2f
fix field obfuscation
gabriellsh Mar 18, 2025
41389f2
add cs
gabriellsh Mar 18, 2025
f8404e2
fix actor info
gabriellsh Mar 19, 2025
7bb42e1
Merge remote-tracking branch 'origin/logs/users' into logs/users
gabriellsh Mar 19, 2025
13394eb
fix cs
gabriellsh Mar 19, 2025
02b0a26
rename _getUpdateFilter
gabriellsh Mar 19, 2025
9e9e4a1
Fix oldUserData check
gabriellsh Mar 19, 2025
7a81681
fix userData
gabriellsh Mar 19, 2025
11b2b4a
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
ff4f4cf
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
1af4646
Merge branch 'develop' into logs/users
tassoevan Mar 20, 2025
002bb2a
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
3e11c3e
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
9c71893
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
faf4ec2
Merge branch 'develop' into logs/users
kodiakhq[bot] Mar 20, 2025
08d8de7
Merge branch 'develop' into logs/users
gabriellsh Mar 20, 2025
43c55ed
Merge branch 'develop' into logs/users
gabriellsh Mar 20, 2025
1e260e4
Merge branch 'develop' into logs/users
gabriellsh Mar 25, 2025
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
8 changes: 8 additions & 0 deletions .changeset/slow-ravens-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/models": minor
---

Implements auditing events for `/v1/users.update` API endpoint
11 changes: 9 additions & 2 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { Filter } from 'mongodb';
import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken';
import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken';
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 { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail';
Expand Down Expand Up @@ -114,18 +115,24 @@ API.v1.addRoute(
if (userData.name && !validateNameChars(userData.name)) {
return API.v1.failure('Name contains invalid characters');
}
const auditStore = new UserChangedAuditStore({
_id: this.user._id,
ip: this.requestIp,
useragent: this.request.headers['user-agent'] || '',
username: this.user.username || '',
});

await saveUser(this.userId, userData);
await saveUser(this.userId, userData, { auditStore });

if (typeof this.bodyParams.data.active !== 'undefined') {
const {
userId,
data: { active },
confirmRelinquish,
} = this.bodyParams;

await executeSetUserActiveStatus(this.userId, userId, active, Boolean(confirmRelinquish));
}

const { fields } = await this.parseJsonQuery();

const user = await Users.findOneById(this.bodyParams.userId, { projection: fields });
Expand Down
35 changes: 32 additions & 3 deletions apps/meteor/app/lib/server/functions/saveUser/saveUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { MeteorError } from '@rocket.chat/core-services';
import { isUserFederated } from '@rocket.chat/core-typings';
import type { IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
Expand All @@ -7,6 +8,7 @@ import type { ClientSession } from 'mongodb';

import { callbacks } from '../../../../../lib/callbacks';
import { wrapInSessionTransaction, onceTransactionCommitedSuccessfully } from '../../../../../server/database/utils';
import type { UserChangedAuditStore } from '../../../../../server/lib/auditServerEvents/userChanged';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser';
import { generatePassword } from '../../lib/generatePassword';
Expand Down Expand Up @@ -50,12 +52,17 @@ export type SaveUserData = {
sendWelcomeEmail?: boolean;

customFields?: Record<string, any>;
active?: boolean;
};
export type UpdateUserData = RequiredField<SaveUserData, '_id'>;
export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id;

type SaveUserOptions = {
auditStore?: UserChangedAuditStore;
};

const _saveUser = (session?: ClientSession) =>
async function (userId: IUser['_id'], userData: SaveUserData) {
async function (userId: IUser['_id'], userData: SaveUserData, options?: SaveUserOptions) {
const oldUserData = userData._id && (await Users.findOneById(userData._id));
if (oldUserData && isUserFederated(oldUserData)) {
throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user');
Expand All @@ -81,10 +88,18 @@ const _saveUser = (session?: ClientSession) =>
}

if (!isUpdateUserData(userData)) {
// pass session?
// TODO audit new users
return saveNewUser(userData, sendPassword);
}

if (!oldUserData) {
throw new MeteorError('error-user-not-found', 'User not found', {
method: 'saveUser',
});
}

options?.auditStore?.setOriginalUser(oldUserData);

await validateUserEditing(userId, userData);

// update user
Expand Down Expand Up @@ -164,6 +179,16 @@ const _saveUser = (session?: ClientSession) =>
await Users.updateFromUpdater({ _id: userData._id }, updater, { session });

await onceTransactionCommitedSuccessfully(async () => {
if (session && options?.auditStore) {
// setting this inside here to avoid moving `executeSetUserActiveStatus` from the endpoint fn
// updater will be commited by this point, so it won't affect the external user activation/deactivation
if (userData.active !== undefined) {
updater.set('active', userData.active);
}
options.auditStore.setUpdateFilter(updater.getRawUpdateFilter());
void options.auditStore.commitAuditEvent();
}

// App IPostUserUpdated event hook
// We need to pass the session here to ensure this record is fetched
// with the uncommited transaction data.
Expand Down Expand Up @@ -209,5 +234,9 @@ export const saveUser = (() => {
if (!process.env.DEBUG_DISABLE_USER_AUDIT) {
return wrapInSessionTransaction(_saveUser);
}
return _saveUser();

const saveUserNoSession = _saveUser();
return function saveUser(userId: IUser['_id'], userData: SaveUserData, _options?: any) {
return saveUserNoSession(userId, userData);
};
})();
1 change: 1 addition & 0 deletions apps/meteor/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {
'<rootDir>/ee/server/patches/**/*.spec.ts',
'<rootDir>/app/cloud/server/functions/supportedVersionsToken/**.spec.ts',
'<rootDir>/app/utils/lib/**.spec.ts',
'<rootDir>/server/lib/auditServerEvents/**.spec.ts',
'<rootDir>/app/api/server/**.spec.ts',
'<rootDir>/app/api/server/middlewares/**.spec.ts',
],
Expand Down
Loading
Loading