Skip to content

Conversation

@yassinedorbozgithub
Copy link
Collaborator

@yassinedorbozgithub yassinedorbozgithub commented Jul 8, 2025

Motivation

The main motivation of this PR is to add an Authorization Module that can manage access

Fixes #1209

Demo

Authorization.Module.REST.integration.webm

Checklist:

  • I have performed a self-review of my own code
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a centralized authorization service to manage user access rights based on roles and permissions.
  • Refactor

    • Streamlined permission checks by integrating the new authorization service, simplifying access control across the application.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 8, 2025

Warning

Rate limit exceeded

@yassinedorbozgithub has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 4 minutes and 26 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between ebc0c75 and fb9c01e.

📒 Files selected for processing (2)
  • api/src/authorization/authorization.service.ts (1 hunks)
  • api/src/user/guards/ability.guard.ts (2 hunks)

"""

Walkthrough

A new global AuthorizationModule has been introduced, consolidating user and permission management. An AuthorizationService provides centralized access control logic. The AbilityGuard now delegates permission checks to this service, removing its internal permission filtering. The main application module imports the new authorization module, integrating it into the application.

Changes

File(s) Change Summary
api/src/authorization/authorization.module.ts Introduced new global AuthorizationModule with relevant providers and exports.
api/src/authorization/authorization.service.ts Added AuthorizationService for access control logic based on user roles and permissions.
api/src/user/guards/ability.guard.ts Refactored guard to delegate permission checks to AuthorizationService; updated dependencies.
api/src/app.module.ts Imported AuthorizationModule into the main application module.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AbilityGuard
    participant AuthorizationService
    participant UserService
    participant PermissionService

    Client->>AbilityGuard: Request with user and HTTP method
    AbilityGuard->>AuthorizationService: canAccess(method, userId, model)
    AuthorizationService->>UserService: getUserById(userId)
    AuthorizationService->>PermissionService: getPermissions()
    AuthorizationService-->>AbilityGuard: true/false (access granted/denied)
    AbilityGuard-->>Client: Allow or deny request
Loading

Assessment against linked issues

Objective Addressed Explanation
Centralize REST/WebSocket access controls in an authorization module (#1209)
Implement access control based on user roles, permissions, and models (#1209)

Poem

A module hops in, permissions in tow,
With logic now central, our access will flow.
Guards call the service, no longer confused,
Roles, models, and rights—neatly fused.
The warren is safer, the codebase more bright,
Thanks to this change, all access is right!
🐇✨
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@yassinedorbozgithub yassinedorbozgithub changed the title 1209 issue add authorization module feat(api): add Authorization Module Jul 8, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
api/src/authorization/authorization.module.ts (1)

21-33: Consider architectural implications of the @global() decorator.

Making this module global means all its providers will be available application-wide without explicit imports. While this simplifies usage of AuthorizationService, it also:

  • Reduces module encapsulation
  • Makes dependency tracking harder
  • Could lead to unintended coupling

Consider whether the @Global() decorator is truly necessary, or if explicit imports in modules that need authorization would be more maintainable.

api/src/authorization/authorization.service.ts (1)

55-58: Improve error handling to distinguish between different failure scenarios.

Currently, all errors result in the same log message and return false. Consider distinguishing between:

  • User not found
  • Permission loading failures
  • Other unexpected errors

This would help with debugging and monitoring.

api/src/user/guards/ability.guard.ts (2)

70-76: Consider handling edge cases for model extraction from pathname.

The current logic returns false if modelFromPathname is undefined. This might block access to valid routes that don't follow the /<model>/... pattern. Consider:

  • Root path /
  • Routes with prefixes like /api/v1/<model>/...
  • Routes with multiple path segments before the model

87-87: Consider preserving the original error for better debugging.

The generic error message doesn't provide context about what actually failed. Consider logging the original error or including more specific information.

-      } catch (e) {
-        throw new NotFoundException('Failed to load permissions');
+      } catch (error) {
+        this.logger.error('Failed to check permissions', error);
+        throw new NotFoundException('Failed to load permissions');

Note: You'll need to inject LoggerService in the constructor if you implement this suggestion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b10d07 and 56a1b29.

📒 Files selected for processing (4)
  • api/src/app.module.ts (2 hunks)
  • api/src/authorization/authorization.module.ts (1 hunks)
  • api/src/authorization/authorization.service.ts (1 hunks)
  • api/src/user/guards/ability.guard.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/authorization/authorization.service.ts (1)
api/src/user/controllers/user.controller.ts (1)
  • permissions (135-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (actions)
  • GitHub Check: API-Tests
  • GitHub Check: Frontend Tests
🔇 Additional comments (2)
api/src/app.module.ts (1)

30-30: LGTM!

The AuthorizationModule is correctly imported and added to the application's module imports.

Also applies to: 64-64

api/src/user/guards/ability.guard.ts (1)

78-88: LGTM! Clean refactoring to use AuthorizationService.

The delegation of permission checking to AuthorizationService improves separation of concerns and makes the guard more maintainable.

targetModel: TModel,
): Promise<boolean> {
try {
const user = await this.userService.findOne(userId);
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure user roles are properly populated.

The findOne method might not populate the user's roles. Based on the code snippet from user.controller.ts, you should use findOneAndPopulate to ensure roles are loaded.

-      const user = await this.userService.findOne(userId);
+      const user = await this.userService.findOneAndPopulate(userId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const user = await this.userService.findOne(userId);
const user = await this.userService.findOneAndPopulate(userId);
🤖 Prompt for AI Agents
In api/src/authorization/authorization.service.ts at line 39, replace the call
to this.userService.findOne(userId) with
this.userService.findOneAndPopulate(userId) to ensure the user's roles are
properly loaded and available for authorization checks.

Copy link
Contributor

@medchedli medchedli Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to use findOneAndPopulate because we only use the role IDs for permission checks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@medchedli You're absolutely right! Looking at the code, the roles are stored as an array of role IDs in the user document, and the permission check uses user.roles.includes(key) which works directly with the IDs. No need for population since we're only doing ID-based comparisons. Thanks for the clarification!

  (\   /)
 ( ._.)
o_(")(")

learns something new


✏️ Learnings added
Learnt from: medchedli
PR: Hexastack/Hexabot#1210
File: api/src/authorization/authorization.service.ts:42-42
Timestamp: 2025-07-08T15:16:44.888Z
Learning: In the Hexabot codebase, user roles are stored as an array of role IDs in the user document, not as references to role objects. Therefore, `userService.findOne()` is sufficient for authorization checks since `user.roles.includes(key)` works directly with the role IDs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +40 to +45
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the structure of permissions returned by getPermissions().

The code assumes that getPermissions() returns an object with role IDs as keys. Please verify this assumption and document the expected return type for clarity.


🏁 Script executed:

#!/bin/bash
# Description: Check the implementation of getPermissions() method to understand its return type

# Search for the getPermissions method implementation
ast-grep --pattern $'getPermissions() {
  $$$
}'

# Also search with async
ast-grep --pattern $'async getPermissions() {
  $$$
}'

# Look for the method signature in PermissionService
rg -A 10 "getPermissions" api/src/user/services/permission.service.ts

Length of output: 549


🏁 Script executed:

#!/bin/bash
# Locate the PermissionsTree type definition
rg -n "type PermissionsTree" -n api
rg -n "interface PermissionsTree" -n api

# Inspect buildTree implementation to see its return shape
rg -A 25 "buildTree" -n api/src/user/services/permission.service.ts

Length of output: 932


Document the PermissionsTree return type for clarity

We’ve confirmed that getPermissions() returns a PermissionsTree (i.e., Record<roleId, Record<modelIdentity, string[]>>), mapping role IDs to model-action arrays. To make this explicit for future maintainers, please add an inline type annotation or JSDoc at the call site:

• File: api/src/authorization/authorization.service.ts (around lines 40–45)

import type { PermissionsTree } from '../../user/types/permission.type';

// Explicitly type the returned permissions structure
const permissions: PermissionsTree = await this.permissionService.getPermissions();

— or —

/**
 * PermissionsTree mapping role IDs to model permissions:
 * Record<roleId, Record<modelIdentity, string[]>>
 */
const permissions = await this.permissionService.getPermissions();

This documents the expected shape and reinforces the assumption that roles are the top-level keys.

🤖 Prompt for AI Agents
In api/src/authorization/authorization.service.ts around lines 40 to 45, the
variable 'permissions' assigned from getPermissions() lacks an explicit type
annotation. To clarify the expected structure for future maintainers, import the
PermissionsTree type from '../../user/types/permission.type' and annotate the
'permissions' variable with this type, or alternatively add a JSDoc comment
above the assignment describing the PermissionsTree as Record<roleId,
Record<modelIdentity, string[]>>.


if (permissions && user?.roles?.length) {
const permissionsFromRoles = Object.entries(permissions)
.filter(([key, _]) => user.roles.includes(key))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Clarify the type of user.roles for consistent filtering.

The filtering logic on line 44 uses user.roles.includes(key), which suggests user.roles is an array of role IDs. However, if roles are populated objects, this comparison won't work correctly.


🏁 Script executed:

#!/bin/bash
# Description: Check the User schema to understand the structure of roles field

# Search for User schema definition
ast-grep --pattern $'class User {
  $$$
  roles$$$
  $$$
}'

# Also check for @Prop decorator on roles field
rg -B 2 -A 2 "roles.*:" api/src/user/schemas/user.schema.ts

Length of output: 510


🏁 Script executed:

#!/bin/bash
# Show lines around the filter usage in authorization.service.ts
rg -n "user.roles.includes" -C 3 api/src/authorization/authorization.service.ts

# Show the containing method signature
rg -n "filter" -C 5 api/src/authorization/authorization.service.ts

# Show imports of User/UserFull in this file
rg -n "import.*User" api/src/authorization/authorization.service.ts

Length of output: 1038


#!/bin/bash
# Find UserService and its findOne method signature
rg -n "class UserService" -C 10 api/src/user/services/user.service.ts
rg -n "findOne" -C 5 api/src/user/services/user.service.ts

🏁 Script executed:

#!/bin/bash
# Inspect UserService class and findOne method to determine return type and roles field type
rg -n "class UserService" -C 10 api/src/user/services/user.service.ts
rg -n "findOne" -C 5 api/src/user/services/user.service.ts

Length of output: 670


#!/bin/bash
# Locate BaseService file
base_service_file=$(fd base-service.ts -t f)

# Display the BaseService class definition
rg -n "export abstract class BaseService" -C 15 "$base_service_file"

# Show the findOne method signature and return type
rg -n -C 15 "findOne" "$base_service_file"

#!/bin/bash
# Inspect BaseService to see what type findOne returns
rg -n "export abstract class BaseService" -C 10 api/src/utils/generics/base-service.ts
rg -n "findOne" -C 10 api/src/utils/generics/base-service.ts

# Inspect UserRepository to see if findOne is overridden or has custom logic
rg -n "export class UserRepository" -C 10 api/src/user/repositories/user.repository.ts
rg -n "findOne" -C 10 api/src/user/repositories/user.repository.ts

Normalize user.roles to string[] before filtering

UserService.findOne returns a full user (UserFull) where roles is an array of Role objects, so calling user.roles.includes(key) (where key is a string ID) will never match. Extract the IDs as strings first:

  • File: api/src/authorization/authorization.service.ts, around line 44
- const permissionsFromRoles = Object.entries(permissions)
-   .filter(([key, _]) => user.roles.includes(key))
-   .map(([_, value]) => value);
+ // extract string IDs from Role objects (or use directly if already string[])
+ const roleIds = user.roles.map(r => typeof r === 'string' ? r : r._id.toString());
+ const permissionsFromRoles = Object.entries(permissions)
+   .filter(([key]) => roleIds.includes(key))
+   .map(([_, value]) => value);

Ensure that user.roles is always a string[] (or converted to one) before using .includes.

🤖 Prompt for AI Agents
In api/src/authorization/authorization.service.ts at line 44, the code calls
user.roles.includes(key) but user.roles is an array of Role objects, not
strings, so the includes check will fail. To fix this, map user.roles to an
array of role ID strings before the filter call, ensuring you compare key
against a string array of role IDs rather than Role objects.

@yassinedorbozgithub yassinedorbozgithub changed the title feat(api): add Authorization Module feat(api): add Authorization Module [main<-1209] Jul 8, 2025
@yassinedorbozgithub yassinedorbozgithub added needs-review security Vulnerabilities, exploits, sensitive data labels Jul 8, 2025
Comment on lines +74 to 82
try {
return await this.authorizationService.canAccess(
method,
user.id,
targetModel,
);
} catch (e) {
throw new NotFoundException('Failed to load permissions');
}
Copy link
Contributor

@medchedli medchedli Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we use throw ForbiddenException if canAccess returns false

Suggested change
try {
return await this.authorizationService.canAccess(
method,
user.id,
targetModel,
);
} catch (e) {
throw new NotFoundException('Failed to load permissions');
}
try {
const accessGranted = await this.authorizationService.canAccess(
method,
user.id,
targetModel,
);
if (!accessGranted) {
throw new ForbiddenException(
'You do not have permission to access this resource',
);
}
return true;
} catch (e) {
throw new InternalServerErrorException(
'An unexpected error occurred during authorization',
);
}

targetModel: TModel,
): Promise<boolean> {
try {
const user = await this.userService.findOne(userId);
Copy link
Contributor

@medchedli medchedli Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to use findOneAndPopulate because we only use the role IDs for permission checks.

const permissionsFromRoles = Object.entries(permissions)
.filter(([key, _]) => user.roles.includes(key))
.map(([_, value]) => value);
const targetModel = _parsedUrl?.pathname?.split('/')[1].toLowerCase() as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we tackle this in a different PR, but it's worth mentioning that we can improve the curent implementation by using the built-in @SetMetadata() decorator from NestJS. This decorator will allow us to attach custom metadata to our controller routes to indicate the database model associated with the route and also the required user roles or permissions needed to access it. https://docs.nestjs.com/fundamentals/execution-context#reflection-and-metadata

@yassinedorbozgithub yassinedorbozgithub marked this pull request as draft July 8, 2025 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request security Vulnerabilities, exploits, sensitive data

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🤔 [ISSUE] - Add Access Control Middleware for WebSocket Endpoints

2 participants