diff --git a/backend/src/managers/auth/guards/main.guard.ts b/backend/src/managers/auth/guards/main.guard.ts index bc7e268..2986fec 100644 --- a/backend/src/managers/auth/guards/main.guard.ts +++ b/backend/src/managers/auth/guards/main.guard.ts @@ -8,13 +8,12 @@ import { import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { plainToClass } from 'class-transformer'; -import { isArray, isEnum, isString, validate } from 'class-validator'; +import { validate } from 'class-validator'; import { - Permissions, - PermissionsList + Permissions } from 'picsur-shared/dist/dto/permissions'; -import { Roles } from 'picsur-shared/dist/dto/roles.dto'; import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types'; +import { isPermissionsArray } from 'picsur-shared/dist/util/permissions'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { EUserBackend } from '../../../models/entities/user.entity'; @@ -42,13 +41,13 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { const permissions = this.extractPermissions(context); if (HasFailed(permissions)) { - this.logger.warn("222"+permissions.getReason()); + this.logger.warn('222' + permissions.getReason()); throw new InternalServerErrorException(); } const userPermissions = await this.usersService.getPermissions(user); if (HasFailed(userPermissions)) { - this.logger.warn("111"+userPermissions.getReason()); + this.logger.warn('111' + userPermissions.getReason()); throw new InternalServerErrorException(); } @@ -69,21 +68,13 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { ); } - if (!this.isPermissionsArray(permissions)) { + if (!isPermissionsArray(permissions)) { return Fail(`Permissions for ${handlerName} is not a string array`); } return permissions; } - private isPermissionsArray(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, PermissionsList))) - return false; - return true; - } - private async validateUser(user: EUserBackend): Promise { const userClass = plainToClass(EUserBackend, user); const errors = await validate(userClass, { diff --git a/backend/src/managers/demo/demomanager.service.ts b/backend/src/managers/demo/demomanager.service.ts index afa5647..90914ca 100644 --- a/backend/src/managers/demo/demomanager.service.ts +++ b/backend/src/managers/demo/demomanager.service.ts @@ -1,4 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { ImageDBService } from '../../collections/imagedb/imagedb.service'; import { RolesService } from '../../collections/roledb/roledb.service'; @@ -15,7 +16,7 @@ export class DemoManagerService { this.logger.warn( 'Modifying roles for demo mode, this will not be reverted automatically', ); - this.rolesService.addPermissions('guest', ['image-upload']); + this.rolesService.addPermissions('guest', [Permission.ImageUpload]); } public execute() { diff --git a/backend/src/routes/api/auth/user.controller.ts b/backend/src/routes/api/auth/user.controller.ts index 5c4c076..e185d15 100644 --- a/backend/src/routes/api/auth/user.controller.ts +++ b/backend/src/routes/api/auth/user.controller.ts @@ -21,6 +21,7 @@ import { UserUpdateRolesRequest, UserUpdateRolesResponse } from 'picsur-shared/dist/dto/api/user.dto'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { @@ -41,7 +42,7 @@ export class UserController { ) {} @Post('login') - @UseLocalAuth('user-login') + @UseLocalAuth(Permission.UserLogin) async login(@Request() req: AuthFasityRequest): Promise { return { jwt_token: await this.authService.createToken(req.user), @@ -49,7 +50,7 @@ export class UserController { } @Post('register') - @RequiredPermissions('user-register') + @RequiredPermissions(Permission.UserRegister) async register( @Body() register: UserRegisterRequest, ): Promise { @@ -74,7 +75,7 @@ export class UserController { } @Post('delete') - @RequiredPermissions('user-manage') + @RequiredPermissions(Permission.UserManage) async delete( @Body() deleteData: UserDeleteRequest, ): Promise { @@ -88,7 +89,7 @@ export class UserController { } @Post('roles') - @RequiredPermissions('user-manage') + @RequiredPermissions(Permission.UserManage) async setPermissions( @Body() body: UserUpdateRolesRequest, ): Promise { @@ -106,7 +107,7 @@ export class UserController { } @Post('info') - @RequiredPermissions('user-manage') + @RequiredPermissions(Permission.UserManage) async getUser(@Body() body: UserInfoRequest): Promise { const user = await this.usersService.findOne(body.username); if (HasFailed(user)) { @@ -118,7 +119,7 @@ export class UserController { } @Get('list') - @RequiredPermissions('user-manage') + @RequiredPermissions(Permission.UserManage) async listUsers(): Promise { const users = await this.usersService.findAll(); if (HasFailed(users)) { @@ -133,7 +134,7 @@ export class UserController { } @Get('me') - @RequiredPermissions('user-view') + @RequiredPermissions(Permission.UserView) async me(@Request() req: AuthFasityRequest): Promise { return { user: req.user, diff --git a/backend/src/routes/api/pref/pref.controller.ts b/backend/src/routes/api/pref/pref.controller.ts index ae39977..dcfa455 100644 --- a/backend/src/routes/api/pref/pref.controller.ts +++ b/backend/src/routes/api/pref/pref.controller.ts @@ -11,13 +11,14 @@ import { SysPreferenceResponse, UpdateSysPreferenceRequest } from 'picsur-shared/dist/dto/api/pref.dto'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; @Controller('api/pref') -@RequiredPermissions('syspref-manage') +@RequiredPermissions(Permission.SysPrefManage) export class PrefController { private readonly logger = new Logger('PrefController'); diff --git a/backend/src/routes/api/roles/roles.controller.ts b/backend/src/routes/api/roles/roles.controller.ts index 6b8c0db..7fa3ad3 100644 --- a/backend/src/routes/api/roles/roles.controller.ts +++ b/backend/src/routes/api/roles/roles.controller.ts @@ -17,12 +17,13 @@ import { RoleUpdateRequest, RoleUpdateResponse } from 'picsur-shared/dist/dto/api/roles.dto'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { RolesService } from '../../../collections/roledb/roledb.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; @Controller('api/roles') -@RequiredPermissions('role-manage') +@RequiredPermissions(Permission.RoleManage) export class RolesController { private readonly logger = new Logger('RolesController'); diff --git a/backend/src/routes/image/imageroute.controller.ts b/backend/src/routes/image/imageroute.controller.ts index 68f8db3..0449d5c 100644 --- a/backend/src/routes/image/imageroute.controller.ts +++ b/backend/src/routes/image/imageroute.controller.ts @@ -13,6 +13,7 @@ import { import { isHash } from 'class-validator'; import { FastifyReply, FastifyRequest } from 'fastify'; import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { MultiPart } from '../../decorators/multipart.decorator'; import { RequiredPermissions } from '../../decorators/permissions.decorator'; @@ -20,7 +21,7 @@ import { ImageManagerService } from '../../managers/imagemanager/imagemanager.se import { ImageUploadDto } from '../../models/dto/imageroute.dto'; @Controller('i') -@RequiredPermissions('image-view') +@RequiredPermissions(Permission.ImageView) export class ImageController { private readonly logger = new Logger('ImageController'); @@ -57,7 +58,7 @@ export class ImageController { } @Post() - @RequiredPermissions('image-upload') + @RequiredPermissions(Permission.ImageUpload) async uploadImage( @Req() req: FastifyRequest, @MultiPart(ImageUploadDto) multipart: ImageUploadDto, diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 468b1ec..487ffc7 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { HasFailed } from 'picsur-shared/dist/types'; import { PermissionService } from 'src/app/api/permission.service'; @@ -29,7 +29,7 @@ export class HeaderComponent implements OnInit { } public get canLogIn() { - return this.permissions.includes('user-login'); + return this.permissions.includes(Permission.UserLogin); } constructor( diff --git a/frontend/src/app/guards/guards.module.ts b/frontend/src/app/guards/guards.module.ts new file mode 100644 index 0000000..2d2f1bb --- /dev/null +++ b/frontend/src/app/guards/guards.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ApiModule } from '../api/api.module'; +import { PermissionGuard } from './permission.guard'; + +@NgModule({ + imports: [CommonModule, ApiModule], + providers: [PermissionGuard], + exports: [], +}) +export class GuardsModule {} diff --git a/frontend/src/app/guards/permission.guard.ts b/frontend/src/app/guards/permission.guard.ts new file mode 100644 index 0000000..56091e9 --- /dev/null +++ b/frontend/src/app/guards/permission.guard.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + RouterStateSnapshot +} from '@angular/router'; +import { Permissions } from 'picsur-shared/dist/dto/permissions'; +import { isPermissionsArray } from 'picsur-shared/dist/util/permissions'; +import { PermissionService } from '../api/permission.service'; + +@Injectable({ + providedIn: 'root', +}) +export class PermissionGuard implements CanActivate { + constructor(private permissionService: PermissionService) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + const requiredPermissions: Permissions = route.data['permissions']; + if (!isPermissionsArray(requiredPermissions)) { + throw new Error( + `PermissionGuard: route data 'permissions' must be an array of Permission values` + ); + } + + const ourPermissions = this.permissionService.snapshot; + + const isOk = requiredPermissions.every((permission) => + ourPermissions.includes(permission) + ); + + console.log( + `PermissionGuard: requiredPermissions=${requiredPermissions} ourPermissions=${ourPermissions} isOk=${isOk}` + ); + + return isOk; + } +} diff --git a/frontend/src/app/router/router.module.ts b/frontend/src/app/router/router.module.ts index c1f83fc..241640b 100644 --- a/frontend/src/app/router/router.module.ts +++ b/frontend/src/app/router/router.module.ts @@ -6,10 +6,13 @@ import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { RouterModule, Routes } from '@angular/router'; import { NgxDropzoneModule } from 'ngx-dropzone'; +import { Permission } from 'picsur-shared/dist/dto/permissions'; import { ApiModule } from '../api/api.module'; import { CopyFieldModule } from '../components/copyfield/copyfield.module'; import { PageNotFoundComponent } from '../components/pagenotfound/pagenotfound.component'; import { PageNotFoundModule } from '../components/pagenotfound/pagenotfound.module'; +import { GuardsModule } from '../guards/guards.module'; +import { PermissionGuard } from '../guards/permission.guard'; import { LoginComponent } from '../routes/login/login.component'; import { ProcessingComponent } from '../routes/processing/processing.component'; import { UploadComponent } from '../routes/upload/upload.component'; @@ -25,13 +28,19 @@ const routes: Routes = [ component: ProcessingComponent, }, { path: 'view/:hash', component: ViewComponent }, - { path: 'login', component: LoginComponent }, + { + path: 'login', + component: LoginComponent, + canActivate: [PermissionGuard], + data: { permissions: [Permission.UserLogin] }, + }, { path: '**', component: PageNotFoundComponent }, ]; @NgModule({ imports: [ CommonModule, + GuardsModule, NgxDropzoneModule, UtilModule, MatProgressSpinnerModule, diff --git a/frontend/src/app/routes/login/login.component.ts b/frontend/src/app/routes/login/login.component.ts index 0c381c3..7de28e1 100644 --- a/frontend/src/app/routes/login/login.component.ts +++ b/frontend/src/app/routes/login/login.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { PermissionService } from 'src/app/api/permission.service'; import { UserService } from 'src/app/api/user.service'; @@ -20,7 +20,7 @@ export class LoginComponent implements OnInit { private permissions: Permissions = []; public get showRegister() { - return this.permissions.includes('user-register'); + return this.permissions.includes(Permission.UserRegister); } model = new LoginControl(); diff --git a/frontend/src/app/routes/upload/upload.component.ts b/frontend/src/app/routes/upload/upload.component.ts index d1233be..387f9c8 100644 --- a/frontend/src/app/routes/upload/upload.component.ts +++ b/frontend/src/app/routes/upload/upload.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { NgxDropzoneChangeEvent } from 'ngx-dropzone'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; import { PermissionService } from 'src/app/api/permission.service'; import { UtilService } from 'src/app/util/util.service'; import { ProcessingViewMetadata } from '../../models/processing-view-metadata'; @@ -16,7 +16,7 @@ export class UploadComponent implements OnInit { // Lets be optimistic here, this makes for a better ux public get hasUploadPermission() { - return this.permissions.includes('image-upload'); + return this.permissions.includes(Permission.ImageUpload); } constructor( diff --git a/shared/src/dto/permissions.ts b/shared/src/dto/permissions.ts index d1b11d1..20df328 100644 --- a/shared/src/dto/permissions.ts +++ b/shared/src/dto/permissions.ts @@ -1,21 +1,18 @@ -import tuple from '../types/tuple'; - // Config -const PermissionsTuple = tuple( - 'image-view', - 'image-upload', - 'user-login', // Ability to log in - 'user-register', // Ability to register - 'user-manage', - 'user-view', // Ability to view user details and refresh token - 'role-manage', - 'syspref-manage', -); +export enum Permission { + ImageView = 'image-view', + ImageUpload = 'image-upload', + UserLogin = 'user-login', // Ability to log in + UserRegister = 'user-register', // Ability to register + UserManage = 'user-manage', + UserView = 'user-view', // Ability to view user details and refresh token + RoleManage = 'role-manage', + SysPrefManage = 'syspref-manage', +} // Derivatives -export const PermissionsList: string[] = PermissionsTuple; +export const PermissionsList: Permission[] = Object.values(Permission); -export type Permission = typeof PermissionsTuple[number]; export type Permissions = Permission[]; diff --git a/shared/src/dto/roles.dto.ts b/shared/src/dto/roles.dto.ts index 8cf14c4..b8752dc 100644 --- a/shared/src/dto/roles.dto.ts +++ b/shared/src/dto/roles.dto.ts @@ -1,5 +1,5 @@ import tuple from '../types/tuple'; -import { Permissions, PermissionsList } from './permissions'; +import { Permission, Permissions, PermissionsList } from './permissions'; // Config @@ -24,10 +24,15 @@ export type SystemRoles = SystemRole[]; export const SystemRoleDefaults: { [key in SystemRole]: Permissions; } = { - guest: ['image-view', 'user-login'], - user: ['image-view', 'user-view', 'user-login', 'image-upload'], + guest: [Permission.ImageView, Permission.UserLogin], + user: [ + Permission.ImageView, + Permission.UserView, + Permission.UserLogin, + Permission.ImageUpload, + ], // Grant all permissions to admin - admin: PermissionsList as Permissions, + admin: PermissionsList, }; // Normal roles types diff --git a/shared/src/util/permissions.ts b/shared/src/util/permissions.ts new file mode 100644 index 0000000..bfd1630 --- /dev/null +++ b/shared/src/util/permissions.ts @@ -0,0 +1,10 @@ +import { isArray, isEnum, isString } from 'class-validator'; +import { Permissions, PermissionsList } from '../dto/permissions'; + +export function isPermissionsArray(value: any): value is Permissions { + if (!isArray(value)) return false; + if (!value.every((item: unknown) => isString(item))) return false; + if (!value.every((item: string) => isEnum(item, PermissionsList))) + return false; + return true; +}