Skip to content

Comments

feat: user permissions handlers & authentication hydration and routing#681

Merged
NathaelB merged 4 commits intomainfrom
feat/user-permissions-handlers
Jan 27, 2026
Merged

feat: user permissions handlers & authentication hydration and routing#681
NathaelB merged 4 commits intomainfrom
feat/user-permissions-handlers

Conversation

@NathaelB
Copy link
Member

@NathaelB NathaelB commented Jan 27, 2026

This pull request introduces a new API endpoint to retrieve a user's permissions and improves the handling of user roles and authentication state throughout the backend and frontend. The most significant changes include the addition of the /permissions endpoint for users, updates to the user entity to make roles optional, and enhancements to authentication hydration and routing logic in the frontend.

Backend: User Permissions Endpoint and Role Handling

  • Added a new API endpoint GET /realms/{realm_name}/users/{user_id}/permissions to retrieve a user's permissions within a specific realm, including request handler, routing, API documentation, and service/policy logic.
  • Updated the User entity to make roles an optional field, and adjusted related code and mappers to reflect this change.
  • Added GetUserPermissionsInput struct for service and policy layers to support the new endpoint.

Frontend: Authentication Hydration and Routing

  • Improved authentication state handling by ensuring the app waits for hydration before redirecting users or checking authentication, preventing premature redirects and errors.
  • Updated routing logic to consistently use the current or default realm for authentication and overview redirects, and improved fallback routes for better user experience.

These changes collectively enhance the API's capabilities for managing user permissions and improve the frontend's robustness in handling authentication state and navigation.

Summary by CodeRabbit

  • New Features

    • Added an endpoint to retrieve a user’s permissions within a realm.
    • Frontend: hydration-aware auth flow and dynamic default-realm routing with improved loading states.
    • Access control: new permission checks for viewing user permissions.
  • Bug Fixes

    • Replaced hardcoded realm references with dynamic routing.
    • User roles now optional and may be omitted when unset (affects serialization/visibility).

✏️ Tip: You can customize this high-level summary in your review settings.

Introduce defaultRealm (realm_name ?? 'master') and refactor App routing
to
use it. Replace the catch-all Navigate with conditional redirects that
show a
spinner while loading. In use-auth add hasHydrated state and subscribe
to
authStore.persist.onFinishHydration to defer auth checks until
persistence
hydration completes; update redirects to include realm_name and adjust
effect
dependencies.
Expose GET /realms/{realm_name}/users/{user_id}/permissions and add
handler
with OpenAPI docs. Add UserService#get_user_permissions and
ApplicationService
forwarding. Implement core logic to resolve realm, authorize via a new
policy
(can_view_user_permissions), aggregate unique permissions from user
roles and
return them. Make User.roles optional and update test builder and DB
mappers.
Add Permissions ToSchema for API documentation.
@NathaelB NathaelB requested a review from LeadcodeDev January 27, 2026 07:43
@NathaelB NathaelB self-assigned this Jan 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds a user-permissions retrieval feature: domain input and service method, policy checks, entity adjustments (roles -> Option<Vec>), an HTTP GET endpoint with OpenAPI docs, infrastructure mapper updates, and frontend navigation/hydration updates for realm-aware flows.

Changes

Cohort / File(s) Summary
User Domain Entity Changes
core/src/domain/user/entities.rs, api/src/application/http/test.rs
User.roles changed from Vec<Role> to Option<Vec<Role>> with skip_serializing_if; User::new and test builders updated to use Some(...); added GetUserPermissionsInput.
User Service & Domain Logic
core/src/domain/user/services.rs, core/src/domain/user/ports.rs, core/src/application/user/mod.rs
Added get_user_permissions input/type and service method; updated UserService/UserPolicy traits; logic includes realm resolution, policy checks, target-user validation, permission derivation and deduplication.
Policies Implementation
core/src/domain/user/policies.rs
Added can_view_user_permissions policy method that validates requester vs target and checks resolved permissions (ManageRealm, ManageUsers, ViewUsers).
Infrastructure Mapping
core/src/infrastructure/user/mappers.rs
Mapper changed to initialize roles as None when converting DB model to domain User.
Permission Schema
core/src/domain/role/entities/permission.rs
Added utoipa::ToSchema derive for Permissions enum.
HTTP Handler & Router
api/src/application/http/user/handlers/get_user_permissions.rs, api/src/application/http/user/handlers.rs, api/src/application/http/user/router.rs
New GET handler /{realm_name}/{user_id}/permissions returning UserPermissionsResponse, route registration, and OpenAPI path entry; handler exported in handlers module.
Test Adjustments
api/src/application/http/test.rs
Updated test user builder to wrap roles in Some(...).
Frontend: Navigation & Auth Hydration
front/src/App.tsx, front/src/hooks/use-auth.ts
Added defaultRealm fallback, replaced hardcoded master realm paths with realm-aware routes, and added hydration-aware gating (hasHydrated) to auth hook logic to delay actions until persistence hydration completes.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HTTPHandler as HTTP Handler
    participant AppService as Application Service
    participant DomainService as Domain User Service
    participant PolicyEngine as Policy Engine
    participant Database as Database

    Client->>HTTPHandler: GET /realms/{realm}/users/{user_id}/permissions
    HTTPHandler->>AppService: get_user_permissions(identity, input)
    AppService->>DomainService: get_user_permissions(identity, input)
    DomainService->>Database: resolve realm by name
    Database-->>DomainService: Realm
    DomainService->>PolicyEngine: can_view_user_permissions(identity, realm, user_id)
    PolicyEngine->>Database: load requesting user
    Database-->>PolicyEngine: User
    PolicyEngine-->>DomainService: bool (authorized)
    DomainService->>Database: fetch target user & roles
    Database-->>DomainService: User (with roles)
    DomainService->>DomainService: derive & deduplicate permissions
    DomainService-->>AppService: Vec<Permissions>
    AppService-->>HTTPHandler: Vec<Permissions>
    HTTPHandler->>Client: 200 OK { data: Vec<Permissions> }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped through realms to peek and see,
