mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-14 15:45:49 +01:00
apply role guard to all routes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
todo.txt
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
|
||||||
21
backend/src/decorators/roles.decorator.ts
Normal file
21
backend/src/decorators/roles.decorator.ts
Normal 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));
|
||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
backend/src/managers/auth/guest.service.ts
Normal file
19
backend/src/managers/auth/guest.service.ts
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
9
shared/src/util/decorator.ts
Normal file
9
shared/src/util/decorator.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user