change validation to be stricter

This commit is contained in:
rubikscraft
2022-03-19 21:34:33 +01:00
parent cc7d9ddef3
commit 94c2a16bc9
24 changed files with 247 additions and 167 deletions

View File

@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import {
ImmuteableRolesList,
@@ -14,6 +13,7 @@ import {
HasFailed,
HasSuccess
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { In, Repository } from 'typeorm';
import { ERoleBackend } from '../../models/entities/role.entity';
@@ -172,7 +172,7 @@ export class RolesService {
return await this.findOne(user);
} else {
user = plainToClass(ERoleBackend, user);
const errors = await validate(user, { forbidUnknownValues: true });
const errors = await strictValidate(user);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid role');

View File

@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import {
InternalSysprefRepresentation,
SysPreferences,
@@ -14,6 +13,7 @@ import {
Failable,
HasFailed
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { Repository } from 'typeorm';
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
@@ -81,9 +81,7 @@ export class SysPreferenceService {
ESysPreferenceBackend,
foundSysPreference,
);
const errors = await validate(foundSysPreference, {
forbidUnknownValues: true,
});
const errors = await strictValidate(foundSysPreference);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid preference');
@@ -183,9 +181,7 @@ export class SysPreferenceService {
verifySysPreference.value = validatedValue;
// Just to be sure
const errors = await validate(verifySysPreference, {
forbidUnknownValues: true,
});
const errors = await strictValidate(verifySysPreference);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid preference');

View File

@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
import {
@@ -11,6 +10,7 @@ import {
HasFailed,
HasSuccess
} from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { Repository } from 'typeorm';
import { EUserBackend } from '../../models/entities/user.entity';
import { GetCols } from '../collectionutils';
@@ -170,7 +170,7 @@ export class UsersService {
return await this.findOne(user);
} else {
user = plainToClass(EUserBackend, user);
const errors = await validate(user, { forbidUnknownValues: true });
const errors = await strictValidate(user);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid user');

View File

@@ -5,10 +5,10 @@ import {
PipeTransform,
Scope
} from '@nestjs/common';
import { validate } from 'class-validator';
import { FastifyRequest } from 'fastify';
import { MultipartFields, MultipartFile } from 'fastify-multipart';
import { Newable } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { MultipartConfigService } from '../config/multipart.config.service';
import {
MultiPartFieldDto,
@@ -61,7 +61,7 @@ export class MultiPartPipe implements PipeTransform {
}
}
const errors = await validate(dtoClass, { forbidUnknownValues: true });
const errors = await strictValidate(dtoClass);
if (errors.length > 0) {
this.logger.warn(errors);
throw new BadRequestException('Invalid file');

View File

@@ -5,6 +5,7 @@ import {
NestFastifyApplication
} from '@nestjs/platform-fastify';
import * as multipart from 'fastify-multipart';
import { ValidateOptions } from 'picsur-shared/dist/util/validate';
import { AppModule } from './app.module';
import { UsersService } from './collections/userdb/userdb.service';
import { HostConfigService } from './config/host.config.service';
@@ -28,12 +29,7 @@ async function bootstrap() {
);
app.useGlobalFilters(new MainExceptionFilter());
app.useGlobalInterceptors(new SuccessInterceptor());
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
forbidUnknownValues: true,
}),
);
app.useGlobalPipes(new ValidationPipe(ValidateOptions));
app.useGlobalGuards(
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
);

View File

@@ -1,8 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { instanceToPlain, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable()
@@ -16,7 +16,7 @@ export class AuthManagerService {
user,
});
const errors = await validate(jwtData, { forbidUnknownValues: true });
const errors = await strictValidate(jwtData);
if (errors.length > 0) {
this.logger.warn(errors);
throw new Error('Invalid jwt token generated');

View File

@@ -5,7 +5,7 @@ import {
Logger
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable()
@@ -20,7 +20,7 @@ export class AdminGuard implements CanActivate {
}
const user = plainToClass(EUserBackend, request.user);
const errors = await validate(user, { forbidUnknownValues: true });
const errors = await strictValidate(user);
if (errors.length > 0) {
this.logger.warn(errors);
return false;

View File

@@ -6,9 +6,9 @@ import {
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable()
@@ -26,9 +26,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
async validate(payload: any): Promise<EUserBackend> {
const jwt = plainToClass(JwtDataDto, payload);
const errors = await validate(jwt, {
forbidUnknownValues: true,
});
const errors = await strictValidate(jwt);
if (errors.length > 0) {
this.logger.warn(errors);

View File

@@ -8,12 +8,12 @@ import {
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import {
Permissions
} from 'picsur-shared/dist/dto/permissions';
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
import { isPermissionsArray } from 'picsur-shared/dist/util/permissions';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { EUserBackend } from '../../../models/entities/user.entity';
@@ -77,9 +77,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
private async validateUser(user: EUserBackend): Promise<EUserBackend> {
const userClass = plainToClass(EUserBackend, user);
const errors = await validate(userClass, {
forbidUnknownValues: true,
});
const errors = await strictValidate(userClass);
if (errors.length > 0) {
this.logger.error(

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { UserApiModule } from './auth/user.module';
import { ExperimentModule } from './experiment/experiment.module';
import { InfoModule } from './info/info.module';
import { PrefModule } from './pref/pref.module';
import { RolesApiModule } from './roles/roles.module';
import { UserApiModule } from './user/user.module';
@Module({
imports: [

View File

@@ -8,18 +8,11 @@ import {
Request
} from '@nestjs/common';
import {
UserDeleteRequest,
UserDeleteResponse,
UserInfoRequest,
UserInfoResponse,
UserListResponse,
UserLoginResponse,
UserMePermissionsResponse,
UserMeResponse,
UserRegisterRequest,
UserRegisterResponse,
UserUpdateRolesRequest,
UserUpdateRolesResponse
UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
@@ -34,7 +27,7 @@ import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/user')
export class UserController {
private readonly logger = new Logger('AuthController');
private readonly logger = new Logger('UserController');
constructor(
private usersService: UsersService,
@@ -66,65 +59,6 @@ export class UserController {
return user;
}
@Post('delete')
@RequiredPermissions(Permission.UserManage)
async delete(
@Body() deleteData: UserDeleteRequest,
): Promise<UserDeleteResponse> {
const user = await this.usersService.delete(deleteData.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not delete user');
}
return user;
}
@Post('roles')
@RequiredPermissions(Permission.UserManage)
async setPermissions(
@Body() body: UserUpdateRolesRequest,
): Promise<UserUpdateRolesResponse> {
const updatedUser = await this.usersService.setRoles(
body.username,
body.roles,
);
if (HasFailed(updatedUser)) {
this.logger.warn(updatedUser.getReason());
throw new InternalServerErrorException('Could not update user');
}
return updatedUser;
}
@Post('info')
@RequiredPermissions(Permission.UserManage)
async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> {
const user = await this.usersService.findOne(body.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not find user');
}
return user;
}
@Get('list')
@RequiredPermissions(Permission.UserManage)
async listUsers(): Promise<UserListResponse> {
const users = await this.usersService.findAll();
if (HasFailed(users)) {
this.logger.warn(users.getReason());
throw new InternalServerErrorException('Could not list users');
}
return {
users,
total: users.length,
};
}
@Get('me')
@RequiredPermissions(Permission.UserMe)
async me(@Request() req: AuthFasityRequest): Promise<UserMeResponse> {

View File

@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common';
import { AuthManagerModule } from '../../../managers/auth/auth.module';
import { UserController } from './user.controller';
import { UserManageController } from './usermanage.controller';
@Module({
imports: [AuthManagerModule],
controllers: [UserController],
controllers: [UserController, UserManageController],
})
export class UserApiModule {}

View File

@@ -0,0 +1,85 @@
import {
Body,
Controller,
Get,
InternalServerErrorException,
Logger,
Post
} from '@nestjs/common';
import {
UserDeleteRequest,
UserDeleteResponse,
UserInfoRequest,
UserInfoResponse,
UserListResponse,
UserUpdateRolesRequest,
UserUpdateRolesResponse
} from 'picsur-shared/dist/dto/api/usermanage.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
@Controller('api/user')
@RequiredPermissions(Permission.UserManage)
export class UserManageController {
private readonly logger = new Logger('UserManageController');
constructor(private usersService: UsersService) {}
@Get('list')
async listUsers(): Promise<UserListResponse> {
const users = await this.usersService.findAll();
if (HasFailed(users)) {
this.logger.warn(users.getReason());
throw new InternalServerErrorException('Could not list users');
}
return {
users,
total: users.length,
};
}
@Post('delete')
async delete(
@Body() deleteData: UserDeleteRequest,
): Promise<UserDeleteResponse> {
const user = await this.usersService.delete(deleteData.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not delete user');
}
return user;
}
@Post('roles')
async setPermissions(
@Body() body: UserUpdateRolesRequest,
): Promise<UserUpdateRolesResponse> {
const updatedUser = await this.usersService.setRoles(
body.username,
body.roles,
);
if (HasFailed(updatedUser)) {
this.logger.warn(updatedUser.getReason());
throw new InternalServerErrorException('Could not update user');
}
return updatedUser;
}
@Post('info')
async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> {
console.log(body);
const user = await this.usersService.findOne(body.username);
if (HasFailed(user)) {
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not find user');
}
return user;
}
}

View File

@@ -0,0 +1,2 @@
<h1>Users</h1>

View File

@@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';
@Component({
templateUrl: './settings-users.component.html',
})
export class SettingsUsersComponent implements OnInit {
constructor() {}
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SettingsUsersComponent } from './settings-users.component';
import { SettingsUsersRoutingModule } from './settings-users.routing.module';
@NgModule({
declarations: [SettingsUsersComponent],
imports: [
CommonModule,
SettingsUsersRoutingModule,
],
})
export class SettingsUsersRouteModule {}

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { PRoutes } from 'src/app/models/picsur-routes';
import { SettingsUsersComponent } from './settings-users.component';
const routes: PRoutes = [
{
path: '',
component: SettingsUsersComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SettingsUsersRoutingModule {}

View File

@@ -7,6 +7,7 @@ import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sideba
import { SettingsGeneralRouteModule } from './settings-general/settings-general.module';
import { SettingsSidebarComponent } from './settings-sidebar/settings-sidebar.component';
import { SettingsSysprefRouteModule } from './settings-syspref/settings-syspref.module';
import { SettingsUsersRouteModule } from './settings-users/settings-users.module';
const SettingsRoutes: PRoutes = [
{
@@ -40,6 +41,18 @@ const SettingsRoutes: PRoutes = [
},
},
},
{
path: 'users',
loadChildren: () => SettingsUsersRouteModule,
data: {
permissions: [Permission.UserManage],
page: {
title: 'Users',
icon: 'people',
category: 'system',
},
},
},
],
canActivate: [PermissionGuard],
canActivateChild: [PermissionGuard],

View File

@@ -1,11 +1,8 @@
import { Injectable } from '@angular/core';
import { ClassConstructor, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import {
ApiResponse,
ApiSuccessResponse
} from 'picsur-shared/dist/dto/api';
import { ApiResponse, ApiSuccessResponse } from 'picsur-shared/dist/dto/api';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { MultiPartRequest } from '../../models/multi-part-request';
import { KeyService } from './key.service';
@@ -31,7 +28,7 @@ export class ApiService {
data: object
): AsyncFailable<W> {
const sendClass = plainToClass(sendType, data);
const errors = await validate(sendClass);
const errors = await strictValidate(sendClass);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Something went wrong');
@@ -68,14 +65,15 @@ export class ApiService {
ApiSuccessResponse<T>,
ApiSuccessResponse<T>
>(ApiSuccessResponse, result);
const resultErrors = await validate(resultClass);
const resultErrors = await strictValidate(resultClass);
if (resultErrors.length > 0) {
this.logger.warn('result', resultErrors);
return Fail('Something went wrong');
}
const dataClass = plainToClass(type, result.data);
const dataErrors = await validate(dataClass);
const dataErrors = await strictValidate(dataClass);
if (dataErrors.length > 0) {
this.logger.warn('data', dataErrors);
return Fail('Something went wrong');

View File

@@ -1,14 +1,17 @@
import { Injectable } from '@angular/core';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import jwt_decode from 'jwt-decode';
import {
UserLoginRequest,
UserLoginResponse, UserMeResponse, UserRegisterRequest, UserRegisterResponse
UserLoginResponse,
UserMeResponse,
UserRegisterRequest,
UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto';
import { JwtDataDto } from 'picsur-shared/dist/dto/jwt.dto';
import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate';
import { BehaviorSubject } from 'rxjs';
import { ApiService } from './api.service';
import { KeyService } from './key.service';
@@ -60,7 +63,10 @@ export class UserService {
return user;
}
public async register(username: string, password: string): AsyncFailable<EUser> {
public async register(
username: string,
password: string
): AsyncFailable<EUser> {
const request: UserRegisterRequest = {
username,
password,
@@ -119,7 +125,7 @@ export class UserService {
}
const jwtData = plainToClass(JwtDataDto, decoded);
const errors = await validate(jwtData);
const errors = await strictValidate(jwtData);
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid token data');

View File

@@ -5,8 +5,7 @@ import {
IsNotEmpty,
IsString,
Max,
Min,
ValidateNested,
Min
} from 'class-validator';
class BaseApiResponse<T extends Object, W extends boolean> {
@@ -24,7 +23,7 @@ class BaseApiResponse<T extends Object, W extends boolean> {
@IsNotEmpty()
timestamp: string;
@ValidateNested()
//@ValidateNested()
@IsDefined()
data: T;
}

View File

@@ -1,15 +1,11 @@
import { Type } from 'class-transformer';
import {
IsArray, IsDefined,
IsEnum,
IsInt,
IsNotEmpty, IsPositive,
IsString,
IsEnum, IsNotEmpty, IsString,
ValidateNested
} from 'class-validator';
import { EUser } from '../../entities/user.entity';
import { Permissions, PermissionsList } from '../permissions';
import { Roles } from '../roles.dto';
// Api
@@ -43,38 +39,6 @@ export class UserRegisterRequest {
export class UserRegisterResponse extends EUser {}
// UserDelete
export class UserDeleteRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserDeleteResponse extends EUser {}
// UserInfo
export class UserInfoRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserInfoResponse extends EUser {}
// UserList
export class UserListResponse {
@IsArray()
@IsDefined()
@ValidateNested()
@Type(() => EUser)
users: EUser[];
@IsInt()
@IsPositive()
@IsDefined()
total: number;
}
// UserMe
export class UserMeResponse {
@IsDefined()
@@ -94,17 +58,3 @@ export class UserMePermissionsResponse {
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
}
// UserUpdateRoles
export class UserUpdateRolesRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsArray()
@IsDefined()
@IsString({ each: true })
roles: Roles;
}
export class UserUpdateRolesResponse extends EUser {}

View File

@@ -0,0 +1,50 @@
import { Type } from 'class-transformer';
import { IsArray, IsDefined, IsInt, IsNotEmpty, IsPositive, IsString, ValidateNested } from 'class-validator';
import { EUser } from '../../entities/user.entity';
import { Roles } from '../roles.dto';
// UserDelete
export class UserDeleteRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserDeleteResponse extends EUser {}
// UserInfo
export class UserInfoRequest {
@IsString()
@IsNotEmpty()
username: string;
}
export class UserInfoResponse extends EUser {}
// UserList
export class UserListResponse {
@IsArray()
@IsDefined()
@ValidateNested()
@Type(() => EUser)
users: EUser[];
@IsInt()
@IsPositive()
@IsDefined()
total: number;
}
// UserUpdateRoles
export class UserUpdateRolesRequest {
@IsString()
@IsNotEmpty()
username: string;
@IsArray()
@IsDefined()
@IsString({ each: true })
roles: Roles;
}
export class UserUpdateRolesResponse extends EUser {}

View File

@@ -0,0 +1,12 @@
import { validate } from 'class-validator';
export const ValidateOptions = {
disableErrorMessages: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
stopAtFirstError: true,
whitelist: true,
};
export const strictValidate = (object: object) =>
validate(object, ValidateOptions);