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    pub action:      String,
43    /// Optional permission description
44    pub description: Option<String>,
45    /// Creation timestamp (ISO 8601)
46    pub created_at:  String,
47}
48
49/// User-Role association for API responses
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct UserRoleDto {
52    /// User ID
53    pub user_id:     String,
54    /// Role ID
55    pub role_id:     String,
56    /// Tenant ID for multi-tenancy
57    pub tenant_id:   Option<String>,
58    /// Assignment timestamp (ISO 8601)
59    pub assigned_at: String,
60}
61
62/// Request to create a new role
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CreateRoleRequest {
65    /// Role name
66    pub name:        String,
67    /// Optional description
68    pub description: Option<String>,
69    /// Initial permissions to assign
70    pub permissions: Vec<String>,
71}
72
73/// Request to create a new permission
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct CreatePermissionRequest {
76    /// Resource name
77    pub resource:    String,
78    /// Action name
79    pub action:      String,
80    /// Optional description
81    pub description: Option<String>,
82}
83
84/// Request to assign a role to a user
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AssignRoleRequest {
87    /// User ID
88    pub user_id: String,
89    /// Role ID to assign
90    pub role_id: String,
91}
92
93/// API state for role and permission management
94#[derive(Clone)]
95pub struct RbacManagementState {
96    /// Database backend for RBAC operations
97    pub db: Arc<db_backend::RbacDbBackend>,
98}
99
100/// Create RBAC management router
101///
102/// Routes:
103/// - POST   /api/roles                           - Create role
104/// - GET    /api/roles                           - List roles
105/// - GET    /api/roles/{role_id}                 - Get role details
106/// - PUT    /api/roles/{role_id}                 - Update role
107/// - DELETE /api/roles/{role_id}                 - Delete role
108/// - POST   /api/permissions                     - Create permission
109/// - GET    /api/permissions                     - List permissions
110/// - GET    /api/permissions/{permission_id}    - Get permission details
111/// - DELETE /api/permissions/{permission_id}    - Delete permission
112/// - POST   /api/user-roles                      - Assign role to user
113/// - GET    /api/user-roles                      - List user-role assignments
114/// - DELETE /api/user-roles/{user_id}/{role_id} - Revoke role from user
115/// - GET    /api/audit/permissions               - Query permission access audit logs
116pub fn rbac_management_router(state: RbacManagementState) -> Router {
117    Router::new()
118        // Role endpoints
119        .route("/api/roles", post(create_role).get(list_roles))
120        .route("/api/roles/:role_id", get(get_role).put(update_role).delete(delete_role))
121        // Permission endpoints
122        .route("/api/permissions", post(create_permission).get(list_permissions))
123        .route("/api/permissions/:permission_id", get(get_permission).delete(delete_permission))
124        // User-role assignment endpoints
125        .route("/api/user-roles", post(assign_role).get(list_user_roles))
126        .route("/api/user-roles/:user_id/:role_id", delete(revoke_role))
127        // Audit endpoints
128        .route("/api/audit/permissions", get(query_permission_audit))
129        .with_state(Arc::new(state))
130}
131
132// =============================================================================
133// Role Management Endpoints
134// =============================================================================
135
136/// Create a new role
137/// POST /api/roles
138async fn create_role(
139    State(state): State<Arc<RbacManagementState>>,
140    Json(payload): Json<CreateRoleRequest>,
141) -> impl IntoResponse {
142    // Phase 11.5 Cycle 3: Call database backend
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(serde_json::to_value(role).unwrap_or_default()))
155            .into_response(),
156        Err(_) => (StatusCode::CONFLICT, Json(serde_json::json!({"error": "role_duplicate"})))
157            .into_response(),
158    }
159}
160
161/// List all roles
162/// GET /api/roles
163async fn list_roles(State(state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
164    // Phase 11.5 Cycle 3: Call database backend
165    // In production: extract tenant from JWT, apply pagination
166    match state.db.list_roles(None, 100, 0).await {
167        Ok(roles) => Json(roles),
168        Err(_) => Json(Vec::<RoleDto>::new()),
169    }
170}
171
172/// Get role details
173/// GET /api/roles/{role_id}
174async fn get_role(
175    State(state): State<Arc<RbacManagementState>>,
176    Path(role_id): Path<String>,
177) -> impl IntoResponse {
178    // Phase 11.5 Cycle 3: Call database backend
179    match state.db.get_role(&role_id).await {
180        Ok(role) => {
181            (StatusCode::OK, Json(serde_json::to_value(role).unwrap_or_default())).into_response()
182        },
183        Err(_) => (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "role_not_found"})))
184            .into_response(),
185    }
186}
187
188/// Update role
189/// PUT /api/roles/{role_id}
190async fn update_role(
191    State(_state): State<Arc<RbacManagementState>>,
192    Path(_role_id): Path<String>,
193    Json(_payload): Json<CreateRoleRequest>,
194) -> impl IntoResponse {
195    // Phase 11.5 Cycle 3: Placeholder - requires additional database method
196    // Would need update_role() method in backend
197    Json(serde_json::json!({"updated": true}))
198}
199
200/// Delete role
201/// DELETE /api/roles/{role_id}
202async fn delete_role(
203    State(state): State<Arc<RbacManagementState>>,
204    Path(role_id): Path<String>,
205) -> impl IntoResponse {
206    // Phase 11.5 Cycle 3: Call database backend
207    match state.db.delete_role(&role_id).await {
208        Ok(_) => StatusCode::NO_CONTENT,
209        Err(_) => StatusCode::CONFLICT,
210    }
211}
212
213// =============================================================================
214// Permission Management Endpoints
215// =============================================================================
216
217/// Create a new permission
218/// POST /api/permissions
219async fn create_permission(
220    State(state): State<Arc<RbacManagementState>>,
221    Json(payload): Json<CreatePermissionRequest>,
222) -> impl IntoResponse {
223    // Phase 11.5 Cycle 3: Call database backend
224    match state
225        .db
226        .create_permission(&payload.resource, &payload.action, payload.description.as_deref())
227        .await
228    {
229        Ok(perm) => (StatusCode::CREATED, Json(serde_json::to_value(perm).unwrap_or_default()))
230            .into_response(),
231        Err(_) => {
232            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "permission_duplicate"})))
233                .into_response()
234        },
235    }
236}
237
238/// List all permissions
239/// GET /api/permissions
240async fn list_permissions(State(_state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
241    // Phase 11.5 Cycle 3: Would call database backend
242    // Placeholder for now
243    Json(Vec::<PermissionDto>::new())
244}
245
246/// Get permission details
247/// GET /api/permissions/{permission_id}
248async fn get_permission(
249    State(_state): State<Arc<RbacManagementState>>,
250    Path(_permission_id): Path<String>,
251) -> impl IntoResponse {
252    // Phase 11.5 Cycle 3: Would call database backend
253    (
254        StatusCode::NOT_FOUND,
255        Json(serde_json::json!({"error": "permission_not_found"})),
256    )
257}
258
259/// Delete permission
260/// DELETE /api/permissions/{permission_id}
261async fn delete_permission(
262    State(_state): State<Arc<RbacManagementState>>,
263    Path(_permission_id): Path<String>,
264) -> impl IntoResponse {
265    // Phase 11.5 Cycle 3: Would call database backend
266    StatusCode::NO_CONTENT
267}
268
269// =============================================================================
270// User-Role Assignment Endpoints
271// =============================================================================
272
273/// Assign a role to a user
274/// POST /api/user-roles
275async fn assign_role(
276    State(state): State<Arc<RbacManagementState>>,
277    Json(payload): Json<AssignRoleRequest>,
278) -> impl IntoResponse {
279    // Phase 11.5 Cycle 3: Call database backend
280    match state.db.assign_role_to_user(&payload.user_id, &payload.role_id, None).await {
281        Ok(assignment) => {
282            (StatusCode::CREATED, Json(serde_json::to_value(assignment).unwrap_or_default()))
283                .into_response()
284        },
285        Err(_) => {
286            (StatusCode::CONFLICT, Json(serde_json::json!({"error": "assignment_duplicate"})))
287                .into_response()
288        },
289    }
290}
291
292/// List user-role assignments
293/// GET /api/user-roles
294async fn list_user_roles(State(_state): State<Arc<RbacManagementState>>) -> impl IntoResponse {
295    // Phase 11.5 Cycle 3: Would call database backend to list assignments
296    // Placeholder for now
297    Json(Vec::<UserRoleDto>::new())
298}
299
300/// Revoke a role from a user
301/// DELETE /api/user-roles/{user_id}/{role_id}
302async fn revoke_role(
303    State(state): State<Arc<RbacManagementState>>,
304    Path((user_id, role_id)): Path<(String, String)>,
305) -> impl IntoResponse {
306    // Phase 11.5 Cycle 3: Call database backend
307    match state.db.revoke_role_from_user(&user_id, &role_id).await {
308        Ok(_) => StatusCode::NO_CONTENT,
309        Err(_) => StatusCode::NOT_FOUND,
310    }
311}
312
313// =============================================================================
314// Audit Endpoints
315// =============================================================================
316
317/// Query permission access audit logs
318/// GET /api/audit/permissions?user_id=...&start_time=...&end_time=...
319async fn query_permission_audit(
320    State(_state): State<Arc<RbacManagementState>>,
321) -> impl IntoResponse {
322    // Phase 11.5 Cycle 1: Placeholder implementation
323    Json(Vec::<serde_json::Value>::new())
324}
325
326/// Database backend for RBAC operations
327pub mod db_backend;
328
329#[cfg(test)]
330mod tests;
331
332#[cfg(test)]
333mod db_backend_tests;
334
335#[cfg(test)]
336mod integration_tests;
337
338#[cfg(test)]
339mod schema_tests;