From 2c150c30277b25313e22d801c07f48fa61af034e Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Thu, 24 Mar 2022 23:08:37 +0100 Subject: [PATCH] migrate permission logic to more backend --- .../src/collections/roledb/roledb.service.ts | 2 +- .../collections/userdb/userrolesdb.service.ts | 2 +- .../src/decorators/permissions.decorator.ts | 2 +- .../src/managers/auth/guards/main.guard.ts | 6 ++-- .../src/managers/demo/demomanager.service.ts | 2 +- backend/src/models/dto/permissions.dto.ts | 10 ++++++ backend/src/models/dto/roles.dto.ts | 7 ++-- backend/src/models/entities/role.entity.ts | 2 +- backend/src/models/util/permissions.ts | 10 ++++++ .../src/routes/api/info/info.controller.ts | 15 +++++++- .../src/routes/api/pref/pref.controller.ts | 2 +- .../src/routes/api/roles/roles.controller.ts | 22 +++++++++--- .../src/routes/api/user/user.controller.ts | 2 +- .../routes/api/user/usermanage.controller.ts | 2 +- .../src/routes/image/imageroute.controller.ts | 2 +- .../app/components/header/header.component.ts | 4 +-- frontend/src/app/guards/permission.guard.ts | 19 +++++------ frontend/src/app/models/forms/role.model.ts | 4 +-- .../app/models/forms/updaterole.control.ts | 21 ++++++------ .../app/models/forms/updateuser.control.ts | 5 ++- frontend/src/app/models/picsur-routes.ts | 3 +- .../settings-roles-edit.component.ts | 27 +++++++++------ .../roles/settings-roles.component.ts | 5 +-- .../settings/settings.routing.module.ts | 2 +- .../src/app/routes/upload/upload.component.ts | 4 +-- .../app/routes/user/login/login.component.ts | 4 +-- .../user/register/register.component.ts | 4 +-- .../app/routes/user/user.routing.module.ts | 2 +- .../app/routes/view/view.routing.module.ts | 2 +- .../app/services/api/permission.service.ts | 34 ++++++++++++------- .../src/app/services/api/syspref.service.ts | 6 ++-- shared/src/dto/api/roles.dto.ts | 7 ++++ shared/src/dto/api/user.dto.ts | 11 +++--- .../{permissions.ts => permissions.dto.ts} | 16 +-------- shared/src/entities/role.entity.ts | 10 +++--- shared/src/util/permissions.ts | 17 ++-------- 36 files changed, 166 insertions(+), 129 deletions(-) create mode 100644 backend/src/models/dto/permissions.dto.ts create mode 100644 backend/src/models/util/permissions.ts rename shared/src/dto/{permissions.ts => permissions.dto.ts} (62%) diff --git a/backend/src/collections/roledb/roledb.service.ts b/backend/src/collections/roledb/roledb.service.ts index d678648..b5674d3 100644 --- a/backend/src/collections/roledb/roledb.service.ts +++ b/backend/src/collections/roledb/roledb.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { plainToClass } from 'class-transformer'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { AsyncFailable, Fail, @@ -10,6 +9,7 @@ import { } from 'picsur-shared/dist/types'; import { strictValidate } from 'picsur-shared/dist/util/validate'; import { In, Repository } from 'typeorm'; +import { Permissions } from '../../models/dto/permissions.dto'; import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto'; import { ERoleBackend } from '../../models/entities/role.entity'; diff --git a/backend/src/collections/userdb/userrolesdb.service.ts b/backend/src/collections/userdb/userrolesdb.service.ts index 6a883b9..478f412 100644 --- a/backend/src/collections/userdb/userrolesdb.service.ts +++ b/backend/src/collections/userdb/userrolesdb.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; +import { Permissions } from '../../models/dto/permissions.dto'; import { EUserBackend } from '../../models/entities/user.entity'; import { RolesService } from '../roledb/roledb.service'; import { UsersService } from './userdb.service'; diff --git a/backend/src/decorators/permissions.decorator.ts b/backend/src/decorators/permissions.decorator.ts index 5532641..177ef97 100644 --- a/backend/src/decorators/permissions.decorator.ts +++ b/backend/src/decorators/permissions.decorator.ts @@ -1,7 +1,7 @@ import { SetMetadata, UseGuards } from '@nestjs/common'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { CombineDecorators } from 'picsur-shared/dist/util/decorator'; import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard'; +import { Permissions } from '../models/dto/permissions.dto'; export const RequiredPermissions = (...permissions: Permissions) => { return SetMetadata('permissions', permissions); diff --git a/backend/src/managers/auth/guards/main.guard.ts b/backend/src/managers/auth/guards/main.guard.ts index d50b328..9a07333 100644 --- a/backend/src/managers/auth/guards/main.guard.ts +++ b/backend/src/managers/auth/guards/main.guard.ts @@ -8,15 +8,13 @@ import { import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { plainToClass } from 'class-transformer'; -import { - Permissions -} from 'picsur-shared/dist/dto/permissions'; import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types'; -import { isPermissionsArray } from 'picsur-shared/dist/util/permissions'; import { strictValidate } from 'picsur-shared/dist/util/validate'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { UserRolesService } from '../../../collections/userdb/userrolesdb.service'; +import { Permissions } from '../../../models/dto/permissions.dto'; import { EUserBackend } from '../../../models/entities/user.entity'; +import { isPermissionsArray } from '../../../models/util/permissions'; @Injectable() export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { diff --git a/backend/src/managers/demo/demomanager.service.ts b/backend/src/managers/demo/demomanager.service.ts index 90914ca..498d6cf 100644 --- a/backend/src/managers/demo/demomanager.service.ts +++ b/backend/src/managers/demo/demomanager.service.ts @@ -1,7 +1,7 @@ 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'; +import { Permission } from '../../models/dto/permissions.dto'; @Injectable() export class DemoManagerService { diff --git a/backend/src/models/dto/permissions.dto.ts b/backend/src/models/dto/permissions.dto.ts new file mode 100644 index 0000000..d23c114 --- /dev/null +++ b/backend/src/models/dto/permissions.dto.ts @@ -0,0 +1,10 @@ +// Config + +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; +export { Permission } from 'picsur-shared/dist/dto/permissions.dto'; + +// Derivatives + +export const PermissionsList: Permission[] = Object.values(Permission); + +export type Permissions = Permission[]; diff --git a/backend/src/models/dto/roles.dto.ts b/backend/src/models/dto/roles.dto.ts index 84fcf4a..f2b73e5 100644 --- a/backend/src/models/dto/roles.dto.ts +++ b/backend/src/models/dto/roles.dto.ts @@ -1,5 +1,5 @@ -import { Permission, Permissions, PermissionsList } from 'picsur-shared/dist/dto/permissions'; import tuple from 'picsur-shared/dist/types/tuple'; +import { Permission, Permissions, PermissionsList } from './permissions.dto'; // Config @@ -8,7 +8,10 @@ const SoulBoundRolesTuple = tuple('guest', 'user'); // These roles can never be modified const ImmutableRolesTuple = tuple('admin'); // These roles can never be removed from the server -const UndeletableRolesTuple = tuple(...SoulBoundRolesTuple, ...ImmutableRolesTuple); +const UndeletableRolesTuple = tuple( + ...SoulBoundRolesTuple, + ...ImmutableRolesTuple, +); // These roles will be applied by default to new users export const DefaultRolesList: string[] = ['user']; diff --git a/backend/src/models/entities/role.entity.ts b/backend/src/models/entities/role.entity.ts index 5deef65..015f56e 100644 --- a/backend/src/models/entities/role.entity.ts +++ b/backend/src/models/entities/role.entity.ts @@ -1,6 +1,6 @@ -import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { Permissions } from '../dto/permissions.dto'; @Entity() export class ERoleBackend extends ERole { diff --git a/backend/src/models/util/permissions.ts b/backend/src/models/util/permissions.ts new file mode 100644 index 0000000..ce423b4 --- /dev/null +++ b/backend/src/models/util/permissions.ts @@ -0,0 +1,10 @@ +import { isArray, isEnum, isString } from 'class-validator'; +import { Permissions, PermissionsList } from '../dto/permissions.dto'; + +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; +} diff --git a/backend/src/routes/api/info/info.controller.ts b/backend/src/routes/api/info/info.controller.ts index b5e3686..0927beb 100644 --- a/backend/src/routes/api/info/info.controller.ts +++ b/backend/src/routes/api/info/info.controller.ts @@ -1,14 +1,17 @@ import { Controller, Get } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; +import { AllPermissionsResponse } from 'picsur-shared/dist/dto/api/roles.dto'; import { HostConfigService } from '../../../config/host.config.service'; import { NoPermissions } from '../../../decorators/permissions.decorator'; +import { PermissionsList } from '../../../models/dto/permissions.dto'; @Controller('api/info') +@NoPermissions() export class InfoController { constructor(private hostConfig: HostConfigService) {} @Get() - @NoPermissions() async getInfo(): Promise { return { demo: this.hostConfig.isDemo(), @@ -16,4 +19,14 @@ export class InfoController { version: this.hostConfig.getVersion(), }; } + + // List all available permissions + @Get('/permissions') + async getPermissions(): Promise { + const result: AllPermissionsResponse = { + Permissions: PermissionsList, + }; + + return plainToClass(AllPermissionsResponse, result); + } } diff --git a/backend/src/routes/api/pref/pref.controller.ts b/backend/src/routes/api/pref/pref.controller.ts index 2f0a79b..78c486d 100644 --- a/backend/src/routes/api/pref/pref.controller.ts +++ b/backend/src/routes/api/pref/pref.controller.ts @@ -15,11 +15,11 @@ import { UpdateSysPreferenceRequest, UpdateSysPreferenceResponse } 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'; +import { Permission } from '../../../models/dto/permissions.dto'; @Controller('api/pref') @RequiredPermissions(Permission.SysPrefManage) diff --git a/backend/src/routes/api/roles/roles.controller.ts b/backend/src/routes/api/roles/roles.controller.ts index 8d33c4c..77df76d 100644 --- a/backend/src/routes/api/roles/roles.controller.ts +++ b/backend/src/routes/api/roles/roles.controller.ts @@ -19,16 +19,19 @@ import { RoleUpdateResponse, SpecialRolesResponse } 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'; +import { + Permission +} from '../../../models/dto/permissions.dto'; import { DefaultRolesList, ImmutableRolesList, SoulBoundRolesList, UndeletableRolesList } from '../../../models/dto/roles.dto'; +import { isPermissionsArray } from '../../../models/util/permissions'; @Controller('api/roles') @RequiredPermissions(Permission.RoleManage) @@ -66,9 +69,14 @@ export class RolesController { async updateRole( @Body() body: RoleUpdateRequest, ): Promise { + const permissions = body.permissions; + if (!isPermissionsArray(permissions)) { + throw new InternalServerErrorException('Invalid permissions array'); + } + const updatedRole = await this.rolesService.setPermissions( body.name, - body.permissions, + permissions, ); if (HasFailed(updatedRole)) { this.logger.warn(updatedRole.getReason()); @@ -82,7 +90,12 @@ export class RolesController { async createRole( @Body() role: RoleCreateRequest, ): Promise { - const newRole = await this.rolesService.create(role.name, role.permissions); + const permissions = role.permissions; + if (!isPermissionsArray(permissions)) { + throw new InternalServerErrorException('Invalid permissions array'); + } + + const newRole = await this.rolesService.create(role.name, permissions); if (HasFailed(newRole)) { this.logger.warn(newRole.getReason()); throw new InternalServerErrorException('Could not create role'); @@ -104,7 +117,7 @@ export class RolesController { return deletedRole; } - @Get('special') + @Get('/special') async getSpecialRoles(): Promise { const result: SpecialRolesResponse = { SoulBoundRoles: SoulBoundRolesList, @@ -115,4 +128,5 @@ export class RolesController { return plainToClass(SpecialRolesResponse, result); } + } diff --git a/backend/src/routes/api/user/user.controller.ts b/backend/src/routes/api/user/user.controller.ts index 53c61ee..f6b7e2e 100644 --- a/backend/src/routes/api/user/user.controller.ts +++ b/backend/src/routes/api/user/user.controller.ts @@ -14,7 +14,6 @@ import { UserRegisterRequest, UserRegisterResponse } 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 { UserRolesService } from '../../../collections/userdb/userrolesdb.service'; @@ -24,6 +23,7 @@ import { UseLocalAuth } from '../../../decorators/permissions.decorator'; import { AuthManagerService } from '../../../managers/auth/auth.service'; +import { Permission } from '../../../models/dto/permissions.dto'; import AuthFasityRequest from '../../../models/requests/authrequest.dto'; @Controller('api/user') diff --git a/backend/src/routes/api/user/usermanage.controller.ts b/backend/src/routes/api/user/usermanage.controller.ts index 54d2b99..579487c 100644 --- a/backend/src/routes/api/user/usermanage.controller.ts +++ b/backend/src/routes/api/user/usermanage.controller.ts @@ -20,10 +20,10 @@ import { UserUpdateRequest, UserUpdateResponse } from 'picsur-shared/dist/dto/api/usermanage.dto'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../../collections/userdb/userdb.service'; import { RequiredPermissions } from '../../../decorators/permissions.decorator'; +import { Permission } from '../../../models/dto/permissions.dto'; import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto'; @Controller('api/user') diff --git a/backend/src/routes/image/imageroute.controller.ts b/backend/src/routes/image/imageroute.controller.ts index 7107e4f..c5823c0 100644 --- a/backend/src/routes/image/imageroute.controller.ts +++ b/backend/src/routes/image/imageroute.controller.ts @@ -13,11 +13,11 @@ 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'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; +import { Permission } from '../../models/dto/permissions.dto'; import { ImageUploadDto } from '../../models/requests/imageroute.dto'; @Controller('i') diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index 7e479dc..7806590 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, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { HasFailed } from 'picsur-shared/dist/types'; import { SnackBarType } from 'src/app/models/snack-bar-type'; @@ -21,7 +21,7 @@ export class HeaderComponent implements OnInit { @Output('onHamburgerClick') onHamburgerClick = new EventEmitter(); private currentUser: EUser | null = null; - private permissions: Permissions = []; + private permissions: string[] = []; public get user() { return this.currentUser; diff --git a/frontend/src/app/guards/permission.guard.ts b/frontend/src/app/guards/permission.guard.ts index a41a730..7030e44 100644 --- a/frontend/src/app/guards/permission.guard.ts +++ b/frontend/src/app/guards/permission.guard.ts @@ -6,8 +6,6 @@ import { Router, RouterStateSnapshot } from '@angular/router'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; -import { isPermissionsArray } from 'picsur-shared/dist/util/permissions'; import { PRouteData } from '../models/picsur-routes'; import { PermissionService } from '../services/api/permission.service'; @@ -34,12 +32,13 @@ export class PermissionGuard implements CanActivate, CanActivateChild { } private async can(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const requiredPermissions: Permissions = this.nestedPermissions(route); - if (!isPermissionsArray(requiredPermissions)) { - throw new Error( - `PermissionGuard: route data 'permissions' must be an array of Permission values` - ); - } + const requiredPermissions: string[] = this.nestedPermissions(route); + // TODO: revive + // if (!isPermissionsArray(requiredPermissions)) { + // throw new Error( + // `PermissionGuard: route data 'permissions' must be an array of Permission values` + // ); + // } const ourPermissions = await this.permissionService.loadedSnapshot(); @@ -53,10 +52,10 @@ export class PermissionGuard implements CanActivate, CanActivateChild { return isOk; } - private nestedPermissions(route: ActivatedRouteSnapshot): Permissions { + private nestedPermissions(route: ActivatedRouteSnapshot): string[] { const data: PRouteData = route.data; - let permissions: Permissions = []; + let permissions: string[] = []; if (data?.permissions) { permissions = permissions.concat(data.permissions); } diff --git a/frontend/src/app/models/forms/role.model.ts b/frontend/src/app/models/forms/role.model.ts index 29db6cd..e7b2290 100644 --- a/frontend/src/app/models/forms/role.model.ts +++ b/frontend/src/app/models/forms/role.model.ts @@ -1,6 +1,4 @@ -import { Permissions } from 'picsur-shared/dist/dto/permissions'; - export interface RoleModel { name: string; - permissions: Permissions; + permissions: string[]; } diff --git a/frontend/src/app/models/forms/updaterole.control.ts b/frontend/src/app/models/forms/updaterole.control.ts index ab46e69..4ad0ada 100644 --- a/frontend/src/app/models/forms/updaterole.control.ts +++ b/frontend/src/app/models/forms/updaterole.control.ts @@ -1,6 +1,5 @@ import { FormControl } from '@angular/forms'; import Fuse from 'fuse.js'; -import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; import { BehaviorSubject, Subscription } from 'rxjs'; import { RoleNameValidators } from './role-validators'; import { RoleModel } from './role.model'; @@ -8,10 +7,10 @@ import { CreateUsernameError } from './user-validators'; export class UpdateRoleControl { // Set once - private permissions: Permissions = []; + private permissions: string[] = []; // Variables - private selectablePermissionsSubject = new BehaviorSubject([]); + private selectablePermissionsSubject = new BehaviorSubject([]); private permissionsInputSubscription: null | Subscription; public rolename = new FormControl('', RoleNameValidators); @@ -19,7 +18,7 @@ export class UpdateRoleControl { public permissionControl = new FormControl('', []); public selectablePermissions = this.selectablePermissionsSubject.asObservable(); - public selectedPermissions: Permissions = []; + public selectedPermissions: string[] = []; public get rolenameValue() { return this.rolename.value; @@ -43,23 +42,23 @@ export class UpdateRoleControl { } } - public addPermission(role: Permission) { - if (!this.selectablePermissionsSubject.value.includes(role)) return; + public addPermission(permission: string) { + if (!this.selectablePermissionsSubject.value.includes(permission)) return; - this.selectedPermissions.push(role); + this.selectedPermissions.push(permission); this.clearInput(); } - public removePermission(role: Permission) { + public removePermission(permission: string) { this.selectedPermissions = this.selectedPermissions.filter( - (r) => r !== role + (r) => r !== permission ); this.updateSelectablePermissions(); } // Data interaction - public putAllPermissions(permissions: Permissions) { + public putAllPermissions(permissions: string[]) { this.permissions = permissions; this.updateSelectablePermissions(); } @@ -68,7 +67,7 @@ export class UpdateRoleControl { this.rolename.setValue(rolename); } - public putPermissions(permissions: Permissions) { + public putPermissions(permissions: string[]) { this.selectedPermissions = permissions; this.updateSelectablePermissions(); } diff --git a/frontend/src/app/models/forms/updateuser.control.ts b/frontend/src/app/models/forms/updateuser.control.ts index 1ca8508..3662ba4 100644 --- a/frontend/src/app/models/forms/updateuser.control.ts +++ b/frontend/src/app/models/forms/updateuser.control.ts @@ -1,6 +1,5 @@ import { FormControl } from '@angular/forms'; import Fuse from 'fuse.js'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { BehaviorSubject, Subscription } from 'rxjs'; import { FullUserModel } from './fulluser.model'; @@ -74,8 +73,8 @@ export class UpdateUserControl { return true; } - public getEffectivePermissions(): Permissions { - const permissions: Permissions = []; + public getEffectivePermissions(): string[] { + const permissions: string[] = []; for (const role of this.selectedRoles) { const fullRole = this.fullRoles.find((r) => r.name === role); if (!fullRole) { diff --git a/frontend/src/app/models/picsur-routes.ts b/frontend/src/app/models/picsur-routes.ts index e9f514e..0d038dc 100644 --- a/frontend/src/app/models/picsur-routes.ts +++ b/frontend/src/app/models/picsur-routes.ts @@ -1,6 +1,5 @@ import { ComponentType, Portal } from '@angular/cdk/portal'; import { Route } from '@angular/router'; -import { Permissions } from 'picsur-shared/dist/dto/permissions'; export type PRouteData = { page?: { @@ -8,7 +7,7 @@ export type PRouteData = { icon?: string; category?: string; }; - permissions?: Permissions; + permissions?: string[]; noContainer?: boolean; sidebar?: ComponentType; _sidebar_portal?: Portal; diff --git a/frontend/src/app/routes/settings/roles/settings-roles-edit/settings-roles-edit.component.ts b/frontend/src/app/routes/settings/roles/settings-roles-edit/settings-roles-edit.component.ts index 982281d..8189acb 100644 --- a/frontend/src/app/routes/settings/roles/settings-roles-edit/settings-roles-edit.component.ts +++ b/frontend/src/app/routes/settings/roles/settings-roles-edit/settings-roles-edit.component.ts @@ -3,14 +3,11 @@ import { Component, OnInit } from '@angular/core'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatChipInputEvent } from '@angular/material/chips'; import { ActivatedRoute, Router } from '@angular/router'; -import { - Permission, - PermissionsList -} from 'picsur-shared/dist/dto/permissions'; import { HasFailed } from 'picsur-shared/dist/types'; import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control'; import { SnackBarType } from 'src/app/models/snack-bar-type'; +import { PermissionService } from 'src/app/services/api/permission.service'; import { RolesService } from 'src/app/services/api/roles.service'; import { UtilService } from 'src/app/util/util.service'; @@ -42,7 +39,8 @@ export class SettingsRolesEditComponent implements OnInit { private route: ActivatedRoute, private router: Router, private utilService: UtilService, - private rolesService: RolesService + private rolesService: RolesService, + private permissionsService: PermissionService ) {} ngOnInit() { @@ -70,27 +68,36 @@ export class SettingsRolesEditComponent implements OnInit { } private async initPermissions() { - this.model.putAllPermissions(PermissionsList); + const allPermissions = await this.permissionsService.fetchAllPermission(); + if (HasFailed(allPermissions)) { + this.utilService.showSnackBar( + 'Failed to fetch permissions', + SnackBarType.Error + ); + return; + } + + this.model.putAllPermissions(allPermissions); } - removePermission(permission: Permission) { + removePermission(permission: string) { this.model.removePermission(permission); } addPermission(event: MatChipInputEvent) { const value = (event.value ?? '').trim(); - this.model.addPermission(value as Permission); + this.model.addPermission(value); } selectedPermission(event: MatAutocompleteSelectedEvent): void { - this.model.addPermission(event.option.viewValue as Permission); + this.model.addPermission(event.option.viewValue); } cancel() { this.router.navigate(['/settings/roles']); } - uiFriendlyPermission(permission: Permission) { + uiFriendlyPermission(permission: string) { return UIFriendlyPermissions[permission]; } diff --git a/frontend/src/app/routes/settings/roles/settings-roles.component.ts b/frontend/src/app/routes/settings/roles/settings-roles.component.ts index 614d85c..c7536b1 100644 --- a/frontend/src/app/routes/settings/roles/settings-roles.component.ts +++ b/frontend/src/app/routes/settings/roles/settings-roles.component.ts @@ -2,9 +2,6 @@ import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; -import { - Permission -} from 'picsur-shared/dist/dto/permissions'; import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { HasFailed } from 'picsur-shared/dist/types'; import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; @@ -88,7 +85,7 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit { await this.fetchRoles(); } - uiFriendlyPermission(permission: Permission) { + uiFriendlyPermission(permission: string) { return UIFriendlyPermissions[permission]; } diff --git a/frontend/src/app/routes/settings/settings.routing.module.ts b/frontend/src/app/routes/settings/settings.routing.module.ts index 01db174..2b5a3de 100644 --- a/frontend/src/app/routes/settings/settings.routing.module.ts +++ b/frontend/src/app/routes/settings/settings.routing.module.ts @@ -1,6 +1,6 @@ import { ModuleWithProviders, NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { PermissionGuard } from 'src/app/guards/permission.guard'; import { PRoutes } from 'src/app/models/picsur-routes'; import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sidebar-resolver.service'; diff --git a/frontend/src/app/routes/upload/upload.component.ts b/frontend/src/app/routes/upload/upload.component.ts index af0d287..bb8d179 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 { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { PermissionService } from 'src/app/services/api/permission.service'; import { UtilService } from 'src/app/util/util.service'; import { ProcessingViewMetadata } from '../../models/processing-view-metadata'; @@ -12,7 +12,7 @@ import { ProcessingViewMetadata } from '../../models/processing-view-metadata'; styleUrls: ['./upload.component.scss'], }) export class UploadComponent implements OnInit { - private permissions: Permissions = []; + private permissions: string[] = []; // Lets be optimistic here, this makes for a better ux public get hasUploadPermission() { diff --git a/frontend/src/app/routes/user/login/login.component.ts b/frontend/src/app/routes/user/login/login.component.ts index f92d7f9..2e47687 100644 --- a/frontend/src/app/routes/user/login/login.component.ts +++ b/frontend/src/app/routes/user/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 { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { SnackBarType } from 'src/app/models/snack-bar-type'; import { PermissionService } from 'src/app/services/api/permission.service'; @@ -17,7 +17,7 @@ import { UserPassModel } from '../../../models/forms/userpass.model'; export class LoginComponent implements OnInit { private readonly logger = console; - private permissions: Permissions = []; + private permissions: string[] = []; public get showRegister() { return this.permissions.includes(Permission.UserRegister); diff --git a/frontend/src/app/routes/user/register/register.component.ts b/frontend/src/app/routes/user/register/register.component.ts index da2fdac..4966791 100644 --- a/frontend/src/app/routes/user/register/register.component.ts +++ b/frontend/src/app/routes/user/register/register.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 { Permission, Permissions } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { HasFailed } from 'picsur-shared/dist/types'; import { UserPassModel } from 'src/app/models/forms/userpass.model'; import { SnackBarType } from 'src/app/models/snack-bar-type'; @@ -17,7 +17,7 @@ import { RegisterControl } from '../../../models/forms/register.control'; export class RegisterComponent implements OnInit { private readonly logger = console; - private permissions: Permissions = []; + private permissions: string[] = []; public get showLogin() { return this.permissions.includes(Permission.UserLogin); diff --git a/frontend/src/app/routes/user/user.routing.module.ts b/frontend/src/app/routes/user/user.routing.module.ts index f416a6c..87faae9 100644 --- a/frontend/src/app/routes/user/user.routing.module.ts +++ b/frontend/src/app/routes/user/user.routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { PermissionGuard } from 'src/app/guards/permission.guard'; import { PRoutes } from 'src/app/models/picsur-routes'; import { LoginComponent } from './login/login.component'; diff --git a/frontend/src/app/routes/view/view.routing.module.ts b/frontend/src/app/routes/view/view.routing.module.ts index 27d581a..088dd76 100644 --- a/frontend/src/app/routes/view/view.routing.module.ts +++ b/frontend/src/app/routes/view/view.routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { PermissionGuard } from 'src/app/guards/permission.guard'; import { PRoutes } from 'src/app/models/picsur-routes'; import { ViewComponent } from './view.component'; diff --git a/frontend/src/app/services/api/permission.service.ts b/frontend/src/app/services/api/permission.service.ts index 42f6d97..f083830 100644 --- a/frontend/src/app/services/api/permission.service.ts +++ b/frontend/src/app/services/api/permission.service.ts @@ -1,10 +1,7 @@ import { Injectable } from '@angular/core'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; +import { AllPermissionsResponse } from 'picsur-shared/dist/dto/api/roles.dto'; import { UserMePermissionsResponse } from 'picsur-shared/dist/dto/api/user.dto'; -import { - Permissions, - PermissionsList -} from 'picsur-shared/dist/dto/permissions'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; import { BehaviorSubject, filter, map, Observable, take } from 'rxjs'; import { ApiService } from './api.service'; @@ -18,29 +15,29 @@ export class PermissionService { this.onUser(); } - public get live(): Observable { + // TODO: add full permission list as default + public get live(): Observable { return this.permissionsSubject.pipe( - map((permissions) => permissions ?? this.defaultPermissions) + map((permissions) => permissions ?? []) ); } - public get snapshot(): Permissions { - return this.permissionsSubject.getValue() ?? this.defaultPermissions; + public get snapshot(): string[] { + return this.permissionsSubject.getValue() ?? []; } // This will not be optimistic, it will instead wait for correct data - public loadedSnapshot(): Promise { + public loadedSnapshot(): Promise { return new Promise((resolve) => { const filtered = this.permissionsSubject.pipe( filter((permissions) => permissions !== null), take(1) ); - (filtered as Observable).subscribe(resolve); + (filtered as Observable).subscribe(resolve); }); } - private defaultPermissions = PermissionsList as Permissions; - private permissionsSubject = new BehaviorSubject(null); + private permissionsSubject = new BehaviorSubject(null); @AutoUnsubscribe() private onUser() { @@ -54,7 +51,7 @@ export class PermissionService { }); } - private async fetchPermissions(): AsyncFailable { + private async fetchPermissions(): AsyncFailable { const got = await this.api.get( UserMePermissionsResponse, '/api/user/me/permissions' @@ -63,4 +60,15 @@ export class PermissionService { return got.permissions; } + + public async fetchAllPermission(): AsyncFailable { + const result = await this.api.get( + AllPermissionsResponse, + '/api/info/permissions' + ); + + if (HasFailed(result)) return result; + + return result.Permissions; + } } diff --git a/frontend/src/app/services/api/syspref.service.ts b/frontend/src/app/services/api/syspref.service.ts index f8bcb8f..564e5c8 100644 --- a/frontend/src/app/services/api/syspref.service.ts +++ b/frontend/src/app/services/api/syspref.service.ts @@ -7,7 +7,7 @@ import { UpdateSysPreferenceRequest, UpdateSysPreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; -import { Permission } from 'picsur-shared/dist/dto/permissions'; +import { Permission } from 'picsur-shared/dist/dto/permissions.dto'; import { SysPreferences, SysPrefValueType @@ -31,7 +31,9 @@ export class SysprefService { return this.sysprefObservable; } - private sysprefObservable = new BehaviorSubject([]); + private sysprefObservable = new BehaviorSubject( + [] + ); constructor( private api: ApiService, diff --git a/shared/src/dto/api/roles.dto.ts b/shared/src/dto/api/roles.dto.ts index 97ed9bd..934260d 100644 --- a/shared/src/dto/api/roles.dto.ts +++ b/shared/src/dto/api/roles.dto.ts @@ -54,3 +54,10 @@ export class SpecialRolesResponse { @IsStringList() DefaultRoles: string[]; } + +// AllPermissions +export class AllPermissionsResponse { + @IsDefined() + @IsStringList() + Permissions: string[]; +} diff --git a/shared/src/dto/api/user.dto.ts b/shared/src/dto/api/user.dto.ts index ae82bf8..009f3e1 100644 --- a/shared/src/dto/api/user.dto.ts +++ b/shared/src/dto/api/user.dto.ts @@ -1,11 +1,11 @@ import { Type } from 'class-transformer'; import { - IsArray, IsDefined, - IsEnum, IsJWT, IsString, + IsDefined, IsJWT, + IsString, ValidateNested } from 'class-validator'; import { EUser, NamePassUser } from '../../entities/user.entity'; -import { Permissions, PermissionsList } from '../permissions'; +import { IsStringList } from '../../validators/string-list.validator'; // Api @@ -40,7 +40,6 @@ export class UserMeResponse { // UserMePermissions export class UserMePermissionsResponse { @IsDefined() - @IsArray() - @IsEnum(PermissionsList, { each: true }) - permissions: Permissions; + @IsStringList() + permissions: string[]; } diff --git a/shared/src/dto/permissions.ts b/shared/src/dto/permissions.dto.ts similarity index 62% rename from shared/src/dto/permissions.ts rename to shared/src/dto/permissions.dto.ts index 8ddc1c9..b4b7b8f 100644 --- a/shared/src/dto/permissions.ts +++ b/shared/src/dto/permissions.dto.ts @@ -1,5 +1,4 @@ -// Config - +// Only add no rename export enum Permission { ImageView = 'image-view', ImageUpload = 'image-upload', @@ -14,16 +13,3 @@ export enum Permission { RoleManage = 'role-manage', // Allow modification of roles SysPrefManage = 'syspref-manage', } - -// Derivatives - -export const PermissionsList: Permission[] = Object.values(Permission); - -export type Permissions = Permission[]; - -// Compound permission lists -export const AdminDashboardPermissions: Permissions = [ - Permission.UserManage, - Permission.RoleManage, - Permission.SysPrefManage, -]; diff --git a/shared/src/entities/role.entity.ts b/shared/src/entities/role.entity.ts index ad8fbd0..97f757c 100644 --- a/shared/src/entities/role.entity.ts +++ b/shared/src/entities/role.entity.ts @@ -1,7 +1,7 @@ -import { IsArray, IsEnum } from 'class-validator'; -import { Permissions, PermissionsList } from '../dto/permissions'; +import { IsDefined } from 'class-validator'; import { EntityID } from '../validators/entity-id.validator'; import { IsRoleName } from '../validators/role.validators'; +import { IsStringList } from '../validators/string-list.validator'; export class RoleNameObject { @IsRoleName() @@ -9,9 +9,9 @@ export class RoleNameObject { } export class RoleNamePermsObject extends RoleNameObject { - @IsArray() - @IsEnum(PermissionsList, { each: true }) - permissions: Permissions; + @IsDefined() + @IsStringList() + permissions: string[]; } export class ERole extends RoleNamePermsObject { diff --git a/shared/src/util/permissions.ts b/shared/src/util/permissions.ts index 9b27fb3..d1c3e6e 100644 --- a/shared/src/util/permissions.ts +++ b/shared/src/util/permissions.ts @@ -1,19 +1,8 @@ -import { isArray, isEnum, isString } from 'class-validator'; -import { Permission, 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; -} - export function HasAPermissionOf( - compoundPermission: Permissions, - yourPermissions: Permissions, + compoundPermission: string[], + yourPermissions: string[], ): boolean { - return compoundPermission.some((permission: Permission) => + return compoundPermission.some((permission: string) => yourPermissions.includes(permission), ); }