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(
125            "/api/permissions/{permission_id}",
126            get(get_permission).delete(delete_permission),
127        )
128        // User-role assignment endpoints
129        .route("/api/user-roles", post(assign_role).get(list_user_roles))
130        .route("/api/user-roles/{user_id}/{role_id}", delete(revoke_role))
131        // Audit endpoints
132        .route("/api/audit/permissions", get(query_permission_audit))
133        .with_state(Arc::new(state))
134}
135
136// =============================================================================
137// Role Management Endpoints
138// =============================================================================
139
140/// Create a new role
141/// POST /api/roles
142async fn create_role(
143    State(state): State<Arc<RbacManagementState>>,
144    Json(payload): Json<CreateRoleRequest>,
145) -> impl IntoResponse {
146    // In production: validate payload, extract tenant from JWT, create role
147    match state
148        .db
149        .create_role(
150            &payload.name,
151            payload.description.as_deref(),
152            payload.permissions,
153            None, // Would extract tenant from JWT
154        )
155        .await
156    {
157        Ok(role) => (StatusCode::CREATED, Json(role)).into_response(),
158        Err(_) => (StatusCode::CONFLICT, Json(serde_json::json!({"error": "role_duplicate"})))
159            .into_response(),
160    }
161}
162
163/// List all roles
164/// GET /api/roles
165async fn list_roles(State(state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
166    // In production: extract tenant from JWT, apply pagination
167    match state.db.list_roles(None, 100, 0).await {
168        Ok(roles) => (StatusCode::OK, Json(roles)).into_response(),
169        Err(_) => (
170            StatusCode::INTERNAL_SERVER_ERROR,
171            Json(serde_json::json!({"error": "database_error"})),
172        )
173            .into_response(),
174    }
175}
176
177/// Get role details
178/// GET `/api/roles/{role_id}`
179async fn get_role(
180    State(state): State<Arc<RbacManagementState>>,
181    Path(role_id): Path<String>,
182) -> impl IntoResponse {
183    match state.db.get_role(&role_id).await {
184        Ok(role) => (StatusCode::OK, Json(role)).into_response(),
185        Err(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "role_not_found"})))
186            .into_response(),
187    }
188}
189
190/// Update role
191/// PUT `/api/roles/{role_id}`
192async fn update_role(
193    State(state): State<Arc<RbacManagementState>>,
194    Path(role_id): Path<String>,
195    Json(payload): Json<CreateRoleRequest>,
196) -> impl IntoResponse {
197    match state
198        .db
199        .update_role(&role_id, &payload.name, payload.description.as_deref(), payload.permissions)
200        .await
201    {
202        Ok(role) => (StatusCode::OK, Json(role)).into_response(),
203        Err(db_backend::RbacDbError::RoleNotFound) => {
204            (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "role_not_found"})))
205                .into_response()
206        },
207        Err(_) => (StatusCode::CONFLICT, Json(serde_json::json!({"error": "update_failed"})))
208            .into_response(),
209    }
210}
211
212/// Delete role
213/// DELETE `/api/roles/{role_id}`
214async fn delete_role(
215    State(state): State<Arc<RbacManagementState>>,
216    Path(role_id): Path<String>,
217) -> impl IntoResponse {
218    match state.db.delete_role(&role_id).await {
219        Ok(()) => StatusCode::NO_CONTENT,
220        Err(db_backend::RbacDbError::RoleNotFound) => StatusCode::NOT_FOUND,
221        Err(_) => StatusCode::CONFLICT,
222    }
223}
224
225// =============================================================================
226// Permission Management Endpoints
227// =============================================================================
228
229/// Create a new permission
230/// POST /api/permissions
231async fn create_permission(
232    State(state): State<Arc<RbacManagementState>>,
233    Json(payload): Json<CreatePermissionRequest>,
234) -> impl IntoResponse {
235    match state
236        .db
237        .create_permission(&payload.resource, &payload.action, payload.description.as_deref())
238        .await
239    {
240        Ok(perm) => (StatusCode::CREATED, Json(perm)).into_response(),
241        Err(_) => {
242            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "permission_duplicate"})))
243                .into_response()
244        },
245    }
246}
247
248/// List all permissions
249/// GET /api/permissions
250async fn list_permissions(State(state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
251    match state.db.list_permissions().await {
252        Ok(perms) => (StatusCode::OK, Json(perms)).into_response(),
253        Err(_) => (
254            StatusCode::INTERNAL_SERVER_ERROR,
255            Json(serde_json::json!({"error": "database_error"})),
256        )
257            .into_response(),
258    }
259}
260
261/// Get permission details
262/// GET `/api/permissions/{permission_id}`
263async fn get_permission(
264    State(state): State<Arc<RbacManagementState>>,
265    Path(permission_id): Path<String>,
266) -> impl IntoResponse {
267    match state.db.get_permission(&permission_id).await {
268        Ok(perm) => (StatusCode::OK, Json(perm)).into_response(),
269        Err(_) => (
270            StatusCode::NOT_FOUND,
271            Json(serde_json::json!({"error": "permission_not_found"})),
272        )
273            .into_response(),
274    }
275}
276
277/// Delete permission
278/// DELETE `/api/permissions/{permission_id}`
279async fn delete_permission(
280    State(state): State<Arc<RbacManagementState>>,
281    Path(permission_id): Path<String>,
282) -> impl IntoResponse {
283    match state.db.delete_permission(&permission_id).await {
284        Ok(()) => StatusCode::NO_CONTENT,
285        Err(db_backend::RbacDbError::PermissionInUse) => StatusCode::CONFLICT,
286        Err(db_backend::RbacDbError::PermissionNotFound) => StatusCode::NOT_FOUND,
287        Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
288    }
289}
290
291// =============================================================================
292// User-Role Assignment Endpoints
293// =============================================================================
294
295/// Assign a role to a user
296/// POST /api/user-roles
297async fn assign_role(
298    State(state): State<Arc<RbacManagementState>>,
299    Json(payload): Json<AssignRoleRequest>,
300) -> impl IntoResponse {
301    match state.db.assign_role_to_user(&payload.user_id, &payload.role_id, None).await {
302        Ok(assignment) => (StatusCode::CREATED, Json(assignment)).into_response(),
303        Err(_) => {
304            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "assignment_duplicate"})))
305                .into_response()
306        },
307    }
308}
309
310/// List user-role assignments
311/// GET /api/user-roles?user_id=...
312async fn list_user_roles(
313    State(state): State<Arc<RbacManagementState>>,
314    axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
315) -> axum::response::Response {
316    let user_id = params.get("user_id").map_or("", String::as_str);
317    if user_id.is_empty() {
318        return (StatusCode::OK, Json(serde_json::json!([]))).into_response();
319    }
320    match state.db.list_user_roles(user_id).await {
321        Ok(assignments) => (StatusCode::OK, Json(assignments)).into_response(),
322        Err(_) => (
323            StatusCode::INTERNAL_SERVER_ERROR,
324            Json(serde_json::json!({"error": "database_error"})),
325        )
326            .into_response(),
327    }
328}
329
330/// Revoke a role from a user
331/// DELETE /api/user-roles/{user_id}/{role_id}
332async fn revoke_role(
333    State(state): State<Arc<RbacManagementState>>,
334    Path((user_id, role_id)): Path<(String, String)>,
335) -> impl IntoResponse {
336    match state.db.revoke_role_from_user(&user_id, &role_id).await {
337        Ok(()) => StatusCode::NO_CONTENT,
338        Err(_) => StatusCode::NOT_FOUND,
339    }
340}
341
342// =============================================================================
343// Audit Endpoints
344// =============================================================================
345
346/// Query permission access audit logs
347/// GET `/api/audit/permissions?user_id=...&start_time=...&end_time=...`
348async fn query_permission_audit(
349    State(_state): State<Arc<RbacManagementState>>,
350) -> impl IntoResponse {
351    Json(Vec::<serde_json::Value>::new())
352}
353
354/// Database backend for RBAC operations
355pub mod db_backend;
356
357#[cfg(test)]
358mod tests;
359
360#[cfg(test)]
361mod db_backend_tests;
362
363#[cfg(test)]
364mod integration_tests;
365
366#[cfg(test)]
367mod schema_tests;