diff --git a/api/src/app.module.ts b/api/src/app.module.ts index e3ce26947..09eef6f67 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -27,6 +27,7 @@ import { AnalyticsModule } from './analytics/analytics.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AttachmentModule } from './attachment/attachment.module'; +import { AuthorizationModule } from './authorization/authorization.module'; import { ChannelModule } from './channel/channel.module'; import { ChatModule } from './chat/chat.module'; import { CmsModule } from './cms/cms.module'; @@ -60,6 +61,7 @@ const i18nOptions: I18nOptions = { @Module({ imports: [ + AuthorizationModule, MailerModule, MongooseModule.forRoot(config.mongo.uri, { dbName: config.mongo.dbName, diff --git a/api/src/authorization/authorization.module.ts b/api/src/authorization/authorization.module.ts new file mode 100644 index 000000000..a6b00734b --- /dev/null +++ b/api/src/authorization/authorization.module.ts @@ -0,0 +1,21 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { Global, Module } from '@nestjs/common'; + +import { UserModule } from '@/user/user.module'; + +import { AuthorizationService } from './authorization.service'; + +@Global() +@Module({ + imports: [UserModule], + providers: [AuthorizationService], + exports: [AuthorizationService], +}) +export class AuthorizationModule {} diff --git a/api/src/authorization/authorization.service.ts b/api/src/authorization/authorization.service.ts new file mode 100644 index 000000000..667580c6f --- /dev/null +++ b/api/src/authorization/authorization.service.ts @@ -0,0 +1,65 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { Injectable } from '@nestjs/common'; + +import { LoggerService } from '@/logger/logger.service'; +import { PermissionService } from '@/user/services/permission.service'; +import { UserService } from '@/user/services/user.service'; +import { MethodToAction } from '@/user/types/action.type'; +import { TModel } from '@/user/types/model.type'; + +@Injectable() +export class AuthorizationService { + constructor( + private readonly logger: LoggerService, + private readonly userService: UserService, + private readonly permissionService: PermissionService, + ) {} + + /** + * Checks if a request (REST/WebSocket) is authorized to get access + * + * @param method - The Request Method + * @param userRoles - An array of ID's user Roles or empty + * @param targetModel - The target model that we want access + * @returns + */ + async canAccess( + method: string, + userId: string, + targetModel?: TModel, + ): Promise { + try { + if (!targetModel) { + return false; + } + const user = await this.userService.findOne(userId); + const permissions = await this.permissionService.getPermissions(); + + if (permissions && user?.roles?.length) { + const permissionsFromRoles = Object.entries(permissions) + .filter(([key, _]) => user.roles.includes(key)) + .map(([_, value]) => value); + + if ( + permissionsFromRoles.some((permission) => + permission[targetModel]?.includes(MethodToAction[method]), + ) + ) { + return true; + } + } + } catch (err) { + this.logger.error('Request has no ability to get access', err); + return false; + } + + return false; + } +} diff --git a/api/src/user/guards/ability.guard.ts b/api/src/user/guards/ability.guard.ts index 96fee534b..ffc190b00 100644 --- a/api/src/user/guards/ability.guard.ts +++ b/api/src/user/guards/ability.guard.ts @@ -16,21 +16,19 @@ import { UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { EventEmitter2 } from '@nestjs/event-emitter'; import { Request } from 'express'; +import { AuthorizationService } from '@/authorization/authorization.service'; + import { TRole } from '../schemas/role.schema'; import { User } from '../schemas/user.schema'; -import { PermissionService } from '../services/permission.service'; -import { MethodToAction } from '../types/action.type'; import { TModel } from '../types/model.type'; @Injectable() export class Ability implements CanActivate { constructor( private reflector: Reflector, - private readonly permissionService: PermissionService, - private readonly eventEmitter: EventEmitter2, + private readonly authorizationService: AuthorizationService, ) {} async canActivate(context: ExecutionContext): Promise { @@ -69,26 +67,17 @@ export class Ability implements CanActivate { ) { return true; } - const modelFromPathname = _parsedUrl?.pathname - ?.split('/')[1] - .toLowerCase() as TModel | undefined; - - const permissions = await this.permissionService.getPermissions(); - - if (permissions) { - const permissionsFromRoles = Object.entries(permissions) - .filter(([key, _]) => user.roles.includes(key)) - .map(([_, value]) => value); + const targetModel = _parsedUrl?.pathname?.split('/')[1].toLowerCase() as + | TModel + | undefined; - if ( - modelFromPathname && - permissionsFromRoles.some((permission) => - permission[modelFromPathname]?.includes(MethodToAction[method]), - ) - ) { - return true; - } - } else { + try { + return await this.authorizationService.canAccess( + method, + user.id, + targetModel, + ); + } catch (e) { throw new NotFoundException('Failed to load permissions'); } }