part of frontend permission guard

This commit is contained in:
rubikscraft 2022-03-12 23:09:46 +01:00
parent 7026c8cb67
commit ac72035f76
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
15 changed files with 117 additions and 52 deletions

View file

@ -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<EUserBackend> {
const userClass = plainToClass(EUserBackend, user);
const errors = await validate(userClass, {

View file

@ -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() {

View file

@ -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<UserLoginResponse> {
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<UserRegisterResponse> {
@ -74,7 +75,7 @@ export class UserController {
}
@Post('delete')
@RequiredPermissions('user-manage')
@RequiredPermissions(Permission.UserManage)
async delete(
@Body() deleteData: UserDeleteRequest,
): Promise<UserDeleteResponse> {
@ -88,7 +89,7 @@ export class UserController {
}
@Post('roles')
@RequiredPermissions('user-manage')
@RequiredPermissions(Permission.UserManage)
async setPermissions(
@Body() body: UserUpdateRolesRequest,
): Promise<UserUpdateRolesResponse> {
@ -106,7 +107,7 @@ export class UserController {
}
@Post('info')
@RequiredPermissions('user-manage')
@RequiredPermissions(Permission.UserManage)
async getUser(@Body() body: UserInfoRequest): Promise<UserInfoResponse> {
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<UserListResponse> {
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<UserMeResponse> {
return {
user: req.user,

View file

@ -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');

View file

@ -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');

View file

@ -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,

View file

@ -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(

View file

@ -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 {}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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();

View file

@ -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(

View file

@ -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[];

View file

@ -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

View file

@ -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;
}