apply role guard to all routes

This commit is contained in:
rubikscraft
2022-03-12 00:14:16 +01:00
parent 9b98f3c005
commit 0aa897fa8d
15 changed files with 168 additions and 37 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules node_modules
todo.txt

View File

@@ -61,7 +61,9 @@ export class SysPreferenceService {
ESysPreferenceBackend, ESysPreferenceBackend,
foundSysPreference, foundSysPreference,
); );
const errors = await validate(foundSysPreference); const errors = await validate(foundSysPreference, {
forbidUnknownValues: true,
});
if (errors.length > 0) { if (errors.length > 0) {
this.logger.warn(errors); this.logger.warn(errors);
return Fail('Invalid preference'); return Fail('Invalid preference');
@@ -85,7 +87,9 @@ export class SysPreferenceService {
verifySysPreference.key = key as SysPreferences; verifySysPreference.key = key as SysPreferences;
verifySysPreference.value = value; verifySysPreference.value = value;
const errors = await validate(verifySysPreference); const errors = await validate(verifySysPreference, {
forbidUnknownValues: true,
});
if (errors.length > 0) { if (errors.length > 0) {
this.logger.warn(errors); this.logger.warn(errors);
return Fail('Invalid preference'); return Fail('Invalid preference');

View File

@@ -1,10 +0,0 @@
import { CanActivate, UseGuards } from '@nestjs/common';
import { AdminGuard } from '../managers/auth/guards/admin.guard';
import { MainAuthGuard } from '../managers/auth/guards/main.guard';
export const Authenticated = (adminOnly: boolean = false) => {
const guards: (Function | CanActivate)[] = [MainAuthGuard];
if (adminOnly) guards.push(AdminGuard);
return UseGuards(...guards);
};

View File

@@ -0,0 +1,21 @@
import { SetMetadata, UseGuards } from '@nestjs/common';
import { Roles as RolesList } from 'picsur-shared/dist/dto/roles.dto';
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
export const GuestRoles = (...roles: RolesList) => {
return SetMetadata('roles', roles);
};
export const UserRoles = (...roles: RolesList) => {
const fullRoles = [...new Set(['user', ...roles])];
return SetMetadata('roles', fullRoles);
};
// Easy to read roles
export const Guest = () => GuestRoles();
export const User = () => UserRoles();
export const Admin = () => UserRoles('admin');
export const UseLocalAuth = () =>
CombineDecorators(Guest(), UseGuards(LocalAuthGuard));

View File

@@ -1,5 +1,5 @@
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory, Reflector } from '@nestjs/core';
import { import {
FastifyAdapter, FastifyAdapter,
NestFastifyApplication NestFastifyApplication
@@ -10,6 +10,7 @@ import { HostConfigService } from './config/host.config.service';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter'; import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor'; import { SuccessInterceptor } from './layers/success/success.interceptor';
import { PicsurLoggerService } from './logger/logger.service'; import { PicsurLoggerService } from './logger/logger.service';
import { MainAuthGuard } from './managers/auth/guards/main.guard';
async function bootstrap() { async function bootstrap() {
const fastifyAdapter = new FastifyAdapter(); const fastifyAdapter = new FastifyAdapter();
@@ -32,6 +33,7 @@ async function bootstrap() {
forbidUnknownValues: true, forbidUnknownValues: true,
}), }),
); );
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
app.useLogger(app.get(PicsurLoggerService)); app.useLogger(app.get(PicsurLoggerService));

View File

@@ -14,6 +14,7 @@ import { AuthManagerService } from './auth.service';
import { GuestStrategy } from './guards/guest.strategy'; import { GuestStrategy } from './guards/guest.strategy';
import { JwtStrategy } from './guards/jwt.strategy'; import { JwtStrategy } from './guards/jwt.strategy';
import { LocalAuthStrategy } from './guards/localauth.strategy'; import { LocalAuthStrategy } from './guards/localauth.strategy';
import { GuestService } from './guest.service';
@Module({ @Module({
imports: [ imports: [
@@ -32,6 +33,7 @@ import { LocalAuthStrategy } from './guards/localauth.strategy';
JwtStrategy, JwtStrategy,
GuestStrategy, GuestStrategy,
JwtSecretProvider, JwtSecretProvider,
GuestService,
], ],
exports: [AuthManagerService], exports: [AuthManagerService],
}) })

View File

@@ -4,6 +4,7 @@ import { Request } from 'express';
import { ParamsDictionary } from 'express-serve-static-core'; import { ParamsDictionary } from 'express-serve-static-core';
import { Strategy } from 'passport-strategy'; import { Strategy } from 'passport-strategy';
import { ParsedQs } from 'qs'; import { ParsedQs } from 'qs';
import { GuestService } from '../guest.service';
type ReqType = Request< type ReqType = Request<
ParamsDictionary, ParamsDictionary,
@@ -18,10 +19,9 @@ class GuestPassportStrategy extends Strategy {
return undefined; return undefined;
} }
override authenticate(req: ReqType, options?: any): void { override async authenticate(req: ReqType, options?: any) {
const user = this.validate(req); const user = await this.validate(req);
req['user'] = user; this.success(user);
this.pass();
} }
} }
@@ -32,8 +32,11 @@ export class GuestStrategy extends PassportStrategy(
) { ) {
private readonly logger = new Logger('GuestStrategy'); private readonly logger = new Logger('GuestStrategy');
constructor(private guestService: GuestService) {
super();
}
override async validate(payload: any) { override async validate(payload: any) {
// TODO: add guest user return this.guestService.createGuest();
return;
} }
} }

