use std::sync::Arc;
use axum::{
Json, Router,
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{delete, get, post},
};
use serde::{Deserialize, Serialize};
use self::db_backend::{AuditFilter, RbacDbError};
const DEFAULT_LIMIT: u32 = 100;
const MAX_LIMIT: u32 = 1000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoleDto {
pub id: String,
pub name: String,
pub description: Option<String>,
pub permissions: Vec<String>,
pub tenant_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionDto {
pub id: String,
pub resource: String,
pub action: String,
pub description: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRoleDto {
pub user_id: String,
pub role_id: String,
pub tenant_id: Option<String>,
pub assigned_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreateRoleRequest {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub permissions: Vec<String>,
#[serde(default)]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CreatePermissionRequest {
pub resource: String,
pub action: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AssignRoleRequest {
pub user_id: String,
pub role_id: String,
#[serde(default)]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ListQuery {
#[serde(default)]
pub tenant_id: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
#[serde(default)]
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UserRolesQuery {
pub user_id: String,
#[serde(default)]
pub tenant_id: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
#[serde(default)]
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuditQuery {
#[serde(default)]
pub user_id: Option<String>,
#[serde(default)]
pub role_id: Option<String>,
#[serde(default)]
pub event_type: Option<String>,
#[serde(default)]
pub tenant_id: Option<String>,
#[serde(default)]
pub start_time: Option<String>,
#[serde(default)]
pub end_time: Option<String>,
#[serde(default)]
pub limit: Option<u32>,
#[serde(default)]
pub offset: Option<u32>,
}
fn resolve_limit(limit: Option<u32>) -> Result<u32, String> {
match limit {
None => Ok(DEFAULT_LIMIT),
Some(0) => Err("limit must be at least 1".to_string()),
Some(n) if n > MAX_LIMIT => Err(format!("limit must not exceed {MAX_LIMIT}")),
Some(n) => Ok(n),
}
}
fn parse_time(
field: &str,
raw: Option<&String>,
) -> Result<Option<chrono::DateTime<chrono::Utc>>, String> {
raw.map(|s| {
chrono::DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&chrono::Utc))
.map_err(|e| format!("Invalid {field} '{s}': {e}"))
})
.transpose()
}
fn bad_request(message: &str) -> Response {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "invalid_request", "message": message})),
)
.into_response()
}
impl IntoResponse for RbacDbError {
fn into_response(self) -> Response {
let (status, code) = match self {
Self::InvalidInput(_) => (StatusCode::BAD_REQUEST, "invalid_request"),
Self::RoleNotFound => (StatusCode::NOT_FOUND, "role_not_found"),
Self::PermissionNotFound => (StatusCode::NOT_FOUND, "permission_not_found"),
Self::AssignmentNotFound => (StatusCode::NOT_FOUND, "assignment_not_found"),
Self::RoleDuplicate => (StatusCode::CONFLICT, "role_duplicate"),
Self::PermissionDuplicate => (StatusCode::CONFLICT, "permission_duplicate"),
Self::AssignmentDuplicate => (StatusCode::CONFLICT, "assignment_duplicate"),
Self::PermissionInUse => (StatusCode::CONFLICT, "permission_in_use"),
Self::ConnectionError(_) | Self::QueryError(_) | Self::TransactionError(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, "database_error")
},
};
let message = if status == StatusCode::INTERNAL_SERVER_ERROR {
tracing::error!(error = %self, "RBAC management operation failed");
"The RBAC store could not complete the operation".to_string()
} else {
self.to_string()
};
(status, Json(serde_json::json!({"error": code, "message": message}))).into_response()
}
}
#[derive(Clone)]
pub struct RbacManagementState {
pub db: Arc<db_backend::RbacDbBackend>,
}
pub fn rbac_management_router(state: RbacManagementState) -> Router {
Router::new()
.route("/api/roles", post(create_role).get(list_roles))
.route("/api/roles/{role_id}", get(get_role).put(update_role).delete(delete_role))
.route("/api/permissions", post(create_permission).get(list_permissions))
.route(
"/api/permissions/{permission_id}",
get(get_permission).delete(delete_permission),
)
.route("/api/user-roles", post(assign_role).get(list_user_roles))
.route("/api/user-roles/{user_id}/{role_id}", delete(revoke_role))
.route("/api/audit/permissions", get(query_permission_audit))
.with_state(Arc::new(state))
}
async fn create_role(
State(state): State<Arc<RbacManagementState>>,
Json(payload): Json<CreateRoleRequest>,
) -> Response {
match state
.db
.create_role(
&payload.name,
payload.description.as_deref(),
payload.permissions,
payload.tenant_id.as_deref(),
)
.await
{
Ok(role) => (StatusCode::CREATED, Json(role)).into_response(),
Err(e) => e.into_response(),
}
}
async fn list_roles(
State(state): State<Arc<RbacManagementState>>,
Query(params): Query<ListQuery>,
) -> Response {
let limit = match resolve_limit(params.limit) {
Ok(limit) => limit,
Err(message) => return bad_request(&message),
};
match state
.db
.list_roles(params.tenant_id.as_deref(), limit, params.offset.unwrap_or(0))
.await
{
Ok(page) => (StatusCode::OK, Json(page)).into_response(),
Err(e) => e.into_response(),
}
}
async fn get_role(
State(state): State<Arc<RbacManagementState>>,
Path(role_id): Path<String>,
) -> Response {
match state.db.get_role(&role_id).await {
Ok(role) => (StatusCode::OK, Json(role)).into_response(),
Err(e) => e.into_response(),
}
}
async fn update_role(
State(state): State<Arc<RbacManagementState>>,
Path(role_id): Path<String>,
Json(payload): Json<CreateRoleRequest>,
) -> Response {
match state
.db
.update_role(&role_id, &payload.name, payload.description.as_deref(), payload.permissions)
.await
{
Ok(role) => (StatusCode::OK, Json(role)).into_response(),
Err(e) => e.into_response(),
}
}
async fn delete_role(
State(state): State<Arc<RbacManagementState>>,
Path(role_id): Path<String>,
) -> Response {
match state.db.delete_role(&role_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => e.into_response(),
}
}
async fn create_permission(
State(state): State<Arc<RbacManagementState>>,
Json(payload): Json<CreatePermissionRequest>,
) -> Response {
match state
.db
.create_permission(&payload.resource, &payload.action, payload.description.as_deref())
.await
{
Ok(perm) => (StatusCode::CREATED, Json(perm)).into_response(),
Err(e) => e.into_response(),
}
}
async fn list_permissions(
State(state): State<Arc<RbacManagementState>>,
Query(params): Query<ListQuery>,
) -> Response {
let limit = match resolve_limit(params.limit) {
Ok(limit) => limit,
Err(message) => return bad_request(&message),
};
match state.db.list_permissions(limit, params.offset.unwrap_or(0)).await {
Ok(page) => (StatusCode::OK, Json(page)).into_response(),
Err(e) => e.into_response(),
}
}
async fn get_permission(
State(state): State<Arc<RbacManagementState>>,
Path(permission_id): Path<String>,
) -> Response {
match state.db.get_permission(&permission_id).await {
Ok(perm) => (StatusCode::OK, Json(perm)).into_response(),
Err(e) => e.into_response(),
}
}
async fn delete_permission(
State(state): State<Arc<RbacManagementState>>,
Path(permission_id): Path<String>,
) -> Response {
match state.db.delete_permission(&permission_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => e.into_response(),
}
}
async fn assign_role(
State(state): State<Arc<RbacManagementState>>,
Json(payload): Json<AssignRoleRequest>,
) -> Response {
match state
.db
.assign_role_to_user(&payload.user_id, &payload.role_id, payload.tenant_id.as_deref())
.await
{
Ok(assignment) => (StatusCode::CREATED, Json(assignment)).into_response(),
Err(e) => e.into_response(),
}
}
async fn list_user_roles(
State(state): State<Arc<RbacManagementState>>,
params: Result<Query<UserRolesQuery>, axum::extract::rejection::QueryRejection>,
) -> Response {
let Ok(Query(params)) = params else {
return bad_request("user_id is required: GET /api/user-roles?user_id=<subject>");
};
let limit = match resolve_limit(params.limit) {
Ok(limit) => limit,
Err(message) => return bad_request(&message),
};
match state
.db
.list_user_roles(
¶ms.user_id,
params.tenant_id.as_deref(),
limit,
params.offset.unwrap_or(0),
)
.await
{
Ok(page) => (StatusCode::OK, Json(page)).into_response(),
Err(e) => e.into_response(),
}
}
async fn revoke_role(
State(state): State<Arc<RbacManagementState>>,
Path((user_id, role_id)): Path<(String, String)>,
) -> Response {
match state.db.revoke_role_from_user(&user_id, &role_id).await {
Ok(()) => StatusCode::NO_CONTENT.into_response(),
Err(e) => e.into_response(),
}
}
async fn query_permission_audit(
State(state): State<Arc<RbacManagementState>>,
Query(params): Query<AuditQuery>,
) -> Response {
let limit = match resolve_limit(params.limit) {
Ok(limit) => limit,
Err(message) => return bad_request(&message),
};
let start_time = match parse_time("start_time", params.start_time.as_ref()) {
Ok(t) => t,
Err(message) => return bad_request(&message),
};
let end_time = match parse_time("end_time", params.end_time.as_ref()) {
Ok(t) => t,
Err(message) => return bad_request(&message),
};
let filter = AuditFilter {
user_id: params.user_id.as_deref(),
role_id: params.role_id.as_deref(),
event_type: params.event_type.as_deref(),
tenant_id: params.tenant_id.as_deref(),
start_time,
end_time,
limit,
offset: params.offset.unwrap_or(0),
};
match state.db.query_audit(&filter).await {
Ok(page) => (StatusCode::OK, Json(page)).into_response(),
Err(e) => e.into_response(),
}
}
pub mod db_backend;
#[cfg(test)]
mod tests;