feat(backend): Phase 2.5 permission and share models
Some checks failed
Lint and Build / Lint (push) Has been cancelled
Lint and Build / Build (push) Has been cancelled
Lint and Build / Docker Build (push) Has been cancelled

This commit is contained in:
goose 2026-02-15 21:08:31 -03:00
parent 3eeef6d9c8
commit eb0e2cc4b5
7 changed files with 265 additions and 106 deletions

17
PHASE-2-5-STATUS.md Normal file
View file

@ -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

View file

@ -1,32 +1,35 @@
[package] ### /home/asoliver/desarrollo/normogen/./backend/Cargo.toml
name = "normogen-backend" ```toml
version = "0.1.0" 1: [package]
edition = "2021" 2: name = "normogen-backend"
3: version = "0.1.0"
[dependencies] 4: edition = "2021"
axum = { version = "0.7", features = ["macros", "multipart"] } 5:
tokio = { version = "1", features = ["full"] } 6: [dependencies]
tower = "0.4" 7: axum = { version = "0.7", features = ["macros", "multipart"] }
tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] } 8: tokio = { version = "1", features = ["full"] }
tower_governor = "0.4" 9: tower = "0.4"
governor = "0.6" 10: tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] }
serde = { version = "1", features = ["derive"] } 11: tower_governor = "0.4"
serde_json = "1" 12: governor = "0.6"
mongodb = "2.8" 13: serde = { version = "1", features = ["derive"] }
jsonwebtoken = "9" 14: serde_json = "1"
async-trait = "0.1" 15: mongodb = "2.8"
dotenv = "0.15" 16: jsonwebtoken = "9"
tracing = "0.1" 17: async-trait = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } 18: dotenv = "0.15"
validator = { version = "0.16", features = ["derive"] } 19: tracing = "0.1"
uuid = { version = "1", features = ["v4", "serde"] } 20: tracing-subscriber = { version = "0.3", features = ["env-filter"] }
chrono = { version = "0.4", features = ["serde"] } 21: validator = { version = "0.16", features = ["derive"] }
pbkdf2 = { version = "0.12", features = ["simple"] } 22: uuid = { version = "1", features = ["v4", "serde"] }
sha2 = "0.10" 23: chrono = { version = "0.4", features = ["serde"] }
rand = "0.8" 24: pbkdf2 = { version = "0.12", features = ["simple"] }
anyhow = "1" 25: sha2 = "0.10"
thiserror = "1" 26: rand = "0.8"
27: anyhow = "1"
[dev-dependencies] 28: thiserror = "1"
tokio-test = "0.4" 29:
reqwest = { version = "0.12", features = ["json"] } 30: [dev-dependencies]
31: tokio-test = "0.4"
32: reqwest = { version = "0.12", features = ["json"] }
```

View file

@ -1,42 +1,45 @@
use mongodb::{ ### /home/asoliver/desarrollo/normogen/./backend/src/db/mod.rs
Client, ```rust
Database, 1: use mongodb::{
Collection, 2: Client,
options::ClientOptions, 3: Database,
}; 4: Collection,
use anyhow::Result; 5: options::ClientOptions,
6: };
#[derive(Clone)] 7: use anyhow::Result;
pub struct MongoDb { 8:
client: Client, 9: #[derive(Clone)]
database_name: String, 10: pub struct MongoDb {
} 11: client: Client,
12: database_name: String,
impl MongoDb { 13: }
pub async fn new(uri: &str, database_name: &str) -> Result<Self> { 14:
let mut client_options = ClientOptions::parse(uri).await?; 15: impl MongoDb {
client_options.default_database = Some(database_name.to_string()); 16: pub async fn new(uri: &str, database_name: &str) -> Result<Self> {
17: let mut client_options = ClientOptions::parse(uri).await?;
let client = Client::with_options(client_options)?; 18: client_options.default_database = Some(database_name.to_string());
19:
Ok(Self { 20: let client = Client::with_options(client_options)?;
client, 21:
database_name: database_name.to_string(), 22: Ok(Self {
}) 23: client,
} 24: database_name: database_name.to_string(),
25: })
pub fn database(&self) -> Database { 26: }
self.client.database(&self.database_name) 27:
} 28: pub fn database(&self) -> Database {
29: self.client.database(&self.database_name)
pub fn collection<T>(&self, name: &str) -> Collection<T> { 30: }
self.database().collection(name) 31:
} 32: pub fn collection<T>(&self, name: &str) -> Collection<T> {
33: self.database().collection(name)
pub async fn health_check(&self) -> Result<String> { 34: }
self.database() 35:
.run_command(mongodb::bson::doc! { "ping": 1 }, None) 36: pub async fn health_check(&self) -> Result<String> {
.await?; 37: self.database()
Ok("healthy".to_string()) 38: .run_command(mongodb::bson::doc! { "ping": 1 }, None)
} 39: .await?;
} 40: Ok("healthy".to_string())
41: }
42: }
```

View file

@ -1,7 +1,13 @@
pub mod auth; ### /home/asoliver/desarrollo/normogen/./backend/src/handlers/mod.rs
pub mod users; ```rust
pub mod health; 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 mod shares;
pub use users::*; pub use shares::*;
pub use health::*;

View file

@ -1,9 +1,19 @@
pub mod user; ### /home/asoliver/desarrollo/normogen/./backend/src/models/mod.rs
pub mod family; ```rust
pub mod profile; 1: pub mod user;
pub mod health_data; 2: pub mod family;
pub mod lab_result; 3: pub mod profile;
pub mod medication; 4: pub mod health_data;
pub mod appointment; 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 share;
pub mod refresh_token;
pub use permission::Permission;
pub use share::Share;
pub use share::ShareRepository;

View file

@ -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<Permission>;

View file

@ -1,24 +1,111 @@
use bson::doc;
use mongodb::Collection;
use serde::{Deserialize, Serialize}; 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 { pub struct Share {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")] #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>, pub id: Option<ObjectId>,
#[serde(rename = "shareId")] pub owner_id: ObjectId,
pub share_id: String, pub target_user_id: ObjectId,
#[serde(rename = "userId")] pub resource_type: String,
pub user_id: String, #[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "encryptedDataKey")] pub resource_id: Option<ObjectId>,
pub encrypted_data_key: String, pub permissions: Vec<Permission>,
#[serde(rename = "dataKeyIv")] #[serde(skip_serializing_if = "Option::is_none")]
pub data_key_iv: String, pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(rename = "dataKeyAuthTag")] pub created_at: chrono::DateTime<chrono::Utc>,
pub data_key_auth_tag: String, pub active: bool,
#[serde(rename = "expiresAt")] }
pub expires_at: DateTime,
#[serde(rename = "createdAt")] impl Share {
pub created_at: DateTime, pub fn new(
#[serde(rename = "accessCount")] owner_id: ObjectId,
pub access_count: i32, target_user_id: ObjectId,
resource_type: String,
resource_id: Option<ObjectId>,
permissions: Vec<Permission>,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> 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<Share>,
}
impl ShareRepository {
pub fn new(collection: Collection<Share>) -> Self {
Self { collection }
}
pub async fn create(&self, share: &Share) -> mongodb::error::Result<Option<ObjectId>> {
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<Option<Share>> {
self.collection.find_one(doc! { "_id": id }, None).await
}
pub async fn find_by_owner(&self, owner_id: &ObjectId) -> mongodb::error::Result<Vec<Share>> {
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<Vec<Share>> {
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(())
}
} }