add permissions to roles

This commit is contained in:
rubikscraft 2022-03-12 15:10:22 +01:00
parent 0aa897fa8d
commit b29b88d7b6
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
27 changed files with 601 additions and 230 deletions

View file

@ -8,9 +8,9 @@ import { PicsurLoggerModule } from './logger/logger.module';
import { AuthManagerModule } from './managers/auth/auth.module';
import { DemoManagerModule } from './managers/demo/demomanager.module';
import { AuthModule } from './routes/api/auth/auth.module';
import { ExperimentModule } from './routes/api/experiment/experiment.module';
import { PrefModule } from './routes/api/pref/pref.module';
import { ImageModule } from './routes/image/imageroute.module';
import { ExperimentModule } from './routes/api/experiment/experiment.module';
@Module({
imports: [
@ -22,6 +22,7 @@ import { ExperimentModule } from './routes/api/experiment/experiment.module';
useExisting: ServeStaticConfigService,
imports: [PicsurConfigModule],
}),
AuthManagerModule,
AuthModule,
ImageModule,

View file

@ -0,0 +1,84 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
ImmuteableRolesList,
SystemRoleDefaults,
SystemRoles,
SystemRolesList
} from 'picsur-shared/dist/dto/roles.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { PicsurConfigModule } from '../../config/config.module';
import { HostConfigService } from '../../config/host.config.service';
import { ERoleBackend } from '../../models/entities/role.entity';
import { RolesService } from './roledb.service';
@Module({
imports: [PicsurConfigModule, TypeOrmModule.forFeature([ERoleBackend])],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule implements OnModuleInit {
private readonly logger = new Logger('RolesModule');
constructor(
private rolesService: RolesService,
private hostConfig: HostConfigService,
) {}
async onModuleInit() {
if (!this.hostConfig.isProduction()) {
await this.nukeRoles();
}
await this.ensureSystemRolesExist();
await this.updateImmutableRoles();
}
private async nukeRoles() {
this.logger.error('Nuking all roles');
const result = this.rolesService.nuke(true);
if (HasFailed(result)) {
this.logger.error(`Failed to nuke roles because: ${result.getReason()}`);
}
}
private async ensureSystemRolesExist() {
for (const systemRole of SystemRolesList as SystemRoles) {
this.logger.debug(`Ensuring system role "${systemRole}" exists`);
const exists = await this.rolesService.exists(systemRole);
if (exists) continue;
const newRole = await this.rolesService.create(
systemRole,
SystemRoleDefaults[systemRole],
);
if (HasFailed(newRole)) {
this.logger.error(
`Failed to create system role "${systemRole}" because: ${newRole.getReason()}`,
);
continue;
}
}
}
private async updateImmutableRoles() {
for (const immutableRole of ImmuteableRolesList as SystemRoles) {
this.logger.debug(
`Updating permissions for immutable role "${immutableRole}"`,
);
const result = await this.rolesService.setPermissions(
immutableRole,
SystemRoleDefaults[immutableRole],
true,
);
if (HasFailed(result)) {
this.logger.error(
`Failed to update permissions for immutable role "${immutableRole}" because: ${result.getReason()}`,
);
continue;
}
}
}
}

View file

@ -0,0 +1,173 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import {
ImmuteableRolesList,
Roles,
SystemRolesList
} from 'picsur-shared/dist/dto/roles.dto';
import {
AsyncFailable,
Fail,
HasFailed,
HasSuccess
} from 'picsur-shared/dist/types';
import { Repository } from 'typeorm';
import { ERoleBackend } from '../../models/entities/role.entity';
@Injectable()
export class RolesService {
private readonly logger = new Logger('UsersService');
constructor(
@InjectRepository(ERoleBackend)
private rolesRepository: Repository<ERoleBackend>,
) {}
public async create(
name: string,
permissions: Permissions,
): AsyncFailable<ERoleBackend> {
if (await this.exists(name)) return Fail('Role already exists');
let role = new ERoleBackend();
role.name = name;
role.permissions = permissions;
try {
role = await this.rolesRepository.save(role, { reload: true });
} catch (e: any) {
return Fail(e?.message);
}
return plainToClass(ERoleBackend, role);
}
public async delete(
role: string | ERoleBackend,
): AsyncFailable<ERoleBackend> {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
if (SystemRolesList.includes(roleToModify.name)) {
return Fail('Cannot delete system role');
}
try {
return await this.rolesRepository.remove(roleToModify);
} catch (e: any) {
return Fail(e?.message);
}
}
public async getPermissions(roles: Roles): AsyncFailable<Permissions> {
const permissions: Permissions = [];
const foundRoles = await Promise.all(
roles.map((role: string) => this.findOne(role)),
);
for (const foundRole of foundRoles) {
if (HasFailed(foundRole)) return foundRole;
permissions.push(...foundRole.permissions);
}
return [...new Set(...[permissions])];
}
public async addPermissions(
role: string | ERoleBackend,
permissions: Permissions,
): AsyncFailable<true> {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
// This is stupid
const newPermissions = [
...new Set([...roleToModify.permissions, ...permissions]),
];
return this.setPermissions(roleToModify, newPermissions);
}
public async removePermissions(
role: string | ERoleBackend,
permissions: Permissions,
): AsyncFailable<true> {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
const newPermissions = roleToModify.permissions.filter(
(permission) => !permissions.includes(permission),
);
return this.setPermissions(roleToModify, newPermissions);
}
public async setPermissions(
role: string | ERoleBackend,
permissions: Permissions,
allowImmutable: boolean = false,
): AsyncFailable<true> {
const roleToModify = await this.resolve(role);
if (HasFailed(roleToModify)) return roleToModify;
if (!allowImmutable && ImmuteableRolesList.includes(roleToModify.name)) {
return Fail('Cannot modify immutable role');
}
roleToModify.permissions = permissions;
try {
await this.rolesRepository.save(roleToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
public async findOne(name: string): AsyncFailable<ERoleBackend> {
try {
const found = await this.rolesRepository.findOne({
where: { name },
});
if (!found) return Fail('User not found');
return found as ERoleBackend;
} catch (e: any) {
return Fail(e?.message);
}
}
public async exists(username: string): Promise<boolean> {
return HasSuccess(await this.findOne(username));
}
public async nuke(iamsure: boolean = false): AsyncFailable<true> {
if (!iamsure) return Fail('Nuke aborted');
try {
await this.rolesRepository.delete({});
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
private async resolve(
user: string | ERoleBackend,
): AsyncFailable<ERoleBackend> {
if (typeof user === 'string') {
return await this.findOne(user);
} else {
user = plainToClass(ERoleBackend, user);
const errors = await validate(user, { forbidUnknownValues: true });
if (errors.length > 0) {
this.logger.warn(errors);
return Fail('Invalid role');
}
return user;
}
}
}

View file

@ -1,11 +1,55 @@
import { Module } from '@nestjs/common';
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HasFailed } from 'picsur-shared/dist/types';
import { AuthConfigService } from '../../config/auth.config.service';
import { PicsurConfigModule } from '../../config/config.module';
import { EUserBackend } from '../../models/entities/user.entity';
import { RolesModule } from '../roledb/roledb.module';
import { UsersService } from './userdb.service';
@Module({
imports: [TypeOrmModule.forFeature([EUserBackend])],
imports: [
PicsurConfigModule,
RolesModule,
TypeOrmModule.forFeature([EUserBackend]),
],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
export class UsersModule implements OnModuleInit {
private readonly logger = new Logger('UsersModule');
constructor(
private usersService: UsersService,
private authConfigService: AuthConfigService,
) {}
async onModuleInit() {
await this.ensureAdminExists();
}
private async ensureAdminExists() {
const username = this.authConfigService.getDefaultAdminUsername();
const password = this.authConfigService.getDefaultAdminPassword();
this.logger.debug(`Ensuring admin user "${username}" exists`);
const exists = await this.usersService.exists(username);
if (exists) return;
const newUser = await this.usersService.create(username, password);
if (HasFailed(newUser)) {
this.logger.error(
`Failed to create admin user "${username}" because: ${newUser.getReason()}`,
);
return;
}
const result = await this.usersService.addRoles(newUser, ['admin']);
if (HasFailed(result)) {
this.logger.error(
`Failed to make admin user "${username}" because: ${result.getReason()}`,
);
return;
}
}
}

View file

@ -1,8 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcrypt';
import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { PermanentRolesList, Roles } from 'picsur-shared/dist/dto/roles.dto';
import {
AsyncFailable,
Fail,
@ -12,6 +14,7 @@ import {
import { Repository } from 'typeorm';
import { EUserBackend } from '../../models/entities/user.entity';
import { GetCols } from '../collectionutils';
import { RolesService } from '../roledb/roledb.service';
@Injectable()
export class UsersService {
@ -20,15 +23,18 @@ export class UsersService {
constructor(
@InjectRepository(EUserBackend)
private usersRepository: Repository<EUserBackend>,
private rolesService: RolesService,
) {}
public async create(
username: string,
hashedPassword: string,
password: string,
roles?: Roles,
): AsyncFailable<EUserBackend> {
if (await this.exists(username)) return Fail('User already exists');
const hashedPassword = await bcrypt.hash(password, 12);
let user = new EUserBackend();
user.username = username;
user.password = hashedPassword;
@ -57,6 +63,70 @@ export class UsersService {
}
}
async authenticate(
username: string,
password: string,
): AsyncFailable<EUserBackend> {
const user = await this.findOne(username, true);
if (HasFailed(user)) return user;
if (!(await bcrypt.compare(password, user.password)))
return Fail('Wrong password');
return await this.findOne(username);
}
public async getPermissions(
user: string | EUserBackend,
): AsyncFailable<Permissions> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
return await this.rolesService.getPermissions(userToModify.roles);
}
public async addRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
// This is stupid
userToModify.roles = [...new Set([...userToModify.roles, ...roles])];
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
public async removeRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
// Make sure we don't remove unremovable roles
roles = roles.filter((role) => !PermanentRolesList.includes(role));
userToModify.roles = userToModify.roles.filter(
(role) => !roles.includes(role),
);
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
public async findOne<B extends true | undefined = undefined>(
username: string,
getPrivate?: B,
@ -90,45 +160,6 @@ export class UsersService {
return HasSuccess(await this.findOne(username));
}
public async addRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
// This is stupid
userToModify.roles = [...new Set([...userToModify.roles, ...roles])];
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
public async removeRoles(
user: string | EUserBackend,
roles: Roles,
): AsyncFailable<true> {
const userToModify = await this.resolve(user);
if (HasFailed(userToModify)) return userToModify;
userToModify.roles = userToModify.roles.filter(
(role) => !roles.includes(role),
);
try {
await this.usersRepository.save(userToModify);
} catch (e: any) {
return Fail(e?.message);
}
return true;
}
private async resolve(
user: string | EUserBackend,
): AsyncFailable<EUserBackend> {

View file

@ -0,0 +1,17 @@
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';
export const RequiredPermissions = (...permissions: Permissions) => {
return SetMetadata('permissions', permissions);
};
// Easy to read roles
export const NoAuth = () => RequiredPermissions();
export const UseLocalAuth = (...permissions: Permissions) =>
CombineDecorators(
RequiredPermissions(...permissions),
UseGuards(LocalAuthGuard),
);

View file

@ -1,21 +0,0 @@
import { SetMetadata, UseGuards } from '@nestjs/common';
import { Roles as RolesList } from 'picsur-shared/dist/dto/roles.dto';
import { CombineDecorators } from 'picsur-shared/dist/util/decorator';
import { LocalAuthGuard } from '../managers/auth/guards/localauth.guard';
export const GuestRoles = (...roles: RolesList) => {
return SetMetadata('roles', roles);
};
export const UserRoles = (...roles: RolesList) => {
const fullRoles = [...new Set(['user', ...roles])];
return SetMetadata('roles', fullRoles);
};
// Easy to read roles
export const Guest = () => GuestRoles();
export const User = () => UserRoles();
export const Admin = () => UserRoles('admin');
export const UseLocalAuth = () =>
CombineDecorators(Guest(), UseGuards(LocalAuthGuard));

View file

@ -6,6 +6,7 @@ import {
} from '@nestjs/platform-fastify';
import * as multipart from 'fastify-multipart';
import { AppModule } from './app.module';
import { UsersService } from './collections/userdb/userdb.service';
import { HostConfigService } from './config/host.config.service';
import { MainExceptionFilter } from './layers/httpexception/httpexception.filter';
import { SuccessInterceptor } from './layers/success/success.interceptor';
@ -33,7 +34,9 @@ async function bootstrap() {
forbidUnknownValues: true,
}),
);
app.useGlobalGuards(new MainAuthGuard(new Reflector()));
app.useGlobalGuards(
new MainAuthGuard(app.get(Reflector), app.get(UsersService)),
);
app.useLogger(app.get(PicsurLoggerService));

View file

@ -1,10 +1,8 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceModule } from '../../collections/syspreferencesdb/syspreferencedb.module';
import { UsersModule } from '../../collections/userdb/userdb.module';
import { AuthConfigService } from '../../config/auth.config.service';
import {
JwtConfigService,
JwtSecretProvider
@ -35,42 +33,6 @@ import { GuestService } from './guest.service';
JwtSecretProvider,
GuestService,
],
exports: [AuthManagerService],
exports: [UsersModule, AuthManagerService],
})
export class AuthManagerModule implements OnModuleInit {
private readonly logger = new Logger('AuthModule');
constructor(
private authService: AuthManagerService,
private authConfigService: AuthConfigService,
) {}
async onModuleInit() {
await this.ensureAdminExists();
}
private async ensureAdminExists() {
const username = this.authConfigService.getDefaultAdminUsername();
const password = this.authConfigService.getDefaultAdminPassword();
this.logger.debug(`Ensuring admin user "${username}" exists`);
const exists = await this.authService.userExists(username);
if (exists) return;
const newUser = await this.authService.createUser(username, password);
if (HasFailed(newUser)) {
this.logger.error(
`Failed to create admin user "${username}" because: ${newUser.getReason()}`,
);
return;
}
const result = await this.authService.makeAdmin(newUser);
if (HasFailed(result)) {
this.logger.error(
`Failed to make admin user "${username}" because: ${result.getReason()}`,
);
return;
}
}
}
export class AuthManagerModule {}

View file

@ -1,48 +1,15 @@
import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { instanceToPlain, plainToClass } from 'class-transformer';
import { validate } from 'class-validator';
import { JwtDataDto } from 'picsur-shared/dist/dto/auth.dto';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../collections/userdb/userdb.service';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable()
export class AuthManagerService {
private readonly logger = new Logger('AuthService');
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async createUser(username: string, password: string): AsyncFailable<EUserBackend> {
const hashedPassword = await bcrypt.hash(password, 12);
return this.usersService.create(username, hashedPassword);
}
async deleteUser(user: string | EUserBackend): AsyncFailable<EUserBackend> {
return this.usersService.delete(user);
}
async listUsers(): AsyncFailable<EUserBackend[]> {
return this.usersService.findAll();
}
async userExists(username: string): Promise<boolean> {
return this.usersService.exists(username);
}
async authenticate(username: string, password: string): AsyncFailable<EUserBackend> {
const user = await this.usersService.findOne(username, true);
if (HasFailed(user)) return user;
if (!(await bcrypt.compare(password, user.password)))
return Fail('Wrong password');
return await this.usersService.findOne(username);
}
constructor(private jwtService: JwtService) {}
async createToken(user: EUserBackend): Promise<string> {
const jwtData: JwtDataDto = plainToClass(JwtDataDto, {
@ -57,12 +24,4 @@ export class AuthManagerService {
return this.jwtService.signAsync(instanceToPlain(jwtData));
}
async makeAdmin(user: string | EUserBackend): AsyncFailable<true> {
return this.usersService.addRoles(user, ['admin']);
}
async revokeAdmin(user: string | EUserBackend): AsyncFailable<true> {
return this.usersService.removeRoles(user, ['admin']);
}
}

View file

@ -2,17 +2,17 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { EUserBackend } from '../../../models/entities/user.entity';
import { AuthManagerService } from '../auth.service';
@Injectable()
export class LocalAuthStrategy extends PassportStrategy(Strategy, 'local') {
constructor(private authService: AuthManagerService) {
constructor(private usersService: UsersService) {
super();
}
async validate(username: string, password: string): AsyncFailable<EUserBackend> {
const user = await this.authService.authenticate(username, password);
const user = await this.usersService.authenticate(username, password);
if (HasFailed(user)) {
throw new UnauthorizedException();
}

View file

@ -8,15 +8,23 @@ import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { plainToClass } from 'class-transformer';
import { isArray, isEnum, isString, validate } from 'class-validator';
import { Roles, RolesList } from 'picsur-shared/dist/dto/roles.dto';
import {
Permissions,
PermissionsList
} 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 { UsersService } from '../../../collections/userdb/userdb.service';
import { EUserBackend } from '../../../models/entities/user.entity';
@Injectable()
export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
private readonly logger = new Logger('MainAuthGuard');
constructor(private reflector: Reflector) {
constructor(
private reflector: Reflector,
private usersService: UsersService,
) {
super();
}
@ -31,39 +39,47 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
context.switchToHttp().getRequest().user,
);
const roles = this.extractRoles(context);
if (HasFailed(roles)) {
this.logger.warn(roles.getReason());
const permissions = this.extractPermissions(context);
if (HasFailed(permissions)) {
this.logger.warn(permissions.getReason());
return false;
}
// User must have all roles
return roles.every((role) => user.roles.includes(role));
const userPermissions = await this.usersService.getPermissions(user);
if (HasFailed(userPermissions)) {
this.logger.warn(userPermissions.getReason());
return false;
}
private extractRoles(context: ExecutionContext): Failable<Roles> {
const handlerName = context.getHandler().name;
const roles =
this.reflector.get<Roles>('roles', context.getHandler()) ??
this.reflector.get<Roles>('roles', context.getClass());
if (roles === undefined) {
return Fail(
`${handlerName} does not have any roles defined, denying access`,
return permissions.every((permission) =>
userPermissions.includes(permission),
);
}
if (!this.isRolesArray(roles)) {
return Fail(`Roles for ${handlerName} is not a string array`);
private extractPermissions(context: ExecutionContext): Failable<Permissions> {
const handlerName = context.getHandler().name;
const permissions =
this.reflector.get<Permissions>('permissions', context.getHandler()) ??
this.reflector.get<Permissions>('permissions', context.getClass());
if (permissions === undefined) {
return Fail(
`${handlerName} does not have any permissions defined, denying access`,
);
}
return roles;
if (!this.isPermissionsArray(permissions)) {
return Fail(`Permissions for ${handlerName} is not a string array`);
}
private isRolesArray(value: any): value is Roles {
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, RolesList))) return false;
if (!value.every((item: string) => isEnum(item, PermissionsList)))
return false;
return true;
}

View file

@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Roles } from 'picsur-shared/dist/dto/roles.dto';
import { EUserBackend } from '../../models/entities/user.entity';
@Injectable()
@ -7,13 +6,9 @@ export class GuestService {
public createGuest(): EUserBackend {
const guest = new EUserBackend();
guest.id = -1;
guest.roles = this.createGuestRoles();
guest.roles = ['guest'];
guest.username = 'guest';
return guest;
}
private createGuestRoles(): Roles {
return [];
}
}

View file

@ -1,5 +1,6 @@
import { EImageBackend } from './image.entity';
import { ERoleBackend } from './role.entity';
import { ESysPreferenceBackend } from './syspreference.entity';
import { EUserBackend } from './user.entity';
export const EntityList = [EImageBackend, EUserBackend, ESysPreferenceBackend];
export const EntityList = [EImageBackend, EUserBackend, ERoleBackend, ESysPreferenceBackend];

View file

@ -0,0 +1,16 @@
import { Permissions } from 'picsur-shared/dist/dto/permissions';
import { ERole } from 'picsur-shared/dist/entities/role.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class ERoleBackend extends ERole {
@PrimaryGeneratedColumn()
override id?: number;
@Index()
@Column({ nullable: false, unique: true })
override name: string;
@Column('text', { nullable: false, array: true })
override permissions: Permissions;
}

View file

@ -3,6 +3,7 @@ import {
Controller,
Get,
InternalServerErrorException,
Logger,
Post,
Request
} from '@nestjs/common';
@ -13,16 +14,22 @@ import {
AuthRegisterRequest
} from 'picsur-shared/dist/dto/auth.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { Admin, UseLocalAuth, User } from '../../../decorators/roles.decorator';
import { UsersService } from '../../../collections/userdb/userdb.service';
import { RequiredPermissions, UseLocalAuth } from '../../../decorators/permissions.decorator';
import { AuthManagerService } from '../../../managers/auth/auth.service';
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/auth')
export class AuthController {
constructor(private authService: AuthManagerService) {}
private readonly logger = new Logger('AuthController');
constructor(
private usersService: UsersService,
private authService: AuthManagerService,
) {}
@Post('login')
@UseLocalAuth()
@UseLocalAuth('user-login')
async login(@Request() req: AuthFasityRequest) {
const response: AuthLoginResponse = {
jwt_token: await this.authService.createToken(req.user),
@ -31,37 +38,37 @@ export class AuthController {
return response;
}
@Post('create')
@Admin()
@Post('register')
@RequiredPermissions('user-register')
async register(
@Request() req: AuthFasityRequest,
@Body() register: AuthRegisterRequest,
) {
const user = await this.authService.createUser(
const user = await this.usersService.create(
register.username,
register.password,
);
if (HasFailed(user)) {
console.warn(user.getReason());
throw new InternalServerErrorException('Could not create user');
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not register user');
}
if (register.isAdmin) {
await this.authService.makeAdmin(user);
await this.usersService.addRoles(user, ['admin']);
}
return user;
}
@Post('delete')
@Admin()
@RequiredPermissions('user-manage')
async delete(
@Request() req: AuthFasityRequest,
@Body() deleteData: AuthDeleteRequest,
) {
const user = await this.authService.deleteUser(deleteData.username);
const user = await this.usersService.delete(deleteData.username);
if (HasFailed(user)) {
console.warn(user.getReason());
this.logger.warn(user.getReason());
throw new InternalServerErrorException('Could not delete user');
}
@ -69,11 +76,11 @@ export class AuthController {
}
@Get('list')
@Admin()
@RequiredPermissions('user-manage')
async listUsers(@Request() req: AuthFasityRequest) {
const users = this.authService.listUsers();
const users = this.usersService.findAll();
if (HasFailed(users)) {
console.warn(users.getReason());
this.logger.warn(users.getReason());
throw new InternalServerErrorException('Could not list users');
}
@ -81,10 +88,17 @@ export class AuthController {
}
@Get('me')
@User()
@RequiredPermissions('user-view')
async me(@Request() req: AuthFasityRequest) {
const permissions = await this.usersService.getPermissions(req.user);
if (HasFailed(permissions)) {
this.logger.warn(permissions.getReason());
throw new InternalServerErrorException('Could not get permissions');
}
const meResponse: AuthMeResponse = new AuthMeResponse();
meResponse.user = req.user;
meResponse.permissions = permissions;
meResponse.newJwtToken = await this.authService.createToken(req.user);
return meResponse;

View file

@ -1,12 +1,11 @@
import { Controller, Get, Request } from '@nestjs/common';
import { Guest } from '../../../decorators/roles.decorator';
import AuthFasityRequest from '../../../models/dto/authrequest.dto';
@Controller('api/experiment')
export class ExperimentController {
@Get()
@Guest()
// @Guest()
async testRoute(@Request() req: AuthFasityRequest) {
return {
message: req.user,

View file

@ -3,6 +3,7 @@ import {
Controller,
Get,
InternalServerErrorException,
Logger,
Param,
Post
} from '@nestjs/common';
@ -12,11 +13,13 @@ import {
} from 'picsur-shared/dist/dto/syspreferences.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { SysPreferenceService } from '../../../collections/syspreferencesdb/syspreferencedb.service';
import { Admin } from '../../../decorators/roles.decorator';
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
@Controller('api/pref')
@Admin()
@RequiredPermissions('syspref-manage')
export class PrefController {
private readonly logger = new Logger('PrefController');
constructor(private prefService: SysPreferenceService) {}
@Get('sys/:key')
@ -25,7 +28,7 @@ export class PrefController {
key as SysPreferences,
);
if (HasFailed(returned)) {
console.warn(returned.getReason());
this.logger.warn(returned.getReason());
throw new InternalServerErrorException('Could not get preference');
}
@ -43,7 +46,7 @@ export class PrefController {
value,
);
if (HasFailed(returned)) {
console.warn(returned.getReason());
this.logger.warn(returned.getReason());
throw new InternalServerErrorException('Could not set preference');
}

View file

@ -3,6 +3,7 @@ import {
Controller,
Get,
InternalServerErrorException,
Logger,
NotFoundException,
Param,
Post,
@ -13,13 +14,15 @@ import { isHash } from 'class-validator';
import { FastifyReply, FastifyRequest } from 'fastify';
import { HasFailed } from 'picsur-shared/dist/types';
import { MultiPart } from '../../decorators/multipart.decorator';
import { Guest } from '../../decorators/roles.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ImageManagerService } from '../../managers/imagemanager/imagemanager.service';
import { ImageUploadDto } from '../../models/dto/imageroute.dto';
@Controller('i')
@Guest()
@RequiredPermissions('image-view')
export class ImageController {
private readonly logger = new Logger('ImageController');
constructor(private readonly imagesService: ImageManagerService) {}
@Get(':hash')
@ -31,7 +34,7 @@ export class ImageController {
const image = await this.imagesService.retrieveComplete(hash);
if (HasFailed(image)) {
console.warn(image.getReason());
this.logger.warn(image.getReason());
throw new NotFoundException('Could not find image');
}
@ -45,7 +48,7 @@ export class ImageController {
const image = await this.imagesService.retrieveInfo(hash);
if (HasFailed(image)) {
console.warn(image.getReason());
this.logger.warn(image.getReason());
throw new NotFoundException('Could not find image');
}
@ -53,7 +56,7 @@ export class ImageController {
}
@Post()
//@User()
@RequiredPermissions('image-upload')
async uploadImage(
@Req() req: FastifyRequest,
@MultiPart(ImageUploadDto) multipart: ImageUploadDto,
@ -61,7 +64,7 @@ export class ImageController {
const fileBuffer = await multipart.image.toBuffer();
const image = await this.imagesService.upload(fileBuffer);
if (HasFailed(image)) {
console.warn(image.getReason());
this.logger.warn(image.getReason());
throw new InternalServerErrorException('Could not upload image');
}

View file

@ -14,6 +14,8 @@ import { KeyService } from './key.service';
providedIn: 'root',
})
export class ApiService {
private readonly logger = console;
constructor(private keyService: KeyService) {}
public async get<T extends Object>(
@ -32,7 +34,7 @@ export class ApiService {
const sendClass = plainToClass(sendType, data);
const errors = await validate(sendClass);
if (errors.length > 0) {
console.warn(errors);
this.logger.warn(errors);
return Fail('Something went wrong');
}
@ -69,14 +71,14 @@ export class ApiService {
>(ApiSuccessResponse, result);
const resultErrors = await validate(resultClass);
if (resultErrors.length > 0) {
console.warn('result', resultErrors);
this.logger.warn('result', resultErrors);
return Fail('Something went wrong');
}
const dataClass = plainToClass(type, result.data);
const dataErrors = await validate(dataClass);
if (dataErrors.length > 0) {
console.warn('data', dataErrors);
this.logger.warn('data', dataErrors);
return Fail('Something went wrong');
}
@ -94,7 +96,7 @@ export class ApiService {
try {
return await response.json();
} catch (e) {
console.warn(e);
this.logger.warn(e);
return Fail('Something went wrong');
}
}
@ -109,7 +111,7 @@ export class ApiService {
try {
return await response.arrayBuffer();
} catch (e) {
console.warn(e);
this.logger.warn(e);
return Fail('Something went wrong');
}
}
@ -130,7 +132,7 @@ export class ApiService {
return await window.fetch(url, options);
} catch (e: any) {
console.warn(e);
this.logger.warn(e);
return Fail('Something went wrong');
}
}

View file

@ -21,6 +21,8 @@ import { KeyService } from './key.service';
providedIn: 'root',
})
export class UserService {
private readonly logger = console;
public get liveUser() {
return this.userSubject;
}
@ -79,7 +81,7 @@ export class UserService {
const user = await this.extractUser(apikey);
if (HasFailed(user)) {
console.warn(user.getReason());
this.logger.warn(user.getReason());
await this.logout();
return;
}
@ -88,7 +90,7 @@ export class UserService {
const fetchedUser = await this.fetchUser();
if (HasFailed(fetchedUser)) {
console.warn(fetchedUser.getReason());
this.logger.warn(fetchedUser.getReason());
await this.logout();
return;
}
@ -107,7 +109,7 @@ export class UserService {
const jwtData = plainToClass(JwtDataDto, decoded);
const errors = await validate(jwtData);
if (errors.length > 0) {
console.warn(errors);
this.logger.warn(errors);
return Fail('Invalid token data');
}

View file

@ -12,6 +12,8 @@ import { LoginControl } from './login.model';
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
private readonly logger = console;
model = new LoginControl();
loginFail = false;
@ -35,7 +37,7 @@ export class LoginComponent implements OnInit {
const user = await this.userService.login(data.username, data.password);
if (HasFailed(user)) {
console.warn(user);
this.logger.warn(user);
this.loginFail = true;
return;
}

View file

@ -1,14 +1,12 @@
import { Type } from 'class-transformer';
import {
IsBoolean,
IsDefined,
IsNotEmpty,
IsArray, IsBoolean,
IsDefined, IsEnum, IsInt, IsNotEmpty,
IsOptional,
IsString,
IsInt,
ValidateNested,
IsString, ValidateNested
} from 'class-validator';
import { EUser } from '../entities/user.entity';
import { Type } from 'class-transformer';
import { Permissions, PermissionsList } from './permissions';
// Api
@ -56,6 +54,11 @@ export class AuthMeResponse {
@Type(() => EUser)
user: EUser;
@IsDefined()
@IsArray()
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
@IsString()
@IsDefined()
newJwtToken: string;

View file

@ -0,0 +1,20 @@
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-view', // Ability to view user info, only granted if logged in
'user-manage',
'syspref-manage',
);
// Derivatives
export const PermissionsList: string[] = PermissionsTuple;
export type Permission = typeof PermissionsTuple[number];
export type Permissions = Permission[];

View file

@ -1,12 +1,36 @@
import tuple from '../types/tuple';
import { Permissions, PermissionsList } from './permissions';
// Config
const RolesTuple = tuple('user', 'admin');
// These roles can never be removed from a user
const PermanentRolesTuple = tuple('guest', 'user');
// These reles can never be modified
const ImmuteableRolesTuple = tuple('admin');
// These roles can never be removed from the server
const SystemRolesTuple = tuple(...PermanentRolesTuple, ...ImmuteableRolesTuple);
// Derivatives
export const RolesList: string[] = RolesTuple;
export const PermanentRolesList: string[] = PermanentRolesTuple;
export const ImmuteableRolesList: string[] = ImmuteableRolesTuple;
export const SystemRolesList: string[] = SystemRolesTuple;
export type Role = typeof RolesTuple[number];
export type SystemRole = typeof SystemRolesTuple[number];
export type SystemRoles = SystemRole[];
// Defaults
export const SystemRoleDefaults: {
[key in SystemRole]: Permissions;
} = {
guest: ['image-view', 'user-login'],
user: ['image-view', 'user-view', 'user-login', 'image-upload'],
// Grant all permissions to admin
admin: PermissionsList as Permissions,
};
// Normal roles types
export type Role = SystemRole | string;
export type Roles = Role[];

View file

@ -0,0 +1,14 @@
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { Permissions, PermissionsList } from '../dto/permissions';
export class ERole {
@IsOptional()
id?: number;
@IsNotEmpty()
name: string;
@IsArray()
@IsEnum(PermissionsList, { each: true })
permissions: Permissions;
}

View file

@ -1,6 +1,10 @@
import { Exclude } from 'class-transformer';
import { IsArray, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
import { Roles, RolesList } from '../dto/roles.dto';
import {
IsArray, IsNotEmpty,
IsOptional,
IsString
} from 'class-validator';
import { Roles } from '../dto/roles.dto';
export class EUser {
@IsOptional()
@ -10,7 +14,7 @@ export class EUser {
username: string;
@IsArray()
@IsEnum(RolesList, { each: true })
@IsString({ each: true })
roles: Roles;
@IsOptional()