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
todo.txt

View File

@@ -61,7 +61,9 @@ export class SysPreferenceService {
ESysPreferenceBackend,
foundSysPreference,
);
const errors = await validate(foundSysPreference);
const errors = await validate(foundSysPreference, {
forbidUnknownValues: true,
});
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid preference');
@@ -85,7 +87,9 @@ export class SysPreferenceService {
verifySysPreference.key = key as SysPreferences;
verifySysPreference.value = value;
const errors = await validate(verifySysPreference);
const errors = await validate(verifySysPreference, {
forbidUnknownValues: true,
});
if (errors.length > 0) {
this.logger.warn(errors);
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 { NestFactory } from '@nestjs/core';
import { NestFactory, Reflector } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication
@@ -10,6 +10,7 @@ import { HostConfigService } from './config/host.config.service';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor';
import { PicsurLoggerService } from './logger/logger.service';
import { MainAuthGuard } from './managers/auth/guards/main.guard';
async function bootstrap() {
const fastifyAdapter = new FastifyAdapter();
@@ -32,6 +33,7 @@ async function bootstrap() {
forbidUnknownValues: true,
}),
);
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
app.useLogger(app.get(PicsurLoggerService));

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -12,11 +12,13 @@ import {
import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify';
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 { ImageUploadDto } from '../../models/dto/imageroute.dto';
@Controller('i')
@Guest()
export class ImageController {
constructor(private readonly imagesService: ImageManagerService) {}
@@ -51,6 +53,7 @@ export class ImageController {
}
@Post()
//@User()
async uploadImage(
@Req() req: FastifyRequest,
@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);
});
}
}