Skip to main content

fraiseql_server/api/
rbac_management.rs

1//! Role and Permission Management API
2//!
3//! REST API endpoints for managing roles, permissions, and user-role associations.
4
5use std::sync::Arc;
6
7use axum::{
8    Json, Router,
9    extract::{Path, State},
10    http::StatusCode,
11    response::IntoResponse,
12    routing::{delete, get, post},
13};
14use serde::{Deserialize, Serialize};
15
16/// Role definition for API responses
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct RoleDto {
19    /// Unique role identifier
20    pub id:          String,
21    /// Human-readable role name
22    pub name:        String,
23    /// Optional role description
24    pub description: Option<String>,
25    /// List of permission IDs assigned to this role
26    pub permissions: Vec<String>,
27    /// Tenant ID for multi-tenancy
28    pub tenant_id:   Option<String>,
29    /// Creation timestamp (ISO 8601)
30    pub created_at:  String,
31    /// Last update timestamp (ISO 8601)
32    pub updated_at:  String,
33}
34
35/// Permission definition for API responses
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct PermissionDto {
38    /// Unique permission identifier
39    pub id:          String,
40    /// Permission resource and action (e.g., "query:read", "mutation:write")
41    pub resource:    String,
42    /// The action part of the permission (e.g., `"read"`, `"write"`, `"delete"`).
43    pub action:      String,
44    /// Optional permission description
45    pub description: Option<String>,
46    /// Creation timestamp (ISO 8601)
47    pub created_at:  String,
48}
49
50/// User-Role association for API responses
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct UserRoleDto {
53    /// User ID
54    pub user_id:     String,
55    /// Role ID
56    pub role_id:     String,
57    /// Tenant ID for multi-tenancy
58    pub tenant_id:   Option<String>,
59    /// Assignment timestamp (ISO 8601)
60    pub assigned_at: String,
61}
62
63/// Request to create a new role
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CreateRoleRequest {
66    /// Role name
67    pub name:        String,
68    /// Optional description
69    pub description: Option<String>,
70    /// Initial permissions to assign
71    pub permissions: Vec<String>,
72}
73
74/// Request to create a new permission
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CreatePermissionRequest {
77    /// Resource name
78    pub resource:    String,
79    /// Action name
80    pub action:      String,
81    /// Optional description
82    pub description: Option<String>,
83}
84
85/// Request to assign a role to a user
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct AssignRoleRequest {
88    /// User ID
89    pub user_id: String,
90    /// Role ID to assign
91    pub role_id: String,
92}
93
94/// API state for role and permission management
95#[derive(Clone)]
96pub struct RbacManagementState {
97    /// Database backend for RBAC operations
98    pub db: Arc<db_backend::RbacDbBackend>,
99}
100
101/// Create RBAC management router
102///
103/// Routes:
104/// - POST   /api/roles                           - Create role
105/// - GET    /api/roles                           - List roles
106/// - GET    `/api/roles/{role_id}`                 - Get role details
107/// - PUT    `/api/roles/{role_id}`                 - Update role
108/// - DELETE `/api/roles/{role_id}`                 - Delete role
109/// - POST   /api/permissions                     - Create permission
110/// - GET    /api/permissions                     - List permissions
111/// - GET    `/api/permissions/{permission_id}`    - Get permission details
112/// - DELETE `/api/permissions/{permission_id}`    - Delete permission
113/// - POST   /api/user-roles                      - Assign role to user
114/// - GET    /api/user-roles                      - List user-role assignments
115/// - DELETE /api/user-roles/{user_id}/{role_id} - Revoke role from user
116/// - GET    /api/audit/permissions               - Query permission access audit logs
117pub fn rbac_management_router(state: RbacManagementState) -> Router {
118    Router::new()
119        // Role endpoints
120        .route("/api/roles", post(create_role).get(list_roles))
121        .route("/api/roles/:role_id", get(get_role).put(update_role).delete(delete_role))
122        // Permission endpoints
123        .route("/api/permissions", post(create_permission).get(list_permissions))
124        .route("/api/permissions/:permission_id", get(get_permission).delete(delete_permission))
125        // User-role assignment endpoints
126        .route("/api/user-roles", post(assign_role).get(list_user_roles))
127        .route("/api/user-roles/:user_id/:role_id", delete(revoke_role))
128        // Audit endpoints
129        .route("/api/audit/permissions", get(query_permission_audit))
130        .with_state(Arc::new(state))
131}
132
133// =============================================================================
134// Role Management Endpoints
135// =============================================================================
136
137/// Create a new role
138/// POST /api/roles
139async fn create_role(
140    State(state): State<Arc<RbacManagementState>>,
141    Json(payload): Json<CreateRoleRequest>,
142) -> impl IntoResponse {
143    // In production: validate payload, extract tenant from JWT, create role
144    match state
145        .db
146        .create_role(
147            &payload.name,
148            payload.description.as_deref(),
149            payload.permissions,
150            None, // Would extract tenant from JWT
151        )
152        .await
153    {
154        Ok(role) => (StatusCode::CREATED, Json(role)).into_response(),
155        Err(_) => (StatusCode::CONFLICT, Json(serde_json::json!({"error": "role_duplicate"})))
156            .into_response(),
157    }
158}
159
160/// List all roles
161/// GET /api/roles
162async fn list_roles(State(state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
163    // In production: extract tenant from JWT, apply pagination
164    match state.db.list_roles(None, 100, 0).await {
165        Ok(roles) => (StatusCode::OK, Json(roles)).into_response(),
166        Err(_) => (
167            StatusCode::INTERNAL_SERVER_ERROR,
168            Json(serde_json::json!({"error": "database_error"})),
169        )
170            .into_response(),
171    }
172}
173
174/// Get role details
175/// GET `/api/roles/{role_id}`
176async fn get_role(
177    State(state): State<Arc<RbacManagementState>>,
178    Path(role_id): Path<String>,
179) -> impl IntoResponse {
180    match state.db.get_role(&role_id).await {
181        Ok(role) => (StatusCode::OK, Json(role)).into_response(),
182        Err(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "role_not_found"})))
183            .into_response(),
184    }
185}
186
187/// Update role
188/// PUT `/api/roles/{role_id}`
189async fn update_role(
190    State(state): State<Arc<RbacManagementState>>,
191    Path(role_id): Path<String>,
192    Json(payload): Json<CreateRoleRequest>,
193) -> impl IntoResponse {
194    match state
195        .db
196        .update_role(&role_id, &payload.name, payload.description.as_deref(), payload.permissions)
197        .await
198    {
199        Ok(role) => (StatusCode::OK, Json(role)).into_response(),
200        Err(db_backend::RbacDbError::RoleNotFound) => {
201            (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "role_not_found"})))
202                .into_response()
203        },
204        Err(_) => (StatusCode::CONFLICT, Json(serde_json::json!({"error": "update_failed"})))
205            .into_response(),
206    }
207}
208
209/// Delete role
210/// DELETE `/api/roles/{role_id}`
211async fn delete_role(
212    State(state): State<Arc<RbacManagementState>>,
213    Path(role_id): Path<String>,
214) -> impl IntoResponse {
215    match state.db.delete_role(&role_id).await {
216        Ok(()) => StatusCode::NO_CONTENT,
217        Err(db_backend::RbacDbError::RoleNotFound) => StatusCode::NOT_FOUND,
218        Err(_) => StatusCode::CONFLICT,
219    }
220}
221
222// =============================================================================
223// Permission Management Endpoints
224// =============================================================================
225
226/// Create a new permission
227/// POST /api/permissions
228async fn create_permission(
229    State(state): State<Arc<RbacManagementState>>,
230    Json(payload): Json<CreatePermissionRequest>,
231) -> impl IntoResponse {
232    match state
233        .db
234        .create_permission(&payload.resource, &payload.action, payload.description.as_deref())
235        .await
236    {
237        Ok(perm) => (StatusCode::CREATED, Json(perm)).into_response(),
238        Err(_) => {
239            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "permission_duplicate"})))
240                .into_response()
241        },
242    }
243}
244
245/// List all permissions
246/// GET /api/permissions
247async fn list_permissions(State(state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
248    match state.db.list_permissions().await {
249        Ok(perms) => (StatusCode::OK, Json(perms)).into_response(),
250        Err(_) => (
251            StatusCode::INTERNAL_SERVER_ERROR,
252            Json(serde_json::json!({"error": "database_error"})),
253        )
254            .into_response(),
255    }
256}
257
258/// Get permission details
259/// GET `/api/permissions/{permission_id}`
260async fn get_permission(
261    State(state): State<Arc<RbacManagementState>>,
262    Path(permission_id): Path<String>,
263) -> impl IntoResponse {
264    match state.db.get_permission(&permission_id).await {
265        Ok(perm) => (StatusCode::OK, Json(perm)).into_response(),
266        Err(_) => (
267            StatusCode::NOT_FOUND,
268            Json(serde_json::json!({"error": "permission_not_found"})),
269        )
270            .into_response(),
271    }
272}
273
274/// Delete permission
275/// DELETE `/api/permissions/{permission_id}`
276async fn delete_permission(
277    State(state): State<Arc<RbacManagementState>>,
278    Path(permission_id): Path<String>,
279) -> impl IntoResponse {
280    match state.db.delete_permission(&permission_id).await {
281        Ok(()) => StatusCode::NO_CONTENT,
282        Err(db_backend::RbacDbError::PermissionInUse) => StatusCode::CONFLICT,
283        Err(db_backend::RbacDbError::PermissionNotFound) => StatusCode::NOT_FOUND,
284        Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
285    }
286}
287
288// =============================================================================
289// User-Role Assignment Endpoints
290// =============================================================================
291
292/// Assign a role to a user
293/// POST /api/user-roles
294async fn assign_role(
295    State(state): State<Arc<RbacManagementState>>,
296    Json(payload): Json<AssignRoleRequest>,
297) -> impl IntoResponse {
298    match state.db.assign_role_to_user(&payload.user_id, &payload.role_id, None).await {
299        Ok(assignment) => (StatusCode::CREATED, Json(assignment)).into_response(),
300        Err(_) => {
301            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "assignment_duplicate"})))
302                .into_response()
303        },
304    }
305}
306
307/// List user-role assignments
308/// GET /api/user-roles?user_id=...
309async fn list_user_roles(
310    State(state): State<Arc<RbacManagementState>>,
311    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
312) -> axum::response::Response {
313    let user_id = params.get("user_id").map_or("", String::as_str);
314    if user_id.is_empty() {
315        return (StatusCode::OK, Json(serde_json::json!([]))).into_response();
316    }
317    match state.db.list_user_roles(user_id).await {
318        Ok(assignments) => (StatusCode::OK, Json(assignments)).into_response(),
319        Err(_) => (
320            StatusCode::INTERNAL_SERVER_ERROR,
321            Json(serde_json::json!({"error": "database_error"})),
322        )
323            .into_response(),
324    }
325}
326
327/// Revoke a role from a user
328/// DELETE /api/user-roles/{user_id}/{role_id}
329async fn revoke_role(
330    State(state): State<Arc<RbacManagementState>>,
331    Path((user_id, role_id)): Path<(String, String)>,
332) -> impl IntoResponse {
333    match state.db.revoke_role_from_user(&user_id, &role_id).await {
334        Ok(()) => StatusCode::NO_CONTENT,
335        Err(_) => StatusCode::NOT_FOUND,
336    }
337}
338
339// =============================================================================
340// Audit Endpoints
341// =============================================================================
342
343/// Query permission access audit logs
344/// GET `/api/audit/permissions?user_id=...&start_time=...&end_time=...`
345async fn query_permission_audit(
346    State(_state): State<Arc<RbacManagementState>>,
347) -> impl IntoResponse {
348    Json(Vec::<serde_json::Value>::new())
349}
350
351/// Database backend for RBAC operations
352pub mod db_backend;
353
354#[cfg(test)]
355mod tests;
356
357#[cfg(test)]
358mod db_backend_tests;
359
360#[cfg(test)]
361mod integration_tests;
362
363#[cfg(test)]
364mod schema_tests;