feat: user permissions handlers & authentication hydration and routing#681
feat: user permissions handlers & authentication hydration and routing#681
Conversation
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.
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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> }
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
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_bitfieldis missing webhook permissions.The
all_permissionsarray is missingManageWebhooks,QueryWebhooks, andViewWebhooks(defined at lines 34-36), which means roundtripping throughfrom_bitfieldwill silently drop these permissions. This affects the newget_user_permissionsfeature 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
HashSettoVecviainto_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_idbelongs to a user in the specified realm. Sinceget_user_rolesreturns an empty vector for non-existent users (viaINNER 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 ofuser_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?;
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub roles: Option<Vec<Role>>, |
There was a problem hiding this comment.
🧩 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 -50Repository: 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=20Repository: 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 -20Repository: 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 -B2Repository: ferriskey/ferriskey
Length of output: 89
🏁 Script executed:
# Search for roles field usage without type flags
rg -n '\.roles' --max-count=30Repository: ferriskey/ferriskey
Length of output: 266
🏁 Script executed:
# Check how roles are assigned/initialized
rg -n 'roles\s*[:=]' --max-count=30Repository: 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 -5Repository: 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 -10Repository: ferriskey/ferriskey
Length of output: 591
🏁 Script executed:
# View the mapper file where roles: None is set
cat -n core/src/infrastructure/user/mappers.rsRepository: 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/srcRepository: 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=20Repository: 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=15Repository: 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=20Repository: 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.
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
/permissionsendpoint 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
GET /realms/{realm_name}/users/{user_id}/permissionsto retrieve a user's permissions within a specific realm, including request handler, routing, API documentation, and service/policy logic.Userentity to makerolesan optional field, and adjusted related code and mappers to reflect this change.GetUserPermissionsInputstruct for service and policy layers to support the new endpoint.Frontend: Authentication Hydration and Routing
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
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.