Collected roles and policed who may be.
I checked each permission, small and grand,
Returned the list, paw prints in the sand.
Hooray — permissions fetched by rabbit hand!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the two main changes: adding user permissions handlers and improving authentication hydration and routing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/src/domain/role/entities/permission.rs (1)

40-61: Pre-existing bug: from_bitfield is missing webhook permissions.

The all_permissions array is missing ManageWebhooks, QueryWebhooks, and ViewWebhooks (defined at lines 34-36), which means roundtripping through from_bitfield will silently drop these permissions. This affects the new get_user_permissions feature if any roles include webhook permissions.

🐛 Proposed fix
         let all_permissions = [
             Self::CreateClient,
             Self::ManageAuthorization,
             Self::ManageClients,
             Self::ManageEvents,
             Self::ManageIdentityProviders,
             Self::ManageRealm,
             Self::ManageUsers,
             Self::ManageRoles,
             Self::QueryClients,
             Self::QueryGroups,
             Self::QueryRealms,
             Self::QueryUsers,
             Self::ViewAuthorization,
             Self::ViewClients,
             Self::ViewEvents,
             Self::ViewIdentityProviders,
             Self::ViewRealm,
             Self::ViewUsers,
             Self::ViewRoles,
+            Self::ManageWebhooks,
+            Self::QueryWebhooks,
+            Self::ViewWebhooks,
         ];
🤖 Fix all issues with AI agents
In `@core/src/domain/user/entities.rs`:
- Around line 24-25: The user entity's roles field was changed to
Option<Vec<Role>> with skip_serializing_if, and the user mapper sets roles: None
so JSON omits the field; either (A) update the mapper (in the user mapper that
assigns roles: None) to always supply Some(Vec::new()) or actual roles (e.g.,
roles: Some(vec![])) so the backend continues to emit roles as an empty array,
or (B) revert the entity field to Vec<Role> (remove Option and the
skip_serializing_if) so roles remain non-optional; choose one approach and apply
it consistently (update any code that constructs User in the mapper and ensure
serde attribute matches the decided type).

In `@core/src/domain/user/policies.rs`:
- Around line 95-117: The can_view_user_permissions function currently checks
for ManageRealm and ManageUsers only; align it with can_view_user by also
allowing ViewUsers for read-only access or explicitly document the intent.
Update can_view_user_permissions (in the async fn can_view_user_permissions) to
include Permissions::ViewUsers in the Permissions::has_one_of_permissions call,
or add a clear comment above the function explaining why ViewUsers is
intentionally excluded and that reviewing permissions requires elevated rights.
🧹 Nitpick comments (3)
api/src/application/http/user/handlers/get_user_permissions.rs (1)

24-38: Consider adding 401 Unauthorized response to OpenAPI documentation.

The endpoint requires authentication (protected by auth middleware), so it can return a 401 Unauthorized response. Adding this to the OpenAPI documentation would provide more complete API documentation for consumers.

📝 Suggested OpenAPI enhancement
     responses(
         (status = 200, body = UserPermissionsResponse, description = "User permissions retrieved successfully"),
+        (status = 401, description = "Unauthorized: Authentication required"),
         (status = 404, description = "User not found"),
         (status = 403, description = "Forbidden: User does not have permission to access this user's permissions")
     )
core/src/domain/user/services.rs (2)

575-580: Note: Permission order is non-deterministic.

Converting from HashSet to Vec via into_iter().collect() produces a non-deterministic order. This is generally fine for permissions, but if API consumers rely on consistent ordering for testing or display, consider sorting:

let mut permissions: Vec<Permissions> = permissions.into_iter().collect();
permissions.sort();
Ok(permissions)

This is a minor consideration and may not be necessary depending on client requirements.


552-581: Add user existence validation to prevent information disclosure through response patterns.

The implementation retrieves permissions without verifying that the user_id belongs to a user in the specified realm. Since get_user_roles returns an empty vector for non-existent users (via INNER JOIN), the endpoint returns an empty permissions list—indistinguishable from a user with no roles. This allows callers to infer user existence based on response patterns, creating minor information disclosure even with the policy check in place.

