fix(core): All calls to plainToInstance should exclude extraneous values (no-changelog) (#9338)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-05-08 15:49:41 +02:00 committed by GitHub
parent 9003c15811
commit 5025d209ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 28 additions and 16 deletions

View file

@ -41,7 +41,7 @@ export class MeController {
@Patch('/') @Patch('/')
async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> { async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
const { id: userId, email: currentEmail } = req.user; const { id: userId, email: currentEmail } = req.user;
const payload = plainToInstance(UserUpdatePayload, req.body); const payload = plainToInstance(UserUpdatePayload, req.body, { excludeExtraneousValues: true });
const { email } = payload; const { email } = payload;
if (!email) { if (!email) {
@ -227,7 +227,9 @@ export class MeController {
*/ */
@Patch('/settings') @Patch('/settings')
async updateCurrentUserSettings(req: MeRequest.UserSettingsUpdate): Promise<User['settings']> { async updateCurrentUserSettings(req: MeRequest.UserSettingsUpdate): Promise<User['settings']> {
const payload = plainToInstance(UserSettingsUpdatePayload, req.body); const payload = plainToInstance(UserSettingsUpdatePayload, req.body, {
excludeExtraneousValues: true,
});
const { id } = req.user; const { id } = req.user;
await this.userService.updateSettings(id, payload); await this.userService.updateSettings(id, payload);

View file

@ -117,7 +117,9 @@ export class UsersController {
@Patch('/:id/settings') @Patch('/:id/settings')
@GlobalScope('user:update') @GlobalScope('user:update')
async updateUserSettings(req: UserRequest.UserSettingsUpdate) { async updateUserSettings(req: UserRequest.UserSettingsUpdate) {
const payload = plainToInstance(UserSettingsUpdatePayload, req.body); const payload = plainToInstance(UserSettingsUpdatePayload, req.body, {
excludeExtraneousValues: true,
});
const id = req.params.id; const id = req.params.id;
@ -293,7 +295,9 @@ export class UsersController {
const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } = const { NO_ADMIN_ON_OWNER, NO_USER, NO_OWNER_ON_OWNER } =
UsersController.ERROR_MESSAGES.CHANGE_ROLE; UsersController.ERROR_MESSAGES.CHANGE_ROLE;
const payload = plainToInstance(UserRoleChangePayload, req.body); const payload = plainToInstance(UserRoleChangePayload, req.body, {
excludeExtraneousValues: true,
});
await validateEntity(payload); await validateEntity(payload);
const targetUser = await this.userRepository.findOne({ const targetUser = await this.userRepository.findOne({

View file

@ -115,7 +115,7 @@ export class User extends WithTimestamps implements IUser {
@AfterLoad() @AfterLoad()
@AfterUpdate() @AfterUpdate()
computeIsPending(): void { computeIsPending(): void {
this.isPending = this.password === null; this.isPending = this.password === null && this.role !== 'global:owner';
} }
/** /**

View file

@ -10,6 +10,7 @@ import type {
IUser, IUser,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Expose } from 'class-transformer';
import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, Length } from 'class-validator'; import { IsBoolean, IsEmail, IsIn, IsOptional, IsString, Length } from 'class-validator';
import { NoXss } from '@db/utils/customValidators'; import { NoXss } from '@db/utils/customValidators';
import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/Interfaces'; import type { PublicUser, SecretsProvider, SecretsProviderState } from '@/Interfaces';
@ -20,14 +21,17 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { WorkflowHistory } from '@db/entities/WorkflowHistory'; import type { WorkflowHistory } from '@db/entities/WorkflowHistory';
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> { export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
@Expose()
@IsEmail() @IsEmail()
email: string; email: string;
@Expose()
@NoXss() @NoXss()
@IsString({ message: 'First name must be of type string.' }) @IsString({ message: 'First name must be of type string.' })
@Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' }) @Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' })
firstName: string; firstName: string;
@Expose()
@NoXss() @NoXss()
@IsString({ message: 'Last name must be of type string.' }) @IsString({ message: 'Last name must be of type string.' })
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' }) @Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
@ -35,16 +39,19 @@ export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'la
} }
export class UserSettingsUpdatePayload { export class UserSettingsUpdatePayload {
@Expose()
@IsBoolean({ message: 'userActivated should be a boolean' }) @IsBoolean({ message: 'userActivated should be a boolean' })
@IsOptional() @IsOptional()
userActivated: boolean; userActivated: boolean;
@Expose()
@IsBoolean({ message: 'allowSSOManualLogin should be a boolean' }) @IsBoolean({ message: 'allowSSOManualLogin should be a boolean' })
@IsOptional() @IsOptional()
allowSSOManualLogin?: boolean; allowSSOManualLogin?: boolean;
} }
export class UserRoleChangePayload { export class UserRoleChangePayload {
@Expose()
@IsIn(['global:admin', 'global:member']) @IsIn(['global:admin', 'global:member'])
newRoleName: AssignableRole; newRoleName: AssignableRole;
} }

View file

@ -356,13 +356,11 @@ const VALID_PATCH_ME_PAYLOADS = [
email: randomEmail(), email: randomEmail(),
firstName: randomName(), firstName: randomName(),
lastName: randomName(), lastName: randomName(),
password: randomValidPassword(),
}, },
{ {
email: randomEmail().toUpperCase(), email: randomEmail().toUpperCase(),
firstName: randomName(), firstName: randomName(),
lastName: randomName(), lastName: randomName(),
password: randomValidPassword(),
}, },
]; ];

View file

@ -87,27 +87,28 @@ describe('MeController', () => {
id: '123', id: '123',
password: 'password', password: 'password',
authIdentities: [], authIdentities: [],
role: 'global:owner', role: 'global:member',
}); });
const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' }; const reqBody = { email: 'valid@email.com', firstName: 'John', lastName: 'Potato' };
const req = mock<MeRequest.UserUpdate>({ user, body: reqBody, browserId }); const req = mock<MeRequest.UserUpdate>({ user, browserId });
req.body = reqBody;
const res = mock<Response>(); const res = mock<Response>();
userRepository.findOneOrFail.mockResolvedValue(user); userRepository.findOneOrFail.mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
// Add invalid data to the request payload // Add invalid data to the request payload
Object.assign(reqBody, { id: '0', role: '42' }); Object.assign(reqBody, { id: '0', role: 'global:owner' });
await controller.updateCurrentUser(req, res); await controller.updateCurrentUser(req, res);
expect(userService.update).toHaveBeenCalled(); expect(userService.update).toHaveBeenCalled();
const updatedUser = userService.update.mock.calls[0][1]; const updatePayload = userService.update.mock.calls[0][1];
expect(updatedUser.email).toBe(reqBody.email); expect(updatePayload.email).toBe(reqBody.email);
expect(updatedUser.firstName).toBe(reqBody.firstName); expect(updatePayload.firstName).toBe(reqBody.firstName);
expect(updatedUser.lastName).toBe(reqBody.lastName); expect(updatePayload.lastName).toBe(reqBody.lastName);
expect(updatedUser.id).not.toBe('0'); expect(updatePayload.id).toBeUndefined();
expect(updatedUser.role).not.toBe('42'); expect(updatePayload.role).toBeUndefined();
}); });
it('should throw BadRequestError if beforeUpdate hook throws BadRequestError', async () => { it('should throw BadRequestError if beforeUpdate hook throws BadRequestError', async () => {