Implement comprehensive permission-based access control system with share management. Features: - Permission model (Read, Write, Admin) - Share model for resource sharing between users - Permission middleware for endpoint protection - Share management API endpoints - Permission check endpoints - MongoDB repository implementations for all models Files Added: - backend/src/db/permission.rs - Permission repository - backend/src/db/share.rs - Share repository - backend/src/db/user.rs - User repository - backend/src/db/profile.rs - Profile repository - backend/src/db/appointment.rs - Appointment repository - backend/src/db/family.rs - Family repository - backend/src/db/health_data.rs - Health data repository - backend/src/db/lab_result.rs - Lab results repository - backend/src/db/medication.rs - Medication repository - backend/src/db/mongodb_impl.rs - MongoDB trait implementations - backend/src/handlers/permissions.rs - Permission API handlers - backend/src/handlers/shares.rs - Share management handlers - backend/src/middleware/permission.rs - Permission checking middleware API Endpoints: - GET /api/permissions/check - Check user permissions - POST /api/shares - Create new share - GET /api/shares - List user shares - GET /api/shares/:id - Get specific share - PUT /api/shares/:id - Update share - DELETE /api/shares/:id - Delete share Status: Phase 2.5 COMPLETE - Building successfully, ready for production
362 lines
12 KiB
Rust
362 lines
12 KiB
Rust
use axum::{
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Json,
|
|
Extension,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use validator::Validate;
|
|
use mongodb::bson::oid::ObjectId;
|
|
|
|
use crate::{
|
|
auth::jwt::Claims,
|
|
config::AppState,
|
|
models::{share::Share, permission::Permission},
|
|
};
|
|
|
|
#[derive(Debug, Deserialize, Validate)]
|
|
pub struct CreateShareRequest {
|
|
pub target_user_email: String,
|
|
pub resource_type: String,
|
|
pub resource_id: Option<String>,
|
|
pub permissions: Vec<String>,
|
|
#[serde(default)]
|
|
pub expires_days: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ShareResponse {
|
|
pub id: String,
|
|
pub target_user_id: String,
|
|
pub resource_type: String,
|
|
pub resource_id: Option<String>,
|
|
pub permissions: Vec<String>,
|
|
pub expires_at: Option<String>,
|
|
pub created_at: String,
|
|
pub active: bool,
|
|
}
|
|
|
|
impl TryFrom<Share> for ShareResponse {
|
|
type Error = anyhow::Error;
|
|
|
|
fn try_from(share: Share) -> Result<Self, Self::Error> {
|
|
Ok(Self {
|
|
id: share.id.map(|id| id.to_string()).unwrap_or_default(),
|
|
target_user_id: share.target_user_id.to_string(),
|
|
resource_type: share.resource_type,
|
|
resource_id: share.resource_id.map(|id| id.to_string()),
|
|
permissions: share.permissions.into_iter().map(|p| p.to_string()).collect(),
|
|
expires_at: share.expires_at.map(|dt| dt.to_rfc3339()),
|
|
created_at: share.created_at.to_rfc3339(),
|
|
active: share.active,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub async fn create_share(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
Json(req): Json<CreateShareRequest>,
|
|
) -> impl IntoResponse {
|
|
if let Err(errors) = req.validate() {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "validation failed",
|
|
"details": errors.to_string()
|
|
}))).into_response();
|
|
}
|
|
|
|
// Find target user by email
|
|
let target_user = match state.db.find_user_by_email(&req.target_user_email).await {
|
|
Ok(Some(user)) => user,
|
|
Ok(None) => {
|
|
return (StatusCode::NOT_FOUND, Json(serde_json::json!({
|
|
"error": "target user not found"
|
|
}))).into_response();
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to find target user: {}", e);
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "database error"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
|
|
let target_user_id = match target_user.id {
|
|
Some(id) => id,
|
|
None => {
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "target user has no ID"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
|
|
let owner_id = match ObjectId::parse_str(&claims.sub) {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "invalid user ID format"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
|
|
// Parse resource_id if provided
|
|
let resource_id = match req.resource_id {
|
|
Some(id) => {
|
|
match ObjectId::parse_str(&id) {
|
|
Ok(oid) => Some(oid),
|
|
Err(_) => {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "invalid resource_id format"
|
|
}))).into_response();
|
|
}
|
|
}
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
// Parse permissions - support all permission types
|
|
let permissions: Vec<Permission> = req.permissions
|
|
.into_iter()
|
|
.filter_map(|p| match p.to_lowercase().as_str() {
|
|
"read" => Some(Permission::Read),
|
|
"write" => Some(Permission::Write),
|
|
"delete" => Some(Permission::Delete),
|
|
"share" => Some(Permission::Share),
|
|
"admin" => Some(Permission::Admin),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
if permissions.is_empty() {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "at least one valid permission is required (read, write, delete, share, admin)"
|
|
}))).into_response();
|
|
}
|
|
|
|
// Calculate expiration
|
|
let expires_at = req.expires_days.map(|days| {
|
|
chrono::Utc::now() + chrono::Duration::days(days as i64)
|
|
});
|
|
|
|
let share = Share::new(
|
|
owner_id.clone(),
|
|
target_user_id,
|
|
req.resource_type,
|
|
resource_id,
|
|
permissions,
|
|
expires_at,
|
|
);
|
|
|
|
match state.db.create_share(&share).await {
|
|
Ok(_) => {
|
|
let response: ShareResponse = match share.try_into() {
|
|
Ok(r) => r,
|
|
Err(_) => {
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to create share response"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
(StatusCode::CREATED, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to create share: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to create share"
|
|
}))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn list_shares(
|
|
State(state): State<AppState>,
|
|
Extension(claims): Extension<Claims>,
|
|
) -> impl IntoResponse {
|
|
let user_id = &claims.sub;
|
|
|
|
match state.db.list_shares_for_user(user_id).await {
|
|
Ok(shares) => {
|
|
let responses: Vec<ShareResponse> = shares
|
|
.into_iter()
|
|
.filter_map(|s| ShareResponse::try_from(s).ok())
|
|
.collect();
|
|
(StatusCode::OK, Json(responses)).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to list shares: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to list shares"
|
|
}))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn get_share(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> impl IntoResponse {
|
|
match state.db.get_share(&id).await {
|
|
Ok(Some(share)) => {
|
|
let response: ShareResponse = match share.try_into() {
|
|
Ok(r) => r,
|
|
Err(_) => {
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to create share response"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Ok(None) => {
|
|
(StatusCode::NOT_FOUND, Json(serde_json::json!({
|
|
"error": "share not found"
|
|
}))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get share: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to get share"
|
|
}))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate)]
|
|
pub struct UpdateShareRequest {
|
|
pub permissions: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
pub active: Option<bool>,
|
|
#[serde(default)]
|
|
pub expires_days: Option<u64>,
|
|
}
|
|
|
|
pub async fn update_share(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<String>,
|
|
Extension(claims): Extension<Claims>,
|
|
Json(req): Json<UpdateShareRequest>,
|
|
) -> impl IntoResponse {
|
|
// First get the share
|
|
let mut share = match state.db.get_share(&id).await {
|
|
Ok(Some(s)) => s,
|
|
Ok(None) => {
|
|
return (StatusCode::NOT_FOUND, Json(serde_json::json!({
|
|
"error": "share not found"
|
|
}))).into_response();
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get share: {}", e);
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to get share"
|
|
}))).into_response()
|
|
}
|
|
};
|
|
|
|
// Verify ownership
|
|
let owner_id = match ObjectId::parse_str(&claims.sub) {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "invalid user ID format"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
|
|
if share.owner_id != owner_id {
|
|
return (StatusCode::FORBIDDEN, Json(serde_json::json!({
|
|
"error": "not authorized to modify this share"
|
|
}))).into_response();
|
|
}
|
|
|
|
// Update fields
|
|
if let Some(permissions) = req.permissions {
|
|
share.permissions = permissions
|
|
.into_iter()
|
|
.filter_map(|p| match p.to_lowercase().as_str() {
|
|
"read" => Some(Permission::Read),
|
|
"write" => Some(Permission::Write),
|
|
"delete" => Some(Permission::Delete),
|
|
"share" => Some(Permission::Share),
|
|
"admin" => Some(Permission::Admin),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
if let Some(active) = req.active {
|
|
share.active = active;
|
|
}
|
|
|
|
if let Some(days) = req.expires_days {
|
|
share.expires_at = Some(chrono::Utc::now() + chrono::Duration::days(days as i64));
|
|
}
|
|
|
|
match state.db.update_share(&share).await {
|
|
Ok(_) => {
|
|
let response: ShareResponse = match share.try_into() {
|
|
Ok(r) => r,
|
|
Err(_) => {
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to create share response"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to update share: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to update share"
|
|
}))).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn delete_share(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<String>,
|
|
Extension(claims): Extension<Claims>,
|
|
) -> impl IntoResponse {
|
|
// First get the share to verify ownership
|
|
let share = match state.db.get_share(&id).await {
|
|
Ok(Some(s)) => s,
|
|
Ok(None) => {
|
|
return (StatusCode::NOT_FOUND, Json(serde_json::json!({
|
|
"error": "share not found"
|
|
}))).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to get share: {}", e);
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to get share"
|
|
}))).into_response()
|
|
}
|
|
};
|
|
|
|
// Verify ownership
|
|
let owner_id = match ObjectId::parse_str(&claims.sub) {
|
|
Ok(id) => id,
|
|
Err(_) => {
|
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
|
"error": "invalid user ID format"
|
|
}))).into_response();
|
|
}
|
|
};
|
|
|
|
if share.owner_id != owner_id {
|
|
return (StatusCode::FORBIDDEN, Json(serde_json::json!({
|
|
"error": "not authorized to delete this share"
|
|
}))).into_response()
|
|
}
|
|
|
|
match state.db.delete_share(&id).await {
|
|
Ok(_) => (StatusCode::NO_CONTENT, ()).into_response(),
|
|
Err(e) => {
|
|
tracing::error!("Failed to delete share: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
|
|
"error": "failed to delete share"
|
|
}))).into_response()
|
|
}
|
|
}
|
|
}
|