The pattern is already established in this codebase: UserRoleServiceImpl::assign_role (line 84–88 of user_role_service.rs) validates user existence and realm membership before operating on roles. Apply the same pattern here.

💡 Suggested validation
         ensure_policy(
             self.policy
                 .can_view_user_permissions(&identity, &realm, input.user_id)
                 .await,
             "insufficient permissions",
         )?;
 
+        // Verify user exists in realm
+        let user = self.user_repository.get_by_id(input.user_id).await?;
+        if user.realm_id != realm.id {
+            return Err(CoreError::InvalidRealm);
+        }
+
         let roles = self
             .user_role_repository
             .get_user_roles(input.user_id)
             .await?;

Comment on lines +24 to +25
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<Role>>,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual file to understand the full context
cat -n core/src/domain/user/entities.rs | head -50

Repository: ferriskey/ferriskey

Length of output: 1845


🏁 Script executed:

# Search for how the roles field is used in the frontend
rg -n --type=ts --type=tsx '\.roles' --max-count=20

Repository: ferriskey/ferriskey

Length of output: 90


🏁 Script executed:

# Check for roles usage in mappers and other backend code
rg -n 'roles.*None\|roles.*Some' core/ --type=rs | head -20

Repository: ferriskey/ferriskey

Length of output: 89


🏁 Script executed:

# Check User initialization to see how roles is typically set
rg -n 'roles:' core/src/domain/user/ --type=rs -A2 -B2

Repository: ferriskey/ferriskey

Length of output: 89


🏁 Script executed:

# Search for roles field usage without type flags
rg -n '\.roles' --max-count=30

Repository: ferriskey/ferriskey

Length of output: 266


🏁 Script executed:

# Check how roles are assigned/initialized
rg -n 'roles\s*[:=]' --max-count=30

Repository: ferriskey/ferriskey

Length of output: 7469


🏁 Script executed:

# Look at the mappers file mentioned in the scratchpad to see how roles is handled
fd -e rs mappers | head -5

Repository: ferriskey/ferriskey

Length of output: 281


🏁 Script executed:

# Check if there's a frontend directory and how it uses roles
find . -type d -name "front*" -o -name "web*" -o -name "client*" | head -10

Repository: ferriskey/ferriskey

Length of output: 591


🏁 Script executed:

# View the mapper file where roles: None is set
cat -n core/src/infrastructure/user/mappers.rs

Repository: ferriskey/ferriskey

Length of output: 1444


🏁 Script executed:

# Check if frontend types have been updated recently or if they still expect non-optional roles
rg -n 'roles\s*[?:]\s*(Role|Array)' front/src

Repository: ferriskey/ferriskey

Length of output: 357


🏁 Script executed:

# Check if roles are populated elsewhere after the initial mapping
rg -n 'roles\s*=\s*Some' core/src --max-count=20

Repository: ferriskey/ferriskey

Length of output: 45


🏁 Script executed:

# Look for any conditional logic or middleware that might populate roles based on endpoint
rg -n 'get_user_roles\|GetUserRoles' api/src core/src --max-count=15

Repository: ferriskey/ferriskey

Length of output: 45


🏁 Script executed:

# Check if frontend has been updated to handle optional roles in actual usage
rg -n 'roles\?' front/src --max-count=20

Repository: ferriskey/ferriskey

Length of output: 245


Breaking change: roles field is now optional and conditionally serialized.

Changing roles from Vec<Role> to Option<Vec<Role>> with skip_serializing_if = "Option::is_none" is a breaking change for API consumers. The mapper at core/src/infrastructure/user/mappers.rs:20 explicitly sets roles: None, which means the field will be completely omitted from JSON responses. However, frontend type definitions still expect roles: Role[] (non-optional) across multiple files (api.client.ts, core.interface.ts, home.types.ts). This mismatch will cause frontend code expecting the roles field to fail.

Update frontend type definitions to make roles optional (roles?: Role[]) or change the backend behavior to always populate and serialize the field.

🤖 Prompt for AI Agents
In `@core/src/domain/user/entities.rs` around lines 24 - 25, The user entity's
roles field was changed to Option<Vec<Role>> with skip_serializing_if, and the
user mapper sets roles: None so JSON omits the field; either (A) update the
mapper (in the user mapper that assigns roles: None) to always supply
Some(Vec::new()) or actual roles (e.g., roles: Some(vec![])) so the backend
continues to emit roles as an empty array, or (B) revert the entity field to
Vec<Role> (remove Option and the skip_serializing_if) so roles remain
non-optional; choose one approach and apply it consistently (update any code
that constructs User in the mapper and ensure serde attribute matches the
decided type).

Replace direct role aggregation with policy lookup via
get_permission_for_target_realm. Fetch the user and validate the user's
realm exists and matches the target realm (or is "master") to prevent
cross-realm access. Remove the user_role_repository role parsing logic.
@NathaelB NathaelB merged commit 9bcd405 into main Jan 27, 2026
9 checks passed
@NathaelB NathaelB deleted the feat/user-permissions-handlers branch January 27, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants