migrate permission logic to more backend

This commit is contained in:
rubikscraft 2022-03-24 23:08:37 +01:00
parent af82a749bc
commit 2c150c3027
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
36 changed files with 166 additions and 129 deletions

View file

@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { import {
AsyncFailable, AsyncFailable,
Fail, Fail,
@ -10,6 +9,7 @@ import {
} from 'picsur-shared/dist/types'; } from 'picsur-shared/dist/types';
import { strictValidate } from 'picsur-shared/dist/util/validate'; import { strictValidate } from 'picsur-shared/dist/util/validate';
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Permissions } from '../../models/dto/permissions.dto';
import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto'; import { ImmutableRolesList, UndeletableRolesList } from '../../models/dto/roles.dto';
import { ERoleBackend } from '../../models/entities/role.entity'; import { ERoleBackend } from '../../models/entities/role.entity';

View file

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { Permissions } from '../../models/dto/permissions.dto';
import { EUserBackend } from '../../models/entities/user.entity'; import { EUserBackend } from '../../models/entities/user.entity';
import { RolesService } from '../roledb/roledb.service'; import { RolesService } from '../roledb/roledb.service';
import { UsersService } from './userdb.service'; import { UsersService } from './userdb.service';

View file

@ -1,7 +1,7 @@
import { SetMetadata, UseGuards } from '@nestjs/common'; import { SetMetadata, UseGuards } from '@nestjs/common';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { CombineDecorators } from 'picsur-shared/dist/util/decorator'; import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard'; import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
import { Permissions } from '../models/dto/permissions.dto';
export const RequiredPermissions = (...permissions: Permissions) => { export const RequiredPermissions = (...permissions: Permissions) => {
return SetMetadata('permissions', permissions); return SetMetadata('permissions', permissions);

View file

@ -8,15 +8,13 @@ import {
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import {
Permissions
} from 'picsur-shared/dist/dto/permissions';
import { Fail, Failable, HasFailed } from 'picsur-shared/dist/types'; 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 { strictValidate } from 'picsur-shared/dist/util/validate';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service'; import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
import { Permissions } from '../../../models/dto/permissions.dto';
import { EUserBackend } from '../../../models/entities/user.entity'; import { EUserBackend } from '../../../models/entities/user.entity';
import { isPermissionsArray } from '../../../models/util/permissions';
@Injectable() @Injectable()
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) { export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {

View file

@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { ImageDBService } from '../../collections/imagedb/imagedb.service'; import { ImageDBService } from '../../collections/imagedb/imagedb.service';
import { RolesService } from '../../collections/roledb/roledb.service'; import { RolesService } from '../../collections/roledb/roledb.service';
import { Permission } from '../../models/dto/permissions.dto';
@Injectable() @Injectable()
export class DemoManagerService { export class DemoManagerService {

View file

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

View file

@ -1,5 +1,5 @@
import { Permission, Permissions, PermissionsList } from 'picsur-shared/dist/dto/permissions';
import tuple from 'picsur-shared/dist/types/tuple'; import tuple from 'picsur-shared/dist/types/tuple';
import { Permission, Permissions, PermissionsList } from './permissions.dto';
// Config // Config
@ -8,7 +8,10 @@ const SoulBoundRolesTuple = tuple('guest', 'user');
// These roles can never be modified // These roles can never be modified
const ImmutableRolesTuple = tuple('admin'); const ImmutableRolesTuple = tuple('admin');
// These roles can never be removed from the server // 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 // These roles will be applied by default to new users
export const DefaultRolesList: string[] = ['user']; export const DefaultRolesList: string[] = ['user'];

View file

@ -1,6 +1,6 @@
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { Permissions } from '../dto/permissions.dto';
@Entity() @Entity()
export class ERoleBackend extends ERole { export class ERoleBackend extends ERole {

View file

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

View file

@ -1,14 +1,17 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; 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 { HostConfigService } from '../../../config/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator'; import { NoPermissions } from '../../../decorators/permissions.decorator';
import { PermissionsList } from '../../../models/dto/permissions.dto';
@Controller('api/info') @Controller('api/info')
@NoPermissions()
export class InfoController { export class InfoController {
constructor(private hostConfig: HostConfigService) {} constructor(private hostConfig: HostConfigService) {}
@Get() @Get()
@NoPermissions()
async getInfo(): Promise<InfoResponse> { async getInfo(): Promise<InfoResponse> {
return { return {
demo: this.hostConfig.isDemo(), demo: this.hostConfig.isDemo(),
@ -16,4 +19,14 @@ export class InfoController {
version: this.hostConfig.getVersion(), version: this.hostConfig.getVersion(),
}; };
} }
// List all available permissions
@Get('/permissions')
async getPermissions(): Promise<AllPermissionsResponse> {
const result: AllPermissionsResponse = {
Permissions: PermissionsList,
};
return plainToClass(AllPermissionsResponse, result);
}
} }

View file

@ -15,11 +15,11 @@ import {
UpdateSysPreferenceRequest, UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/pref.dto'; } 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 { SysPreferences } 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 { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Permission } from '../../../models/dto/permissions.dto';
@Controller('api/pref') @Controller('api/pref')
@RequiredPermissions(Permission.SysPrefManage) @RequiredPermissions(Permission.SysPrefManage)

View file

@ -19,16 +19,19 @@ import {
RoleUpdateResponse, RoleUpdateResponse,
SpecialRolesResponse SpecialRolesResponse
} from 'picsur-shared/dist/dto/api/roles.dto'; } from 'picsur-shared/dist/dto/api/roles.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { RolesService } from '../../../collections/roledb/roledb.service'; import { RolesService } from '../../../collections/roledb/roledb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import {
Permission
} from '../../../models/dto/permissions.dto';
import { import {
DefaultRolesList, DefaultRolesList,
ImmutableRolesList, ImmutableRolesList,
SoulBoundRolesList, SoulBoundRolesList,
UndeletableRolesList UndeletableRolesList
} from '../../../models/dto/roles.dto'; } from '../../../models/dto/roles.dto';
import { isPermissionsArray } from '../../../models/util/permissions';
@Controller('api/roles') @Controller('api/roles')
@RequiredPermissions(Permission.RoleManage) @RequiredPermissions(Permission.RoleManage)
@ -66,9 +69,14 @@ export class RolesController {
async updateRole( async updateRole(
@Body() body: RoleUpdateRequest, @Body() body: RoleUpdateRequest,
): Promise<RoleUpdateResponse> { ): Promise<RoleUpdateResponse> {
const permissions = body.permissions;
if (!isPermissionsArray(permissions)) {
throw new InternalServerErrorException('Invalid permissions array');
}
const updatedRole = await this.rolesService.setPermissions( const updatedRole = await this.rolesService.setPermissions(
body.name, body.name,
body.permissions, permissions,
); );
if (HasFailed(updatedRole)) { if (HasFailed(updatedRole)) {
this.logger.warn(updatedRole.getReason()); this.logger.warn(updatedRole.getReason());
@ -82,7 +90,12 @@ export class RolesController {
async createRole( async createRole(
@Body() role: RoleCreateRequest, @Body() role: RoleCreateRequest,
): Promise<RoleCreateResponse> { ): Promise<RoleCreateResponse> {
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)) { if (HasFailed(newRole)) {
this.logger.warn(newRole.getReason()); this.logger.warn(newRole.getReason());
throw new InternalServerErrorException('Could not create role'); throw new InternalServerErrorException('Could not create role');
@ -104,7 +117,7 @@ export class RolesController {
return deletedRole; return deletedRole;
} }
@Get('special') @Get('/special')
async getSpecialRoles(): Promise<SpecialRolesResponse> { async getSpecialRoles(): Promise<SpecialRolesResponse> {
const result: SpecialRolesResponse = { const result: SpecialRolesResponse = {
SoulBoundRoles: SoulBoundRolesList, SoulBoundRoles: SoulBoundRolesList,
@ -115,4 +128,5 @@ export class RolesController {
return plainToClass(SpecialRolesResponse, result); return plainToClass(SpecialRolesResponse, result);
} }
} }

View file

@ -14,7 +14,6 @@ import {
UserRegisterRequest, UserRegisterRequest,
UserRegisterResponse UserRegisterResponse
} from 'picsur-shared/dist/dto/api/user.dto'; } from 'picsur-shared/dist/dto/api/user.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { UserRolesService } from '../../../collections/userdb/userrolesdb.service'; import { UserRolesService } from '../../../collections/userdb/userrolesdb.service';
@ -24,6 +23,7 @@ import {
UseLocalAuth UseLocalAuth
} from '../../../decorators/permissions.decorator'; } from '../../../decorators/permissions.decorator';
import { AuthManagerService } from '../../../managers/auth/auth.service'; import { AuthManagerService } from '../../../managers/auth/auth.service';
import { Permission } from '../../../models/dto/permissions.dto';
import AuthFasityRequest from '../../../models/requests/authrequest.dto'; import AuthFasityRequest from '../../../models/requests/authrequest.dto';
@Controller('api/user') @Controller('api/user')

View file

@ -20,10 +20,10 @@ import {
UserUpdateRequest, UserUpdateRequest,
UserUpdateResponse UserUpdateResponse
} from 'picsur-shared/dist/dto/api/usermanage.dto'; } from 'picsur-shared/dist/dto/api/usermanage.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service'; import { UsersService } from '../../../collections/userdb/userdb.service';
import { RequiredPermissions } from '../../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../../decorators/permissions.decorator';
import { Permission } from '../../../models/dto/permissions.dto';
import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto'; import { ImmutableUsersList, LockedLoginUsersList, UndeletableUsersList } from '../../../models/dto/specialusers.dto';
@Controller('api/user') @Controller('api/user')

View file

@ -13,11 +13,11 @@ import {
import { isHash } from 'class-validator'; import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify'; import { FastifyReply, FastifyRequest } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; 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 { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator'; import { MultiPart } from '../../decorators/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator'; import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service'; import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { Permission } from '../../models/dto/permissions.dto';
import { ImageUploadDto } from '../../models/requests/imageroute.dto'; import { ImageUploadDto } from '../../models/requests/imageroute.dto';
@Controller('i') @Controller('i')

View file

@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; 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 { EUser } from 'picsur-shared/dist/entities/user.entity';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { SnackBarType } from 'src/app/models/snack-bar-type'; import { SnackBarType } from 'src/app/models/snack-bar-type';
@ -21,7 +21,7 @@ export class HeaderComponent implements OnInit {
@Output('onHamburgerClick') onHamburgerClick = new EventEmitter<void>(); @Output('onHamburgerClick') onHamburgerClick = new EventEmitter<void>();
private currentUser: EUser | null = null; private currentUser: EUser | null = null;
private permissions: Permissions = []; private permissions: string[] = [];
public get user() { public get user() {
return this.currentUser; return this.currentUser;

View file

@ -6,8 +6,6 @@ import {
Router, Router,
RouterStateSnapshot RouterStateSnapshot
} from '@angular/router'; } 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 { PRouteData } from '../models/picsur-routes';
import { PermissionService } from '../services/api/permission.service'; import { PermissionService } from '../services/api/permission.service';
@ -34,12 +32,13 @@ export class PermissionGuard implements CanActivate, CanActivateChild {
} }
private async can(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { private async can(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const requiredPermissions: Permissions = this.nestedPermissions(route); const requiredPermissions: string[] = this.nestedPermissions(route);
if (!isPermissionsArray(requiredPermissions)) { // TODO: revive
throw new Error( // if (!isPermissionsArray(requiredPermissions)) {
`PermissionGuard: route data 'permissions' must be an array of Permission values` // throw new Error(
); // `PermissionGuard: route data 'permissions' must be an array of Permission values`
} // );
// }
const ourPermissions = await this.permissionService.loadedSnapshot(); const ourPermissions = await this.permissionService.loadedSnapshot();
@ -53,10 +52,10 @@ export class PermissionGuard implements CanActivate, CanActivateChild {
return isOk; return isOk;
} }
private nestedPermissions(route: ActivatedRouteSnapshot): Permissions { private nestedPermissions(route: ActivatedRouteSnapshot): string[] {
const data: PRouteData = route.data; const data: PRouteData = route.data;
let permissions: Permissions = []; let permissions: string[] = [];
if (data?.permissions) { if (data?.permissions) {
permissions = permissions.concat(data.permissions); permissions = permissions.concat(data.permissions);
} }

View file

@ -1,6 +1,4 @@
import { Permissions } from 'picsur-shared/dist/dto/permissions';
export interface RoleModel { export interface RoleModel {
name: string; name: string;
permissions: Permissions; permissions: string[];
} }

View file

@ -1,6 +1,5 @@
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { Permission, Permissions } from 'picsur-shared/dist/dto/permissions';
import { BehaviorSubject, Subscription } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { RoleNameValidators } from './role-validators'; import { RoleNameValidators } from './role-validators';
import { RoleModel } from './role.model'; import { RoleModel } from './role.model';
@ -8,10 +7,10 @@ import { CreateUsernameError } from './user-validators';
export class UpdateRoleControl { export class UpdateRoleControl {
// Set once // Set once
private permissions: Permissions = []; private permissions: string[] = [];
// Variables // Variables
private selectablePermissionsSubject = new BehaviorSubject<Permissions>([]); private selectablePermissionsSubject = new BehaviorSubject<string[]>([]);
private permissionsInputSubscription: null | Subscription; private permissionsInputSubscription: null | Subscription;
public rolename = new FormControl('', RoleNameValidators); public rolename = new FormControl('', RoleNameValidators);
@ -19,7 +18,7 @@ export class UpdateRoleControl {
public permissionControl = new FormControl('', []); public permissionControl = new FormControl('', []);
public selectablePermissions = public selectablePermissions =
this.selectablePermissionsSubject.asObservable(); this.selectablePermissionsSubject.asObservable();
public selectedPermissions: Permissions = []; public selectedPermissions: string[] = [];
public get rolenameValue() { public get rolenameValue() {
return this.rolename.value; return this.rolename.value;
@ -43,23 +42,23 @@ export class UpdateRoleControl {
} }
} }
public addPermission(role: Permission) { public addPermission(permission: string) {
if (!this.selectablePermissionsSubject.value.includes(role)) return; if (!this.selectablePermissionsSubject.value.includes(permission)) return;
this.selectedPermissions.push(role); this.selectedPermissions.push(permission);
this.clearInput(); this.clearInput();
} }
public removePermission(role: Permission) { public removePermission(permission: string) {
this.selectedPermissions = this.selectedPermissions.filter( this.selectedPermissions = this.selectedPermissions.filter(
(r) => r !== role (r) => r !== permission
); );
this.updateSelectablePermissions(); this.updateSelectablePermissions();
} }
// Data interaction // Data interaction
public putAllPermissions(permissions: Permissions) { public putAllPermissions(permissions: string[]) {
this.permissions = permissions; this.permissions = permissions;
this.updateSelectablePermissions(); this.updateSelectablePermissions();
} }
@ -68,7 +67,7 @@ export class UpdateRoleControl {
this.rolename.setValue(rolename); this.rolename.setValue(rolename);
} }
public putPermissions(permissions: Permissions) { public putPermissions(permissions: string[]) {
this.selectedPermissions = permissions; this.selectedPermissions = permissions;
this.updateSelectablePermissions(); this.updateSelectablePermissions();
} }

View file

@ -1,6 +1,5 @@
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { BehaviorSubject, Subscription } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { FullUserModel } from './fulluser.model'; import { FullUserModel } from './fulluser.model';
@ -74,8 +73,8 @@ export class UpdateUserControl {
return true; return true;
} }
public getEffectivePermissions(): Permissions { public getEffectivePermissions(): string[] {
const permissions: Permissions = []; const permissions: string[] = [];
for (const role of this.selectedRoles) { for (const role of this.selectedRoles) {
const fullRole = this.fullRoles.find((r) => r.name === role); const fullRole = this.fullRoles.find((r) => r.name === role);
if (!fullRole) { if (!fullRole) {

View file

@ -1,6 +1,5 @@
import { ComponentType, Portal } from '@angular/cdk/portal'; import { ComponentType, Portal } from '@angular/cdk/portal';
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
export type PRouteData = { export type PRouteData = {
page?: { page?: {
@ -8,7 +7,7 @@ export type PRouteData = {
icon?: string; icon?: string;
category?: string; category?: string;
}; };
permissions?: Permissions; permissions?: string[];
noContainer?: boolean; noContainer?: boolean;
sidebar?: ComponentType<unknown>; sidebar?: ComponentType<unknown>;
_sidebar_portal?: Portal<unknown>; _sidebar_portal?: Portal<unknown>;

View file

@ -3,14 +3,11 @@ import { Component, OnInit } from '@angular/core';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import {
Permission,
PermissionsList
} from 'picsur-shared/dist/dto/permissions';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n';
import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control'; import { UpdateRoleControl } from 'src/app/models/forms/updaterole.control';
import { SnackBarType } from 'src/app/models/snack-bar-type'; 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 { RolesService } from 'src/app/services/api/roles.service';
import { UtilService } from 'src/app/util/util.service'; import { UtilService } from 'src/app/util/util.service';
@ -42,7 +39,8 @@ export class SettingsRolesEditComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private utilService: UtilService, private utilService: UtilService,
private rolesService: RolesService private rolesService: RolesService,
private permissionsService: PermissionService
) {} ) {}
ngOnInit() { ngOnInit() {
@ -70,27 +68,36 @@ export class SettingsRolesEditComponent implements OnInit {
} }
private async initPermissions() { 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;
} }
removePermission(permission: Permission) { this.model.putAllPermissions(allPermissions);
}
removePermission(permission: string) {
this.model.removePermission(permission); this.model.removePermission(permission);
} }
addPermission(event: MatChipInputEvent) { addPermission(event: MatChipInputEvent) {
const value = (event.value ?? '').trim(); const value = (event.value ?? '').trim();
this.model.addPermission(value as Permission); this.model.addPermission(value);
} }
selectedPermission(event: MatAutocompleteSelectedEvent): void { selectedPermission(event: MatAutocompleteSelectedEvent): void {
this.model.addPermission(event.option.viewValue as Permission); this.model.addPermission(event.option.viewValue);
} }
cancel() { cancel() {
this.router.navigate(['/settings/roles']); this.router.navigate(['/settings/roles']);
} }
uiFriendlyPermission(permission: Permission) { uiFriendlyPermission(permission: string) {
return UIFriendlyPermissions[permission]; return UIFriendlyPermissions[permission];
} }

View file

@ -2,9 +2,6 @@ import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import {
Permission
} from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n'; import { UIFriendlyPermissions } from 'src/app/i18n/permissions.i18n';
@ -88,7 +85,7 @@ export class SettingsRolesComponent implements OnInit, AfterViewInit {
await this.fetchRoles(); await this.fetchRoles();
} }
uiFriendlyPermission(permission: Permission) { uiFriendlyPermission(permission: string) {
return UIFriendlyPermissions[permission]; return UIFriendlyPermissions[permission];
} }

View file

@ -1,6 +1,6 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; 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 { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes'; import { PRoutes } from 'src/app/models/picsur-routes';
import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sidebar-resolver.service'; import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sidebar-resolver.service';

View file

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator';
import { NgxDropzoneChangeEvent } from 'ngx-dropzone'; 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 { PermissionService } from 'src/app/services/api/permission.service';
import { UtilService } from 'src/app/util/util.service'; import { UtilService } from 'src/app/util/util.service';
import { ProcessingViewMetadata } from '../../models/processing-view-metadata'; import { ProcessingViewMetadata } from '../../models/processing-view-metadata';
@ -12,7 +12,7 @@ import { ProcessingViewMetadata } from '../../models/processing-view-metadata';
styleUrls: ['./upload.component.scss'], styleUrls: ['./upload.component.scss'],
}) })
export class UploadComponent implements OnInit { export class UploadComponent implements OnInit {
private permissions: Permissions = []; private permissions: string[] = [];
// Lets be optimistic here, this makes for a better ux // Lets be optimistic here, this makes for a better ux
public get hasUploadPermission() { public get hasUploadPermission() {

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; 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 { HasFailed } from 'picsur-shared/dist/types';
import { SnackBarType } from 'src/app/models/snack-bar-type'; import { SnackBarType } from 'src/app/models/snack-bar-type';
import { PermissionService } from 'src/app/services/api/permission.service'; 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 { export class LoginComponent implements OnInit {
private readonly logger = console; private readonly logger = console;
private permissions: Permissions = []; private permissions: string[] = [];
public get showRegister() { public get showRegister() {
return this.permissions.includes(Permission.UserRegister); return this.permissions.includes(Permission.UserRegister);

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; 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 { HasFailed } from 'picsur-shared/dist/types';
import { UserPassModel } from 'src/app/models/forms/userpass.model'; import { UserPassModel } from 'src/app/models/forms/userpass.model';
import { SnackBarType } from 'src/app/models/snack-bar-type'; 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 { export class RegisterComponent implements OnInit {
private readonly logger = console; private readonly logger = console;
private permissions: Permissions = []; private permissions: string[] = [];
public get showLogin() { public get showLogin() {
return this.permissions.includes(Permission.UserLogin); return this.permissions.includes(Permission.UserLogin);

View file

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; 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 { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes'; import { PRoutes } from 'src/app/models/picsur-routes';
import { LoginComponent } from './login/login.component'; import { LoginComponent } from './login/login.component';

View file

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; 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 { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/picsur-routes'; import { PRoutes } from 'src/app/models/picsur-routes';
import { ViewComponent } from './view.component'; import { ViewComponent } from './view.component';

View file

@ -1,10 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; 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 { 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 { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { BehaviorSubject, filter, map, Observable, take } from 'rxjs'; import { BehaviorSubject, filter, map, Observable, take } from 'rxjs';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
@ -18,29 +15,29 @@ export class PermissionService {
this.onUser(); this.onUser();
} }
public get live(): Observable<Permissions> { // TODO: add full permission list as default
public get live(): Observable<string[]> {
return this.permissionsSubject.pipe( return this.permissionsSubject.pipe(
map((permissions) => permissions ?? this.defaultPermissions) map((permissions) => permissions ?? [])
); );
} }
public get snapshot(): Permissions { public get snapshot(): string[] {
return this.permissionsSubject.getValue() ?? this.defaultPermissions; return this.permissionsSubject.getValue() ?? [];
} }
// This will not be optimistic, it will instead wait for correct data // This will not be optimistic, it will instead wait for correct data
public loadedSnapshot(): Promise<Permissions> { public loadedSnapshot(): Promise<string[]> {
return new Promise((resolve) => { return new Promise((resolve) => {
const filtered = this.permissionsSubject.pipe( const filtered = this.permissionsSubject.pipe(
filter((permissions) => permissions !== null), filter((permissions) => permissions !== null),
take(1) take(1)
); );
(filtered as Observable<Permissions>).subscribe(resolve); (filtered as Observable<string[]>).subscribe(resolve);
}); });
} }
private defaultPermissions = PermissionsList as Permissions; private permissionsSubject = new BehaviorSubject<string[] | null>(null);
private permissionsSubject = new BehaviorSubject<Permissions | null>(null);
@AutoUnsubscribe() @AutoUnsubscribe()
private onUser() { private onUser() {
@ -54,7 +51,7 @@ export class PermissionService {
}); });
} }
private async fetchPermissions(): AsyncFailable<Permissions> { private async fetchPermissions(): AsyncFailable<string[]> {
const got = await this.api.get( const got = await this.api.get(
UserMePermissionsResponse, UserMePermissionsResponse,
'/api/user/me/permissions' '/api/user/me/permissions'
@ -63,4 +60,15 @@ export class PermissionService {
return got.permissions; return got.permissions;
} }
public async fetchAllPermission(): AsyncFailable<string[]> {
const result = await this.api.get(
AllPermissionsResponse,
'/api/info/permissions'
);
if (HasFailed(result)) return result;
return result.Permissions;
}
} }

View file

@ -7,7 +7,7 @@ import {
UpdateSysPreferenceRequest, UpdateSysPreferenceRequest,
UpdateSysPreferenceResponse UpdateSysPreferenceResponse
} from 'picsur-shared/dist/dto/api/pref.dto'; } 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 { import {
SysPreferences, SysPreferences,
SysPrefValueType SysPrefValueType
@ -31,7 +31,9 @@ export class SysprefService {
return this.sysprefObservable; return this.sysprefObservable;
} }
private sysprefObservable = new BehaviorSubject<SysPreferenceBaseResponse[]>([]); private sysprefObservable = new BehaviorSubject<SysPreferenceBaseResponse[]>(
[]
);
constructor( constructor(
private api: ApiService, private api: ApiService,

View file

@ -54,3 +54,10 @@ export class SpecialRolesResponse {
@IsStringList() @IsStringList()
DefaultRoles: string[]; DefaultRoles: string[];
} }
// AllPermissions
export class AllPermissionsResponse {
@IsDefined()
@IsStringList()
Permissions: string[];
}

View file

@ -1,11 +1,11 @@
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsArray, IsDefined, IsDefined, IsJWT,
IsEnum, IsJWT, IsString, IsString,
ValidateNested ValidateNested
} from 'class-validator'; } from 'class-validator';
import { EUser, NamePassUser } from '../../entities/user.entity'; import { EUser, NamePassUser } from '../../entities/user.entity';
import { Permissions, PermissionsList } from '../permissions'; import { IsStringList } from '../../validators/string-list.validator';
// Api // Api
@ -40,7 +40,6 @@ export class UserMeResponse {
// UserMePermissions // UserMePermissions
export class UserMePermissionsResponse { export class UserMePermissionsResponse {
@IsDefined() @IsDefined()
@IsArray() @IsStringList()
@IsEnum(PermissionsList, { each: true }) permissions: string[];
permissions: Permissions;
} }

View file

@ -1,5 +1,4 @@
// Config // Only add no rename
export enum Permission { export enum Permission {
ImageView = 'image-view', ImageView = 'image-view',
ImageUpload = 'image-upload', ImageUpload = 'image-upload',
@ -14,16 +13,3 @@ export enum Permission {
RoleManage = 'role-manage', // Allow modification of roles RoleManage = 'role-manage', // Allow modification of roles
SysPrefManage = 'syspref-manage', 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,
];

View file

@ -1,7 +1,7 @@
import { IsArray, IsEnum } from 'class-validator'; import { IsDefined } from 'class-validator';
import { Permissions, PermissionsList } from '../dto/permissions';
import { EntityID } from '../validators/entity-id.validator'; import { EntityID } from '../validators/entity-id.validator';
import { IsRoleName } from '../validators/role.validators'; import { IsRoleName } from '../validators/role.validators';
import { IsStringList } from '../validators/string-list.validator';
export class RoleNameObject { export class RoleNameObject {
@IsRoleName() @IsRoleName()
@ -9,9 +9,9 @@ export class RoleNameObject {
} }
export class RoleNamePermsObject extends RoleNameObject { export class RoleNamePermsObject extends RoleNameObject {
@IsArray() @IsDefined()
@IsEnum(PermissionsList, { each: true }) @IsStringList()
permissions: Permissions; permissions: string[];
} }
export class ERole extends RoleNamePermsObject { export class ERole extends RoleNamePermsObject {

View file

@ -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( export function HasAPermissionOf(
compoundPermission: Permissions, compoundPermission: string[],
yourPermissions: Permissions, yourPermissions: string[],
): boolean { ): boolean {
return compoundPermission.some((permission: Permission) => return compoundPermission.some((permission: string) =>
yourPermissions.includes(permission), yourPermissions.includes(permission),
); );
} }