View File

@@ -1,5 +1,84 @@
import { Injectable } from '@nestjs/common'; import {
ExecutionContext,
Injectable,
InternalServerErrorException,
Logger
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { isArray, isEnum, isString, validate } from 'class-validator';
import { Roles, RolesList } from 'picsur-shared/dist/dto/roles.dto';
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable() @Injectable()
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {} export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
private readonly logger = new Logger('MainAuthGuard');
constructor(private reflector: Reflector) {
super();
}
override async canActivate(context: ExecutionContext): Promise<boolean> {
const result = await super.canActivate(context);
if (result !== true) {
this.logger.error('Main Auth has denied access, this should not happen');
return false;
}
const user = await this.validateUser(
context.switchToHttp().getRequest().user,
);
const roles = this.extractRoles(context);
if (HasFailed(roles)) {
this.logger.warn(roles.getReason());
return false;
}
// User must have all roles
return roles.every((role) => user.roles.includes(role));
}
private extractRoles(context: ExecutionContext): Failable<Roles> {
const handlerName = context.getHandler().name;
const roles =
this.reflector.get<Roles>('roles', context.getHandler()) ??
this.reflector.get<Roles>('roles', context.getClass());
if (roles === undefined) {
return Fail(
`${handlerName} does not have any roles defined, denying access`,
);
}
if (!this.isRolesArray(roles)) {
return Fail(`Roles for ${handlerName} is not a string array`);
}
return roles;
}
private isRolesArray(value: any): value is Roles {
if (!isArray(value)) return false;
if (!value.every((item: unknown) => isString(item))) return false;
if (!value.every((item: string) => isEnum(item, RolesList))) return false;
return true;
}
private async validateUser(user: EUserBackend): Promise<EUserBackend> {
const userClass = plainToClass(EUserBackend, user);
const errors = await validate(userClass, {
forbidUnknownValues: true,
});
if (errors.length > 0) {
this.logger.error(
'Invalid user object, where it should always be valid: ' + errors,
);
throw new InternalServerErrorException();
}
return userClass;
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable()
export class GuestService {
public createGuest(): EUserBackend {
const guest = new EUserBackend();
guest.id = -1;
guest.roles = this.createGuestRoles();
guest.username = 'guest';
return guest;
}
private createGuestRoles(): Roles {
return [];
}
}

View File

@@ -4,8 +4,7 @@ import {
Get, Get,
InternalServerErrorException, InternalServerErrorException,
Post, Post,
Request, Request
UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
AuthDeleteRequest, AuthDeleteRequest,
@@ -14,9 +13,8 @@ import {
AuthRegisterRequest AuthRegisterRequest
} from 'picsur-shared/dist/dto/auth.dto'; } from 'picsur-shared/dist/dto/auth.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { Authenticated } from '../../../decorators/authenticated'; import { Admin, UseLocalAuth, User } from '../../../decorators/roles.decorator';
import { AuthManagerService } from '../../../managers/auth/auth.service'; import { AuthManagerService } from '../../../managers/auth/auth.service';
import { LocalAuthGuard } from '../../../managers/auth/guards/localauth.guard';
import AuthFasityRequest from '../../../models/dto/authrequest.dto'; import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/auth') @Controller('api/auth')
@@ -24,7 +22,7 @@ export class AuthController {
constructor(private authService: AuthManagerService) {} constructor(private authService: AuthManagerService) {}
@Post('login') @Post('login')
@UseGuards(LocalAuthGuard) @UseLocalAuth()
async login(@Request() req: AuthFasityRequest) { async login(@Request() req: AuthFasityRequest) {
const response: AuthLoginResponse = { const response: AuthLoginResponse = {
jwt_token: await this.authService.createToken(req.user), jwt_token: await this.authService.createToken(req.user),
@@ -34,7 +32,7 @@ export class AuthController {
} }
@Post('create') @Post('create')
@Authenticated(true) @Admin()
async register( async register(
@Request() req: AuthFasityRequest, @Request() req: AuthFasityRequest,
@Body() register: AuthRegisterRequest, @Body() register: AuthRegisterRequest,
@@ -56,7 +54,7 @@ export class AuthController {
} }
@Post('delete') @Post('delete')
@Authenticated(true) @Admin()
async delete( async delete(
@Request() req: AuthFasityRequest, @Request() req: AuthFasityRequest,
@Body() deleteData: AuthDeleteRequest, @Body() deleteData: AuthDeleteRequest,
@@ -71,7 +69,7 @@ export class AuthController {
} }
@Get('list') @Get('list')
@Authenticated(true) @Admin()
async listUsers(@Request() req: AuthFasityRequest) { async listUsers(@Request() req: AuthFasityRequest) {
const users = this.authService.listUsers(); const users = this.authService.listUsers();
if (HasFailed(users)) { if (HasFailed(users)) {
@@ -83,7 +81,7 @@ export class AuthController {
} }
@Get('me') @Get('me')
@Authenticated() @User()
async me(@Request() req: AuthFasityRequest) { async me(@Request() req: AuthFasityRequest) {
const meResponse: AuthMeResponse = new AuthMeResponse(); const meResponse: AuthMeResponse = new AuthMeResponse();
meResponse.user = req.user; meResponse.user = req.user;

View File

@@ -1,13 +1,13 @@
import { Controller, Get, Request, UseGuards } from '@nestjs/common'; import { Controller, Get, Request } from '@nestjs/common';
import { MainAuthGuard } from '../../../managers/auth/guards/main.guard'; import { Guest } from '../../../decorators/roles.decorator';
import AuthFasityRequest from '../../../models/dto/authrequest.dto'; import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/experiment') @Controller('api/experiment')
export class ExperimentController { export class ExperimentController {
@Get() @Get()
@UseGuards(MainAuthGuard) @Guest()
async testRoute(@Request() req: AuthFasityRequest) { async testRoute(@Request() req: AuthFasityRequest) {
console.log("calledroutes")
return { return {
message: req.user, message: req.user,
}; };

View File

@@ -12,10 +12,10 @@ import {
} from 'picsur-shared/dist/dto/syspreferences.dto'; } from 'picsur-shared/dist/dto/syspreferences.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service'; import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
import { Authenticated } from '../../../decorators/authenticated'; import { Admin } from '../../../decorators/roles.decorator';
@Controller('api/pref') @Controller('api/pref')
@Authenticated(true) @Admin()
export class PrefController { export class PrefController {
constructor(private prefService: SysPreferenceService) {} constructor(private prefService: SysPreferenceService) {}

View File

@@ -12,11 +12,13 @@ import {
import { isHash } from 'class-validator'; import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify'; import { FastifyReply, FastifyRequest } from 'fastify';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart'; import { MultiPart } from '../../decorators/multipart.decorator';
import { Guest } from '../../decorators/roles.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { ImageUploadDto } from '../../models/dto/imageroute.dto'; import { ImageUploadDto } from '../../models/dto/imageroute.dto';
@Controller('i') @Controller('i')
@Guest()
export class ImageController { export class ImageController {
constructor(private readonly imagesService: ImageManagerService) {} constructor(private readonly imagesService: ImageManagerService) {}
@@ -51,6 +53,7 @@ export class ImageController {
} }
@Post() @Post()
//@User()
async uploadImage( async uploadImage(
@Req() req: FastifyRequest, @Req() req: FastifyRequest,
@MultiPart(ImageUploadDto) multipart: ImageUploadDto, @MultiPart(ImageUploadDto) multipart: ImageUploadDto,

View File

@@ -0,0 +1,9 @@
type FCDecorator = MethodDecorator & ClassDecorator;
export function CombineDecorators(...decorators: FCDecorator[]) {
return (target: any, key: string, descriptor: PropertyDescriptor) => {
decorators.forEach(decorator => {
decorator(target, key, descriptor);
});
}
}