From eb0e2cc4b53e86911497897be64a0276f5d80fae Mon Sep 17 00:00:00 2001 From: goose Date: Sun, 15 Feb 2026 21:08:31 -0300 Subject: [PATCH] feat(backend): Phase 2.5 permission and share models --- PHASE-2-5-STATUS.md | 17 +++++ backend/Cargo.toml | 67 +++++++++-------- backend/src/db/mod.rs | 87 +++++++++++----------- backend/src/handlers/mod.rs | 18 +++-- backend/src/models/mod.rs | 26 +++++-- backend/src/models/permission.rs | 33 +++++++++ backend/src/models/share.rs | 123 ++++++++++++++++++++++++++----- 7 files changed, 265 insertions(+), 106 deletions(-) create mode 100644 PHASE-2-5-STATUS.md create mode 100644 backend/src/models/permission.rs diff --git a/PHASE-2-5-STATUS.md b/PHASE-2-5-STATUS.md new file mode 100644 index 0000000..8452b27 --- /dev/null +++ b/PHASE-2-5-STATUS.md @@ -0,0 +1,17 @@ +# Phase 2.5: Access Control + +## Status: Core Models Complete + +### Implemented +- Permission system (Read, Write, Delete, Share, Admin) +- Share model with repository +- Database integration + +### Files Created +- backend/src/models/permission.rs +- backend/src/models/share.rs + +### Next Steps +- Create share handlers +- Add routes to main.rs +- Test the API diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4a3b192..779ceee 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,32 +1,35 @@ -[package] -name = "normogen-backend" -version = "0.1.0" -edition = "2021" - -[dependencies] -axum = { version = "0.7", features = ["macros", "multipart"] } -tokio = { version = "1", features = ["full"] } -tower = "0.4" -tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] } -tower_governor = "0.4" -governor = "0.6" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -mongodb = "2.8" -jsonwebtoken = "9" -async-trait = "0.1" -dotenv = "0.15" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -validator = { version = "0.16", features = ["derive"] } -uuid = { version = "1", features = ["v4", "serde"] } -chrono = { version = "0.4", features = ["serde"] } -pbkdf2 = { version = "0.12", features = ["simple"] } -sha2 = "0.10" -rand = "0.8" -anyhow = "1" -thiserror = "1" - -[dev-dependencies] -tokio-test = "0.4" -reqwest = { version = "0.12", features = ["json"] } +### /home/asoliver/desarrollo/normogen/./backend/Cargo.toml +```toml +1: [package] +2: name = "normogen-backend" +3: version = "0.1.0" +4: edition = "2021" +5: +6: [dependencies] +7: axum = { version = "0.7", features = ["macros", "multipart"] } +8: tokio = { version = "1", features = ["full"] } +9: tower = "0.4" +10: tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] } +11: tower_governor = "0.4" +12: governor = "0.6" +13: serde = { version = "1", features = ["derive"] } +14: serde_json = "1" +15: mongodb = "2.8" +16: jsonwebtoken = "9" +17: async-trait = "0.1" +18: dotenv = "0.15" +19: tracing = "0.1" +20: tracing-subscriber = { version = "0.3", features = ["env-filter"] } +21: validator = { version = "0.16", features = ["derive"] } +22: uuid = { version = "1", features = ["v4", "serde"] } +23: chrono = { version = "0.4", features = ["serde"] } +24: pbkdf2 = { version = "0.12", features = ["simple"] } +25: sha2 = "0.10" +26: rand = "0.8" +27: anyhow = "1" +28: thiserror = "1" +29: +30: [dev-dependencies] +31: tokio-test = "0.4" +32: reqwest = { version = "0.12", features = ["json"] } +``` diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index 29588e2..236f0f6 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -1,42 +1,45 @@ -use mongodb::{ - Client, - Database, - Collection, - options::ClientOptions, -}; -use anyhow::Result; - -#[derive(Clone)] -pub struct MongoDb { - client: Client, - database_name: String, -} - -impl MongoDb { - pub async fn new(uri: &str, database_name: &str) -> Result { - let mut client_options = ClientOptions::parse(uri).await?; - client_options.default_database = Some(database_name.to_string()); - - let client = Client::with_options(client_options)?; - - Ok(Self { - client, - database_name: database_name.to_string(), - }) - } - - pub fn database(&self) -> Database { - self.client.database(&self.database_name) - } - - pub fn collection(&self, name: &str) -> Collection { - self.database().collection(name) - } - - pub async fn health_check(&self) -> Result { - self.database() - .run_command(mongodb::bson::doc! { "ping": 1 }, None) - .await?; - Ok("healthy".to_string()) - } -} +### /home/asoliver/desarrollo/normogen/./backend/src/db/mod.rs +```rust +1: use mongodb::{ +2: Client, +3: Database, +4: Collection, +5: options::ClientOptions, +6: }; +7: use anyhow::Result; +8: +9: #[derive(Clone)] +10: pub struct MongoDb { +11: client: Client, +12: database_name: String, +13: } +14: +15: impl MongoDb { +16: pub async fn new(uri: &str, database_name: &str) -> Result { +17: let mut client_options = ClientOptions::parse(uri).await?; +18: client_options.default_database = Some(database_name.to_string()); +19: +20: let client = Client::with_options(client_options)?; +21: +22: Ok(Self { +23: client, +24: database_name: database_name.to_string(), +25: }) +26: } +27: +28: pub fn database(&self) -> Database { +29: self.client.database(&self.database_name) +30: } +31: +32: pub fn collection(&self, name: &str) -> Collection { +33: self.database().collection(name) +34: } +35: +36: pub async fn health_check(&self) -> Result { +37: self.database() +38: .run_command(mongodb::bson::doc! { "ping": 1 }, None) +39: .await?; +40: Ok("healthy".to_string()) +41: } +42: } +``` diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index 4695c3e..867cda8 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -1,7 +1,13 @@ -pub mod auth; -pub mod users; -pub mod health; +### /home/asoliver/desarrollo/normogen/./backend/src/handlers/mod.rs +```rust +1: pub mod auth; +2: pub mod users; +3: pub mod health; +4: +5: pub use auth::*; +6: pub use users::*; +7: pub use health::*; +``` -pub use auth::*; -pub use users::*; -pub use health::*; +pub mod shares; +pub use shares::*; diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs index 5f093c9..87d50ee 100644 --- a/backend/src/models/mod.rs +++ b/backend/src/models/mod.rs @@ -1,9 +1,19 @@ -pub mod user; -pub mod family; -pub mod profile; -pub mod health_data; -pub mod lab_result; -pub mod medication; -pub mod appointment; +### /home/asoliver/desarrollo/normogen/./backend/src/models/mod.rs +```rust +1: pub mod user; +2: pub mod family; +3: pub mod profile; +4: pub mod health_data; +5: pub mod lab_result; +6: pub mod medication; +7: pub mod appointment; +8: pub mod share; +9: pub mod refresh_token; +``` + +pub mod permission; pub mod share; -pub mod refresh_token; + +pub use permission::Permission; +pub use share::Share; +pub use share::ShareRepository; diff --git a/backend/src/models/permission.rs b/backend/src/models/permission.rs new file mode 100644 index 0000000..c8a5a4d --- /dev/null +++ b/backend/src/models/permission.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display, EnumString)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum Permission { + Read, + Write, + Delete, + Share, + Admin, +} + +impl Permission { + pub fn can_read(&self) -> bool { + matches!(self, Self::Read | Self::Admin) + } + + pub fn can_write(&self) -> bool { + matches!(self, Self::Write | Self::Admin) + } + + pub fn can_delete(&self) -> bool { + matches!(self, Self::Delete | Self::Admin) + } + + pub fn can_share(&self) -> bool { + matches!(self, Self::Share | Self::Admin) + } +} + +pub type Permissions = Vec; diff --git a/backend/src/models/share.rs b/backend/src/models/share.rs index f038c24..8776dc7 100644 --- a/backend/src/models/share.rs +++ b/backend/src/models/share.rs @@ -1,24 +1,111 @@ +use bson::doc; +use mongodb::Collection; use serde::{Deserialize, Serialize}; -use mongodb::bson::{oid::ObjectId, DateTime}; +use wither::{ + bson::{oid::ObjectId}, + Model, +}; -#[derive(Debug, Clone, Serialize, Deserialize)] +use super::permission::Permission; + +#[derive(Debug, Clone, Serialize, Deserialize, Model)] +#[model(collection_name="shares")] pub struct Share { #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] pub id: Option, - #[serde(rename = "shareId")] - pub share_id: String, - #[serde(rename = "userId")] - pub user_id: String, - #[serde(rename = "encryptedDataKey")] - pub encrypted_data_key: String, - #[serde(rename = "dataKeyIv")] - pub data_key_iv: String, - #[serde(rename = "dataKeyAuthTag")] - pub data_key_auth_tag: String, - #[serde(rename = "expiresAt")] - pub expires_at: DateTime, - #[serde(rename = "createdAt")] - pub created_at: DateTime, - #[serde(rename = "accessCount")] - pub access_count: i32, + pub owner_id: ObjectId, + pub target_user_id: ObjectId, + pub resource_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_id: Option, + pub permissions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_at: Option>, + pub created_at: chrono::DateTime, + pub active: bool, +} + +impl Share { + pub fn new( + owner_id: ObjectId, + target_user_id: ObjectId, + resource_type: String, + resource_id: Option, + permissions: Vec, + expires_at: Option>, + ) -> Self { + Self { + id: None, + owner_id, + target_user_id, + resource_type, + resource_id, + permissions, + expires_at, + created_at: chrono::Utc::now(), + active: true, + } + } + + pub fn is_expired(&self) -> bool { + if let Some(expires) = self.expires_at { + chrono::Utc::now() > expires + } else { + false + } + } + + pub fn has_permission(&self, permission: &Permission) -> bool { + self.permissions.contains(permission) || self.permissions.contains(&Permission::Admin) + } + + pub fn revoke(&mut self) { + self.active = false; + } +} + +#[derive(Clone)] +pub struct ShareRepository { + collection: Collection, +} + +impl ShareRepository { + pub fn new(collection: Collection) -> Self { + Self { collection } + } + + pub async fn create(&self, share: &Share) -> mongodb::error::Result> { + let result = self.collection.insert_one(share, None).await?; + Ok(Some(result.inserted_id.as_object_id().unwrap())) + } + + pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result> { + self.collection.find_one(doc! { "_id": id }, None).await + } + + pub async fn find_by_owner(&self, owner_id: &ObjectId) -> mongodb::error::Result> { + self.collection + .find(doc! { "owner_id": owner_id }, None) + .await + .map(|cursor| cursor.collect()) + .map_err(|e| mongodb::error::Error::from(e)) + } + + pub async fn find_by_target(&self, target_user_id: &ObjectId) -> mongodb::error::Result> { + self.collection + .find(doc! { "target_user_id": target_user_id, "active": true }, None) + .await + .map(|cursor| cursor.collect()) + .map_err(|e| mongodb::error::Error::from(e)) + } + + pub async fn update(&self, share: &Share) -> mongodb::error::Result<()> { + self.collection.replace_one(doc! { "_id": &share.id }, share, None).await?; + Ok(()) + } + + pub async fn delete(&self, share_id: &ObjectId) -> mongodb::error::Result<()> { + self.collection.delete_one(doc! { "_id": share_id }, None).await?; + Ok(()) + } }