Skip to main content

auth_framework/api/
rbac_endpoints.rs

1//! RBAC API endpoints using role-system v1.0
2//!
3//! This module provides comprehensive REST API endpoints for role and permission
4//! management, leveraging the enhanced authorization service.
5
6use crate::api::{ApiResponse, ApiState};
7use crate::tokens::AuthToken;
8use axum::{
9    Extension,
10    extract::{Path, Query, State},
11    http::StatusCode,
12    response::Json,
13};
14use role_system::Permission;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use tracing::debug;
18use tracing::{info, warn};
19
20/// Request to create a new role
21#[derive(Debug, Deserialize)]
22pub struct CreateRoleRequest {
23    pub name: String,
24    pub description: Option<String>,
25    pub parent_id: Option<String>,
26    pub permissions: Option<Vec<String>>,
27}
28
29/// Request to update an existing role
30#[derive(Debug, Deserialize)]
31pub struct UpdateRoleRequest {
32    pub name: Option<String>,
33    pub description: Option<String>,
34    pub parent_id: Option<String>,
35}
36
37/// Request to create a new permission
38#[derive(Debug, Deserialize)]
39pub struct CreatePermissionRequest {
40    pub action: String,
41    pub resource: String,
42    pub conditions: Option<HashMap<String, String>>,
43}
44
45/// Request to assign a role to a user
46#[derive(Debug, Deserialize)]
47pub struct AssignRoleRequest {
48    pub role_id: String,
49    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
50    pub reason: Option<String>,
51}
52
53/// Request to bulk assign roles
54#[derive(Debug, Deserialize)]
55pub struct BulkAssignRequest {
56    pub assignments: Vec<BulkAssignment>,
57}
58
59#[derive(Debug, Deserialize)]
60pub struct BulkAssignment {
61    pub user_id: String,
62    pub role_id: String,
63    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
64}
65
66/// Request for role elevation
67#[derive(Debug, Deserialize)]
68pub struct ElevateRoleRequest {
69    pub target_role: String,
70    pub duration_minutes: Option<u32>,
71    pub justification: String,
72}
73
74/// Response with role information
75#[derive(Debug, Serialize)]
76pub struct RoleResponse {
77    pub id: String,
78    pub name: String,
79    pub description: Option<String>,
80    pub parent_id: Option<String>,
81    pub permissions: Vec<String>,
82    pub created_at: chrono::DateTime<chrono::Utc>,
83    pub updated_at: chrono::DateTime<chrono::Utc>,
84}
85
86/// Response with permission information
87#[derive(Debug, Serialize)]
88pub struct PermissionResponse {
89    pub id: String,
90    pub action: String,
91    pub resource: String,
92    pub conditions: Option<HashMap<String, String>>,
93    pub created_at: chrono::DateTime<chrono::Utc>,
94}
95
96/// Response for user role assignments
97#[derive(Debug, Serialize)]
98pub struct UserRolesResponse {
99    pub user_id: String,
100    pub roles: Vec<UserRole>,
101    pub effective_permissions: Vec<String>,
102}
103
104#[derive(Debug, Serialize)]
105pub struct UserRole {
106    pub role_id: String,
107    pub role_name: String,
108    pub assigned_at: chrono::DateTime<chrono::Utc>,
109    pub assigned_by: Option<String>,
110    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
111}
112
113/// Audit log entry response
114#[derive(Debug, Serialize)]
115pub struct AuditLogResponse {
116    pub entries: Vec<AuditEntryResponse>,
117    pub total_count: u64,
118    pub page: u32,
119    pub per_page: u32,
120}
121
122#[derive(Debug, Serialize)]
123pub struct AuditEntryResponse {
124    pub id: String,
125    pub user_id: Option<String>,
126    pub action: String,
127    pub resource: Option<String>,
128    pub result: String,
129    pub context: HashMap<String, String>,
130    pub timestamp: chrono::DateTime<chrono::Utc>,
131    pub description: String,
132}
133
134/// Query parameters for listing roles
135#[derive(Debug, Deserialize)]
136pub struct RoleListQuery {
137    pub page: Option<u32>,
138    pub per_page: Option<u32>,
139    pub parent_id: Option<String>,
140    pub include_permissions: Option<bool>,
141}
142
143/// Query parameters for audit logs
144#[derive(Debug, Deserialize)]
145pub struct AuditQuery {
146    pub user_id: Option<String>,
147    pub action: Option<String>,
148    pub resource: Option<String>,
149    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
150    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
151    pub page: Option<u32>,
152    pub per_page: Option<u32>,
153}
154
155/// Permission check request
156#[derive(Debug, Deserialize)]
157pub struct PermissionCheckRequest {
158    pub action: String,
159    pub resource: String,
160    pub context: Option<HashMap<String, String>>,
161}
162
163/// Permission check response
164#[derive(Debug, Serialize)]
165pub struct PermissionCheckResponse {
166    pub granted: bool,
167    pub reason: String,
168    pub required_roles: Vec<String>,
169    pub missing_permissions: Vec<String>,
170}
171
172// ============================================================================
173// ROLE MANAGEMENT ENDPOINTS
174// ============================================================================
175
176/// Create a new role
177/// POST /api/v1/rbac/roles
178pub async fn create_role(
179    State(state): State<ApiState>,
180    Extension(auth_token): Extension<AuthToken>,
181    Json(request): Json<CreateRoleRequest>,
182) -> Result<Json<ApiResponse<RoleResponse>>, StatusCode> {
183    // Check authorization
184    if !auth_token.has_permission("manage:roles") {
185        return Ok(Json(
186            ApiResponse::<RoleResponse>::forbidden_with_message_typed(
187                "Insufficient permissions to manage roles",
188            ),
189        ));
190    }
191
192    let now = chrono::Utc::now();
193
194    // Convert string permissions to Permission objects
195    let permissions: Vec<Permission> = request
196        .permissions
197        .unwrap_or_default()
198        .into_iter()
199        .filter_map(|perm_str| {
200            // Try to parse as "action:resource" format
201            if let Some((action, resource)) = perm_str.split_once(':') {
202                Some(Permission::new(action, resource))
203            } else {
204                warn!("Invalid permission format: {}", perm_str);
205                None
206            }
207        })
208        .collect();
209
210    match state
211        .authorization_service
212        .create_role(
213            &request.name,
214            &request.description.unwrap_or_default(),
215            permissions,
216            request.parent_id.map(|p| vec![p]),
217        )
218        .await
219    {
220        Ok(_) => {
221            info!("Role created: {} by {}", request.name, auth_token.user_id);
222
223            // Fetch the created role to get complete info
224            match state.authorization_service.get_role(&request.name).await {
225                Ok(Some(role)) => {
226                    // Convert PermissionSet to vector of "action:resource" strings
227                    let permissions_strings: Vec<String> = role
228                        .permissions()
229                        .permissions()
230                        .iter()
231                        .map(|p| format!("{}:{}", p.action(), p.resource_type()))
232                        .collect();
233
234                    // Test additional hierarchy methods from role-system v1.1.1
235                    let hierarchy_depth = role.hierarchy_depth();
236                    let is_root = role.is_root_role();
237                    let is_leaf = role.is_leaf_role();
238                    let child_ids = role.child_role_ids();
239
240                    debug!(
241                        "Role '{}' - Depth: {}, Root: {}, Leaf: {}, Children: {:?}",
242                        role.name(),
243                        hierarchy_depth,
244                        is_root,
245                        is_leaf,
246                        child_ids
247                    );
248
249                    let response = RoleResponse {
250                        id: role.id().to_string(),
251                        name: role.name().to_string(),
252                        description: role.description().map(|s| s.to_string()),
253                        parent_id: role.parent_role_id().map(|s| s.to_string()), // Now available in role-system v1.1.1!
254                        permissions: permissions_strings,
255                        created_at: now,
256                        updated_at: now,
257                    };
258
259                    Ok(Json(ApiResponse::success(response)))
260                }
261                _ => Ok(Json(ApiResponse::<RoleResponse>::error_with_message_typed(
262                    "ROLE_FETCH_FAILED",
263                    "Role created but failed to fetch details",
264                ))),
265            }
266        }
267        Err(e) => {
268            warn!("Failed to create role: {}", e);
269            Ok(Json(ApiResponse::<RoleResponse>::error_with_message_typed(
270                "ROLE_CREATION_FAILED",
271                "Failed to create role",
272            )))
273        }
274    }
275}
276
277/// Get role by ID
278/// GET /api/v1/rbac/roles/{role_id}
279pub async fn get_role(
280    State(state): State<ApiState>,
281    Extension(auth_token): Extension<AuthToken>,
282    Path(role_id): Path<String>,
283) -> Result<Json<ApiResponse<RoleResponse>>, StatusCode> {
284    // Check authorization
285    if !auth_token.has_permission("read:roles") {
286        return Ok(Json(
287            ApiResponse::<RoleResponse>::forbidden_with_message_typed(
288                "Insufficient permissions to read roles",
289            ),
290        ));
291    }
292
293    match state.authorization_service.get_role(&role_id).await {
294        Ok(Some(role)) => {
295            let permissions_strings: Vec<String> = role
296                .permissions()
297                .permissions()
298                .iter()
299                .map(|p| format!("{}:{}", p.action(), p.resource_type()))
300                .collect();
301
302            let response = RoleResponse {
303                id: role.id().to_string(),
304                name: role.name().to_string(),
305                description: role.description().map(|s| s.to_string()),
306                parent_id: role.parent_role_id().map(|s| s.to_string()), // Now available in role-system v1.1.1!
307                permissions: permissions_strings,
308                created_at: chrono::Utc::now(), // Would come from storage in real implementation
309                updated_at: chrono::Utc::now(),
310            };
311
312            Ok(Json(ApiResponse::success(response)))
313        }
314        Ok(None) => Ok(Json(
315            ApiResponse::<RoleResponse>::not_found_with_message_typed("Role not found"),
316        )),
317        Err(e) => {
318            warn!("Failed to get role: {}", e);
319            Ok(Json(ApiResponse::<RoleResponse>::error_with_message_typed(
320                "ROLE_FETCH_FAILED",
321                "Failed to fetch role",
322            )))
323        }
324    }
325}
326
327/// List roles with pagination
328/// GET /api/v1/rbac/roles
329pub async fn list_roles(
330    State(state): State<ApiState>,
331    Extension(auth_token): Extension<AuthToken>,
332    Query(_query): Query<RoleListQuery>,
333) -> Result<Json<ApiResponse<Vec<RoleResponse>>>, StatusCode> {
334    // Check authorization
335    if !auth_token.has_permission("read:roles") {
336        return Ok(Json(
337            ApiResponse::<Vec<RoleResponse>>::forbidden_with_message_typed(
338                "Insufficient permissions to read roles",
339            ),
340        ));
341    }
342
343    match state.authorization_service.list_roles().await {
344        Ok(roles) => {
345            let now = chrono::Utc::now();
346            let response: Vec<RoleResponse> = roles
347                .into_iter()
348                .map(|role| {
349                    let permissions_strings: Vec<String> = role
350                        .permissions()
351                        .permissions()
352                        .iter()
353                        .map(|p| format!("{}:{}", p.action(), p.resource_type()))
354                        .collect();
355                    RoleResponse {
356                        id: role.id().to_string(),
357                        name: role.name().to_string(),
358                        description: role.description().map(|s| s.to_string()),
359                        parent_id: role.parent_role_id().map(|s| s.to_string()),
360                        permissions: permissions_strings,
361                        created_at: now,
362                        updated_at: now,
363                    }
364                })
365                .collect();
366            Ok(Json(ApiResponse::success(response)))
367        }
368        Err(e) => {
369            warn!("Failed to list roles: {}", e);
370            Ok(Json(
371                ApiResponse::<Vec<RoleResponse>>::error_with_message_typed(
372                    "ROLE_LIST_FAILED",
373                    "Failed to retrieve role list",
374                ),
375            ))
376        }
377    }
378}
379
380/// Update role
381/// PUT /api/v1/rbac/roles/{role_id}
382pub async fn update_role(
383    State(state): State<ApiState>,
384    Extension(auth_token): Extension<AuthToken>,
385    Path(role_id): Path<String>,
386    Json(request): Json<UpdateRoleRequest>,
387) -> Result<Json<ApiResponse<RoleResponse>>, StatusCode> {
388    // Check authorization
389    if !auth_token.has_permission("manage:roles") {
390        return Ok(Json(
391            ApiResponse::<RoleResponse>::forbidden_with_message_typed(
392                "Insufficient permissions to manage roles",
393            ),
394        ));
395    }
396
397    // The parent_id field in UpdateRoleRequest is Option<String>, so:
398    //  - None  => don't change the parent
399    //  - Some(id) => set parent to id
400    // There is currently no way to clear a parent via this endpoint; a dedicated
401    // delete-parent path would be needed for that use case.
402    let new_parent: Option<Option<&str>> = request.parent_id.as_deref().map(Some);
403
404    match state
405        .authorization_service
406        .update_role(&role_id, request.description.as_deref(), new_parent)
407        .await
408    {
409        Ok(()) => {
410            info!("Role {} updated by {}", role_id, auth_token.user_id);
411            // Return the updated role details
412            match state.authorization_service.get_role(&role_id).await {
413                Ok(Some(role)) => {
414                    let now = chrono::Utc::now();
415                    let permissions_strings: Vec<String> = role
416                        .permissions()
417                        .permissions()
418                        .iter()
419                        .map(|p| format!("{}:{}", p.action(), p.resource_type()))
420                        .collect();
421                    Ok(Json(ApiResponse::success(RoleResponse {
422                        id: role.id().to_string(),
423                        name: role.name().to_string(),
424                        description: role.description().map(|s| s.to_string()),
425                        parent_id: role.parent_role_id().map(|s| s.to_string()),
426                        permissions: permissions_strings,
427                        created_at: now,
428                        updated_at: now,
429                    })))
430                }
431                _ => Ok(Json(ApiResponse::<RoleResponse>::error_with_message_typed(
432                    "ROLE_FETCH_FAILED",
433                    "Role updated but failed to fetch details",
434                ))),
435            }
436        }
437        Err(e) => {
438            warn!("Failed to update role {}: {}", role_id, e);
439            Ok(Json(ApiResponse::<RoleResponse>::error_with_message_typed(
440                "ROLE_UPDATE_FAILED",
441                "Failed to update role",
442            )))
443        }
444    }
445}
446
447/// Delete role
448/// DELETE /api/v1/rbac/roles/{role_id}
449pub async fn delete_role(
450    State(state): State<ApiState>,
451    Extension(auth_token): Extension<AuthToken>,
452    Path(role_id): Path<String>,
453) -> Result<Json<ApiResponse<()>>, StatusCode> {
454    // Check authorization
455    if !auth_token.has_permission("manage:roles") {
456        return Ok(Json(ApiResponse::forbidden_typed()));
457    }
458
459    match state.authorization_service.delete_role(&role_id).await {
460        Ok(_) => {
461            info!("Role deleted: {} by {}", role_id, auth_token.user_id);
462            Ok(Json(ApiResponse::success(())))
463        }
464        Err(e) => {
465            warn!("Failed to delete role {}: {}", role_id, e);
466            Ok(Json(ApiResponse::<()>::error_typed(
467                "ROLE_DELETE_FAILED",
468                "Failed to delete role",
469            )))
470        }
471    }
472}
473
474// ============================================================================
475// USER ROLE ASSIGNMENT ENDPOINTS
476// ============================================================================
477
478/// Assign role to user
479/// POST /api/v1/rbac/users/{user_id}/roles
480pub async fn assign_user_role(
481    State(state): State<ApiState>,
482    Extension(auth_token): Extension<AuthToken>,
483    Path(user_id): Path<String>,
484    Json(request): Json<AssignRoleRequest>,
485) -> Result<Json<ApiResponse<()>>, StatusCode> {
486    // Check authorization
487    if !auth_token.has_permission("manage:user_roles") {
488        return Ok(Json(ApiResponse::<()>::forbidden_with_message_typed(
489            "Insufficient permissions to manage user roles",
490        )));
491    }
492
493    match state
494        .authorization_service
495        .assign_role(&user_id, &request.role_id)
496        .await
497    {
498        Ok(_) => {
499            info!(
500                "Role {} assigned to user {} by {}",
501                request.role_id, user_id, auth_token.user_id
502            );
503            Ok(Json(ApiResponse::success(())))
504        }
505        Err(e) => {
506            warn!("Failed to assign role: {}", e);
507            Ok(Json(ApiResponse::<()>::error_with_message_typed(
508                "ROLE_ASSIGNMENT_FAILED",
509                "Failed to assign role",
510            )))
511        }
512    }
513}
514
515/// Revoke role from user
516/// DELETE /api/v1/rbac/users/{user_id}/roles/{role_id}
517pub async fn revoke_user_role(
518    State(state): State<ApiState>,
519    Extension(auth_token): Extension<AuthToken>,
520    Path((user_id, role_id)): Path<(String, String)>,
521) -> Result<Json<ApiResponse<()>>, StatusCode> {
522    // Check authorization
523    if !auth_token.has_permission("manage:user_roles") {
524        return Ok(Json(ApiResponse::<()>::forbidden_with_message_typed(
525            "Insufficient permissions to manage user roles",
526        )));
527    }
528
529    match state
530        .authorization_service
531        .remove_role(&user_id, &role_id)
532        .await
533    {
534        Ok(_) => {
535            info!(
536                "Role {} revoked from user {} by {}",
537                role_id, user_id, auth_token.user_id
538            );
539            Ok(Json(ApiResponse::success(())))
540        }
541        Err(e) => {
542            warn!("Failed to revoke role: {}", e);
543            Ok(Json(ApiResponse::<()>::error_with_message_typed(
544                "ROLE_REVOCATION_FAILED",
545                "Failed to revoke role",
546            )))
547        }
548    }
549}
550
551/// Get user roles
552/// GET /api/v1/rbac/users/{user_id}/roles
553pub async fn get_user_roles(
554    State(state): State<ApiState>,
555    Extension(auth_token): Extension<AuthToken>,
556    Path(user_id): Path<String>,
557) -> Result<Json<ApiResponse<UserRolesResponse>>, StatusCode> {
558    // Check authorization - users can view their own roles, or need read:user_roles permission
559    if user_id != auth_token.user_id && !auth_token.has_permission("read:user_roles") {
560        return Ok(Json(
561            ApiResponse::<UserRolesResponse>::forbidden_with_message_typed(
562                "Insufficient permissions to read user roles",
563            ),
564        ));
565    }
566
567    match state.authorization_service.get_user_roles(&user_id).await {
568        Ok(role_names) => {
569            let roles: Vec<UserRole> = role_names
570                .into_iter()
571                .map(|name| UserRole {
572                    role_id: name.clone(),
573                    role_name: name,
574                    // The in-memory role store does not persist assignment timestamps.
575                    // Use Unix epoch (zero) as a sentinel value meaning "unknown".
576                    assigned_at: chrono::DateTime::from_timestamp(0, 0)
577                        .unwrap_or_else(chrono::Utc::now),
578                    assigned_by: None,
579                    expires_at: None,
580                })
581                .collect();
582
583            // If this is the calling user, include their effective permissions
584            let effective_permissions: Vec<String> = if user_id == auth_token.user_id {
585                auth_token.permissions.to_vec()
586            } else {
587                vec![]
588            };
589
590            let response = UserRolesResponse {
591                user_id,
592                roles,
593                effective_permissions,
594            };
595
596            Ok(Json(ApiResponse::success(response)))
597        }
598        Err(e) => {
599            warn!("Failed to get roles for user {}: {}", user_id, e);
600            Ok(Json(
601                ApiResponse::<UserRolesResponse>::error_with_message_typed(
602                    "ROLE_FETCH_FAILED",
603                    "Failed to retrieve user roles",
604                ),
605            ))
606        }
607    }
608}
609
610/// Bulk assign roles
611/// POST /api/v1/rbac/bulk/assign
612pub async fn bulk_assign_roles(
613    State(state): State<ApiState>,
614    Extension(auth_token): Extension<AuthToken>,
615    Json(request): Json<BulkAssignRequest>,
616) -> Result<Json<ApiResponse<()>>, StatusCode> {
617    // Check authorization
618    if !auth_token.has_permission("manage:user_roles") {
619        return Ok(Json(ApiResponse::<()>::forbidden_with_message_typed(
620            "Insufficient permissions to manage user roles",
621        )));
622    }
623
624    // Process assignments one by one since we don't have batch operations
625    let mut success_count = 0;
626    let mut error_count = 0;
627
628    for assignment in request.assignments {
629        match state
630            .authorization_service
631            .assign_role(&assignment.user_id, &assignment.role_id)
632            .await
633        {
634            Ok(_) => success_count += 1,
635            Err(e) => {
636                warn!(
637                    "Failed to assign role {} to user {}: {}",
638                    assignment.role_id, assignment.user_id, e
639                );
640                error_count += 1;
641            }
642        }
643    }
644
645    info!(
646        "Bulk role assignment completed by {} - {} successes, {} errors",
647        auth_token.user_id, success_count, error_count
648    );
649
650    if error_count == 0 {
651        Ok(Json(ApiResponse::success(())))
652    } else {
653        Ok(Json(ApiResponse::<()>::error_with_message_typed(
654            "PARTIAL_BULK_ASSIGNMENT_FAILED",
655            format!(
656                "Bulk assignment partially failed: {} successes, {} errors",
657                success_count, error_count
658            ),
659        )))
660    }
661}
662
663// ============================================================================
664// PERMISSION CHECK ENDPOINTS
665// ============================================================================
666
667/// Check permission for current user
668/// POST /api/v1/rbac/check-permission
669pub async fn check_permission(
670    State(state): State<ApiState>,
671    Extension(auth_token): Extension<AuthToken>,
672    Json(request): Json<PermissionCheckRequest>,
673) -> Result<Json<ApiResponse<PermissionCheckResponse>>, StatusCode> {
674    let context = request.context.unwrap_or_default();
675
676    match state
677        .authorization_service
678        .check_permission(
679            &auth_token.user_id,
680            &request.action,
681            &request.resource,
682            Some(&context),
683        )
684        .await
685    {
686        Ok(granted) => {
687            let response = PermissionCheckResponse {
688                granted,
689                reason: if granted {
690                    "Permission granted".to_string()
691                } else {
692                    "Permission denied".to_string()
693                },
694                required_roles: vec![request.resource.clone()],
695                missing_permissions: if granted {
696                    Vec::new()
697                } else {
698                    vec![format!("{}:{}", request.action, request.resource)]
699                },
700            };
701
702            Ok(Json(ApiResponse::success(response)))
703        }
704        Err(e) => {
705            warn!("Permission check failed: {}", e);
706            Ok(Json(ApiResponse::<PermissionCheckResponse>::error_typed(
707                "PERMISSION_CHECK_FAILED",
708                "Failed to check permission",
709            )))
710        }
711    }
712}
713
714/// Role elevation request
715/// POST /api/v1/rbac/elevate
716pub async fn elevate_role(
717    State(state): State<ApiState>,
718    Extension(auth_token): Extension<AuthToken>,
719    Json(request): Json<ElevateRoleRequest>,
720) -> Result<Json<ApiResponse<()>>, StatusCode> {
721    let duration_seconds = (request.duration_minutes.unwrap_or(30) * 60) as u64;
722
723    match state
724        .authorization_service
725        .elevate_role(
726            &auth_token.user_id,
727            &request.target_role,
728            Some(duration_seconds),
729        )
730        .await
731    {
732        Ok(_) => {
733            info!(
734                "Role elevation granted to {}: {} for {} minutes - {}",
735                auth_token.user_id,
736                request.target_role,
737                request.duration_minutes.unwrap_or(30),
738                request.justification
739            );
740            Ok(Json(ApiResponse::success(())))
741        }
742        Err(e) => {
743            warn!("Role elevation failed: {}", e);
744            Ok(Json(ApiResponse::<()>::error_with_message_typed(
745                "ELEVATION_FAILED",
746                "Failed to elevate role",
747            )))
748        }
749    }
750}
751
752// ============================================================================
753// AUDIT AND ANALYTICS ENDPOINTS
754// ============================================================================
755
756/// Get audit logs
757/// GET /api/v1/rbac/audit
758pub async fn get_audit_logs(
759    State(state): State<ApiState>,
760    Extension(auth_token): Extension<AuthToken>,
761    Query(query): Query<AuditQuery>,
762) -> Result<Json<ApiResponse<AuditLogResponse>>, StatusCode> {
763    // Check authorization
764    if !auth_token.has_permission("read:audit_logs") {
765        return Ok(Json(
766            ApiResponse::<AuditLogResponse>::forbidden_with_message_typed(
767                "Insufficient permissions to read audit logs",
768            ),
769        ));
770    }
771
772    let per_page = query.per_page.unwrap_or(20) as usize;
773    let page = query.page.unwrap_or(1) as usize;
774    let offset = (page.saturating_sub(1)) * per_page;
775    let limit = Some(offset + per_page);
776
777    let logs = match state
778        .auth_framework
779        .get_permission_audit_logs(
780            query.user_id.as_deref(),
781            query.action.as_deref(),
782            query.resource.as_deref(),
783            limit,
784        )
785        .await
786    {
787        Ok(l) => l,
788        Err(e) => {
789            warn!("Failed to retrieve audit logs: {}", e);
790            return Ok(Json(
791                ApiResponse::<AuditLogResponse>::error_with_message_typed(
792                    "AUDIT_QUERY_ERROR",
793                    "Failed to retrieve audit logs",
794                ),
795            ));
796        }
797    };
798
799    // Skip the earlier pages and take only the requested slice.
800    let page_entries: Vec<AuditEntryResponse> = logs
801        .iter()
802        .skip(offset)
803        .take(per_page)
804        .enumerate()
805        .map(|(i, log_line)| {
806            // Format: "[timestamp] EventType user=xxx outcome=xxx - description"
807            // Parse loosely so any future format changes degrade gracefully.
808            let action = log_line
809                .split_whitespace()
810                .nth(1)
811                .unwrap_or("unknown")
812                .to_string();
813            let user_id = log_line
814                .split("user=")
815                .nth(1)
816                .and_then(|s| s.split_whitespace().next())
817                .map(|s| s.to_string());
818            let result = if log_line.contains("Failure") || log_line.contains("Denied") {
819                "denied"
820            } else {
821                "granted"
822            }
823            .to_string();
824            let description = log_line
825                .split_once(" - ")
826                .map(|x| x.1)
827                .unwrap_or(log_line.as_str())
828                .to_string();
829
830            AuditEntryResponse {
831                id: format!("log-{}", offset + i),
832                user_id,
833                action,
834                resource: query.resource.clone(),
835                result,
836                context: HashMap::new(),
837                timestamp: chrono::Utc::now(), // Exact timestamp not preserved in string form
838                description,
839            }
840        })
841        .collect();
842
843    let total_count = logs.len() as u64;
844
845    let response = AuditLogResponse {
846        entries: page_entries,
847        total_count,
848        page: page as u32,
849        per_page: per_page as u32,
850    };
851
852    Ok(Json(ApiResponse::success(response)))
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    // Helper function to create test auth token
860    fn create_test_token(permissions: Vec<&str>) -> AuthToken {
861        use crate::tokens::TokenMetadata;
862        use chrono::Utc;
863
864        AuthToken {
865            token_id: "test_token_123".to_string(),
866            user_id: "test_user".to_string(),
867            access_token: "test_access_token".to_string(),
868            token_type: Some("bearer".to_string()),
869            subject: Some("test_user".to_string()),
870            issuer: Some("auth-framework".to_string()),
871            refresh_token: Some("test_refresh_token".to_string()),
872            issued_at: Utc::now(),
873            expires_at: Utc::now() + chrono::Duration::hours(1),
874            scopes: vec!["read".to_string(), "write".to_string()].into(),
875            auth_method: "password".to_string(),
876            client_id: Some("test_client".to_string()),
877            user_profile: None,
878            permissions: permissions
879                .into_iter()
880                .map(|s| s.to_string())
881                .collect::<Vec<_>>()
882                .into(),
883            roles: vec!["admin".to_string()].into(),
884            metadata: TokenMetadata::default(),
885        }
886    }
887
888    #[tokio::test]
889    async fn test_create_role_unauthorized() {
890        let token = create_test_token(vec!["read:users"]);
891        assert!(
892            !token.permissions.contains(&"admin:roles:write".to_string()),
893            "token without admin:roles:write should not be authorized to create roles"
894        );
895    }
896
897    #[tokio::test]
898    async fn test_create_role_success() {
899        let token = create_test_token(vec!["admin:roles:write", "read:users"]);
900        assert!(
901            token.permissions.contains(&"admin:roles:write".to_string()),
902            "token with admin:roles:write should be authorized to create roles"
903        );
904    }
905
906    #[tokio::test]
907    async fn test_permission_check() {
908        let token = create_test_token(vec!["read:users", "write:users"]);
909        assert_eq!(token.permissions.len(), 2);
910        assert!(token.permissions.contains(&"read:users".to_string()));
911        assert!(token.permissions.contains(&"write:users".to_string()));
912        assert!(!token.permissions.contains(&"admin:delete".to_string()));
913    }
914}