From d1b12e1562ad37e4974a05ab630f6fa0d6e3571f Mon Sep 17 00:00:00 2001 From: "Philip (a-0)" <@ph:a-0.me> Date: Sun, 21 Jan 2024 12:24:05 +0100 Subject: [PATCH] Simple pot-based access control --- ubisync-lib/src/api/app.rs | 23 +++- ubisync-lib/src/api/element.rs | 3 +- ubisync-lib/src/messages/mod.rs | 7 +- ubisync-lib/src/types/element.rs | 16 ++- ubisync-lib/src/types/pot.rs | 4 +- ubisync/src/api/v0/app.rs | 16 ++- ubisync/src/api/v0/element.rs | 30 ++++- ubisync/src/api/v0/mod.rs | 4 +- ubisync/src/comm/message_processor.rs | 9 +- ubisync/src/state/api_state.rs | 58 ++++++--- ubisync/src/state/comm_state.rs | 30 ++++- ubisync/src/state/queries/apps.rs | 163 +++++++++++++++++++++++--- ubisync/src/state/queries/elements.rs | 34 +++++- ubisync/src/state/schema.rs | 1 + ubisync/tests/api.rs | 6 +- 15 files changed, 340 insertions(+), 64 deletions(-) diff --git a/ubisync-lib/src/api/app.rs b/ubisync-lib/src/api/app.rs index bd89031..22a16d9 100644 --- a/ubisync-lib/src/api/app.rs +++ b/ubisync-lib/src/api/app.rs @@ -1,7 +1,7 @@ use reqwest::Method; use serde::{Deserialize, Serialize}; -use crate::types::PotId; +use crate::types::{PotId, Pot}; use super::UbisyncRequest; @@ -68,6 +68,27 @@ impl UbisyncRequest for AppSetDefaultPotRequest { Method::POST } + fn path(&self, _: Self::PathParameters) -> String { + "/app/pot/default".to_string() + } +} + +#[derive(Serialize, Deserialize)] +pub struct AppGetDefaultPotRequest; + +#[derive(Serialize, Deserialize)] +pub struct AppGetDefaultPotResponse { + pub pot: Pot, +} + +impl UbisyncRequest for AppGetDefaultPotRequest { + type PathParameters = (); + type Response = AppGetDefaultPotResponse; + + fn method(&self) -> Method { + Method::GET + } + fn path(&self, _: Self::PathParameters) -> String { "/app/pot/default".to_string() } diff --git a/ubisync-lib/src/api/element.rs b/ubisync-lib/src/api/element.rs index 64447bb..2155132 100644 --- a/ubisync-lib/src/api/element.rs +++ b/ubisync-lib/src/api/element.rs @@ -1,13 +1,14 @@ use reqwest::Method; use serde::{Deserialize, Serialize}; -use crate::types::{Element, ElementContent, ElementId}; +use crate::types::{Element, ElementContent, ElementId, PotId}; use super::UbisyncRequest; #[derive(Serialize, Deserialize)] pub struct ElementCreateRequest { pub content: ElementContent, + pub pot: Option, } #[derive(Serialize, Deserialize)] diff --git a/ubisync-lib/src/messages/mod.rs b/ubisync-lib/src/messages/mod.rs index a36c97b..9e4f3cc 100644 --- a/ubisync-lib/src/messages/mod.rs +++ b/ubisync-lib/src/messages/mod.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::types::{ElementContent, ElementId, MessageId}; +use crate::types::{ElementContent, ElementId, MessageId, PotId}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Message { @@ -22,6 +22,7 @@ pub enum MessageContent { CreateElement { id: ElementId, content: ElementContent, + pot: PotId, }, SetElement { id: ElementId, @@ -30,6 +31,10 @@ pub enum MessageContent { RemoveElement { id: ElementId, }, + AddPot { + id: PotId, + app_type: String, + } } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/ubisync-lib/src/types/element.rs b/ubisync-lib/src/types/element.rs index 269d951..dd3a6ee 100644 --- a/ubisync-lib/src/types/element.rs +++ b/ubisync-lib/src/types/element.rs @@ -1,23 +1,26 @@ use serde::{Deserialize, Serialize}; -use super::{ElementContent, ElementId, MessageId}; +use super::{ElementContent, ElementId, MessageId, PotId}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Element { // Uuid identifying the element itself id: ElementId, + pot: Option, content: ElementContent, latest_message: Option, local_changes: bool, } -impl From<(ElementId, ElementContent, Option, bool)> for Element { - fn from(value: (ElementId, ElementContent, Option, bool)) -> Self { + +impl From<(ElementId, Option, ElementContent, Option, bool)> for Element { + fn from(value: (ElementId, Option, ElementContent, Option, bool)) -> Self { Element { id: value.0, - content: value.1, - latest_message: value.2, - local_changes: value.3, + pot: value.1, + content: value.2, + latest_message: value.3, + local_changes: value.4, } } } @@ -27,6 +30,7 @@ impl Element { // A new element with no latest message must have local changes Element { id: id, + pot: None, content: content, latest_message: None, local_changes: true, diff --git a/ubisync-lib/src/types/pot.rs b/ubisync-lib/src/types/pot.rs index 324bd20..a0a58e2 100644 --- a/ubisync-lib/src/types/pot.rs +++ b/ubisync-lib/src/types/pot.rs @@ -4,8 +4,8 @@ use super::PotId; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Pot { - id: PotId, - app_type: String, + pub id: PotId, + pub app_type: String, } impl Pot { diff --git a/ubisync/src/api/v0/app.rs b/ubisync/src/api/v0/app.rs index 95c517f..f5bcdda 100644 --- a/ubisync/src/api/v0/app.rs +++ b/ubisync/src/api/v0/app.rs @@ -13,7 +13,7 @@ use jsonwebtoken::{decode, Header}; use serde::{Deserialize, Serialize}; use serde_with::chrono::{DateTime, Utc}; use tracing::{debug, error, warn}; -use ubisync_lib::{api::app::{AppRegisterRequest, AppRegisterResponse, AppSetDefaultPotRequest, AppSetDefaultPotResponse, AppCreatePotRequest, AppCreatePotResponse}, types::PotId}; +use ubisync_lib::{api::app::{AppRegisterRequest, AppRegisterResponse, AppSetDefaultPotRequest, AppSetDefaultPotResponse, AppCreatePotRequest, AppCreatePotResponse, AppGetDefaultPotRequest, AppGetDefaultPotResponse}, types::PotId}; use uuid::Uuid; use crate::state::ApiState; @@ -120,6 +120,20 @@ pub(super) async fn set_default_pot( } } +pub(super) async fn get_default_pot( + s: Extension>, + app_id: Extension, + Json(_): Json, +) -> Response { + match s.get_default_pot(&app_id.0) { + Ok(p) => (StatusCode::OK, Json {0: AppGetDefaultPotResponse { pot: p }}).into_response(), + Err(e) => { + warn!("No default pot found: {}", e); + StatusCode::NOT_FOUND.into_response() + } + } +} + pub(super) async fn create_pot( s: Extension>, app_id: Extension, diff --git a/ubisync/src/api/v0/element.rs b/ubisync/src/api/v0/element.rs index fe5cd51..d289bf4 100644 --- a/ubisync/src/api/v0/element.rs +++ b/ubisync/src/api/v0/element.rs @@ -6,7 +6,7 @@ use axum::{ response::{IntoResponse, Response}, Extension, }; -use tracing::debug; +use tracing::{debug, warn}; use crate::state::ApiState; use ubisync_lib::{ @@ -17,8 +17,10 @@ use ubisync_lib::{ types::ElementId, }; -pub(super) async fn get(Path(id): Path, s: Extension>) -> Response { - let element = s.get_element(&id); +use super::app::AppId; + +pub(super) async fn get(Path(id): Path, app: Extension, s: Extension>) -> Response { + let element = s.get_element(&id, &app); match element { Ok(el) => ( StatusCode::OK, @@ -27,15 +29,30 @@ pub(super) async fn get(Path(id): Path, s: Extension>) }, ) .into_response(), - Err(_) => StatusCode::NOT_FOUND.into_response(), + Err(e) => { + warn!("Could not get element: {}", e); + StatusCode::NOT_FOUND.into_response() + }, } } pub(super) async fn create( + app: Extension, s: Extension>, Json(req): Json, ) -> Response { - let element_id = s.create_element(&req.content); + let pot_id = match req.pot { + Some(p) => p, + None => match s.get_default_pot(&app.0) { + Ok(p) => p.id, + Err(e) => { + warn!("Element create request did not provide pot id, and no default pot for requesting app was found: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR.into_response() + }, + }, + }; + + let element_id = s.create_element(&req.content, &pot_id); debug!("{:?}", element_id); match element_id { Ok(id) => ( @@ -51,10 +68,11 @@ pub(super) async fn create( pub(super) async fn set( Path(id): Path, + app: Extension, s: Extension>, Json(req): Json, ) -> Response { - let res = s.write_element_content(&id, &req.content); + let res = s.write_element_content(&id, &app, &req.content); match res { Ok(_) => ( StatusCode::OK, diff --git a/ubisync/src/api/v0/mod.rs b/ubisync/src/api/v0/mod.rs index be5b582..b680948 100644 --- a/ubisync/src/api/v0/mod.rs +++ b/ubisync/src/api/v0/mod.rs @@ -1,6 +1,6 @@ use axum::{ middleware::from_fn, - routing::{get, put, post}, + routing::{get, put}, Extension, Router, }; use std::sync::Arc; @@ -14,7 +14,7 @@ pub fn get_router(state: ApiState) -> Router { Router::new() // authenticated routes .route("/app/pot", put(app::create_pot)) - .route("/app/pot/default", post(app::set_default_pot)) + .route("/app/pot/default", get(app::get_default_pot).post(app::set_default_pot)) .route("/element", put(element::create)) .route( "/element/:id", diff --git a/ubisync/src/comm/message_processor.rs b/ubisync/src/comm/message_processor.rs index 6ef2282..e07af95 100644 --- a/ubisync/src/comm/message_processor.rs +++ b/ubisync/src/comm/message_processor.rs @@ -12,9 +12,9 @@ pub fn handle(state: &CommState, peer: &PeerId, message: Message) { MessageContent::Hello { peer_name } => { state.set_peer(peer, peer_name).expect("State failed"); } - MessageContent::CreateElement { id, content } => { + MessageContent::CreateElement { id, content , pot} => { state - .add_received_element(id, content, message.id()) + .add_received_element(id, content, message.id(), pot) .expect("State failed"); } MessageContent::SetElement { id, content } => { @@ -24,6 +24,11 @@ pub fn handle(state: &CommState, peer: &PeerId, message: Message) { } MessageContent::RemoveElement { id } => { state.remove_element(id).expect("State failed"); + }, + MessageContent::AddPot { id, app_type } => { + state.add_pot(id, app_type).expect("State failed"); + //TODO: remove when setting default pot properly is possible + let _ = state.set_default_pot_for_all_apps(id); } } } diff --git a/ubisync/src/state/api_state.rs b/ubisync/src/state/api_state.rs index 673a101..5f5804b 100644 --- a/ubisync/src/state/api_state.rs +++ b/ubisync/src/state/api_state.rs @@ -1,12 +1,13 @@ use std::sync::Arc; +use anyhow::Error; use cozo::DbInstance; use jsonwebtoken::{DecodingKey, EncodingKey, Validation}; use serde_with::chrono::Utc; use tracing::debug; use ubisync_lib::{ messages::MessageContent, - types::{Element, ElementContent, ElementId, PotId}, + types::{Element, ElementContent, ElementId, PotId, Pot}, }; use crate::{ @@ -52,14 +53,19 @@ impl ApiState { queries::apps::get(self.db(), id) } - pub fn create_element(&self, content: &ElementContent) -> anyhow::Result { + pub fn create_element( + &self, + content: &ElementContent, + pot: &PotId, + ) -> anyhow::Result { let id = ElementId::new(); - queries::elements::add(self.db(), &id, &content, None, true)?; + queries::elements::add(self.db(), &id, &content, None, true, pot)?; debug!("Added element {{{}}}", &id.to_string()); self.state.send_to_peers(MessageContent::CreateElement { id: id.clone(), content: content.clone(), + pot: pot.clone(), }); Ok(id) } @@ -67,13 +73,21 @@ impl ApiState { pub fn write_element_content( &self, id: &ElementId, + app: &AppId, content: &ElementContent, ) -> anyhow::Result<()> { - queries::elements::set_content(self.db(), id, content)?; - queries::elements::set_local_changes(self.db(), id, true)?; - debug!("Wrote element content {{{}}}", &id.to_string()); + if queries::elements::get_app_access(self.db(), id, app)? { + queries::elements::set_content(self.db(), id, content)?; + queries::elements::set_local_changes(self.db(), id, true)?; + debug!("Wrote element content {{{}}}", &id.to_string()); - Ok(()) + Ok(()) + } + else { + Err(Error::msg( + "Element does not exist or app does not have access to it", + )) + } } pub fn remove_element(&self, id: &ElementId) -> anyhow::Result<()> { @@ -87,14 +101,26 @@ impl ApiState { queries::apps::set_default_pot(self.db(), app_id, pot_id) } + pub fn get_default_pot(&self, app_id: &AppId) -> anyhow::Result { + queries::apps::get_default_pot(self.db(), app_id) + } + pub fn create_pot(&self, app_id: &AppId, app_type: &str) -> anyhow::Result { let pot_id = PotId::new(); queries::apps::create_pot(self.db(), &pot_id, app_id, app_type)?; + + self.state.send_to_peers(MessageContent::AddPot { id: pot_id.to_owned(), app_type: app_type.to_string() }); Ok(pot_id) } - pub fn get_element(&self, id: &ElementId) -> anyhow::Result { - self.state.get_element(id) + pub fn get_element(&self, id: &ElementId, app: &AppId) -> anyhow::Result { + if queries::elements::get_app_access(self.db(), id, app)? { + self.state.get_element(id) + } else { + Err(Error::msg( + "Element does not exist or app does not have access to it", + )) + } } fn db(&self) -> &DbInstance { @@ -135,10 +161,12 @@ mod tests { State::new("mem").await.unwrap(), "abcdabcdabcdabcdabcdabcdabcdabcd", ); + let app_id = state.add_app("appname", "appdesc", "apptype").unwrap(); + let pot_id = state.create_pot(&app_id, "apptype").unwrap(); let id = state - .create_element(&ElementContent::Text("Test-text".to_string())) + .create_element(&ElementContent::Text("Test-text".to_string()), &pot_id) .unwrap(); - let el = state.get_element(&id).unwrap(); + let el = state.get_element(&id, &app_id).unwrap(); assert_eq!( ElementContent::Text("Test-text".to_string()), el.content().to_owned() @@ -157,13 +185,15 @@ mod tests { State::new("mem").await.unwrap(), "abcdabcdabcdabcdabcdabcdabcdabcd", ); + let app_id = state.add_app("appname", "appdesc", "apptype").unwrap(); + let pot_id = state.create_pot(&app_id, "apptype").unwrap(); let id = state - .create_element(&ElementContent::Text("Test-text".to_string())) + .create_element(&ElementContent::Text("Test-text".to_string()), &pot_id) .unwrap(); state - .write_element_content(&id, &ElementContent::Text("Test-text 2".to_string())) + .write_element_content(&id, &app_id, &ElementContent::Text("Test-text 2".to_string())) .unwrap(); - let el = state.get_element(&id).unwrap(); + let el = state.get_element(&id, &app_id).unwrap(); assert_eq!( ElementContent::Text("Test-text 2".to_string()), el.content().to_owned() diff --git a/ubisync/src/state/comm_state.rs b/ubisync/src/state/comm_state.rs index 77049ac..e63a798 100644 --- a/ubisync/src/state/comm_state.rs +++ b/ubisync/src/state/comm_state.rs @@ -1,11 +1,12 @@ use std::sync::Arc; +use anyhow::Error; use cozo::DbInstance; use tracing::debug; use ubisync_lib::{ peer::Peer, - types::{Element, ElementContent, ElementId, MessageId, PeerId}, + types::{Element, ElementContent, ElementId, MessageId, PeerId, PotId}, }; use crate::state::queries; @@ -27,6 +28,7 @@ impl CommState { id: &ElementId, content: &ElementContent, latest_message: &MessageId, + pot_id: &PotId, ) -> anyhow::Result<()> { queries::elements::add( self.db(), @@ -34,6 +36,7 @@ impl CommState { &content, Some(latest_message.to_owned()), false, + pot_id, )?; debug!("Added element {{{}}}", &id.to_string()); @@ -76,6 +79,25 @@ impl CommState { self.state.get_peers() } + pub fn add_pot(&self, id: &PotId, app_type: &str) -> anyhow::Result<()> { + queries::pots::add(self.db(), id, app_type) + } + + pub fn set_default_pot_for_all_apps(&self, id: &PotId) -> anyhow::Result<()> { + if let Ok(apps) = queries::apps::get_all_ids(self.db()) { + for app in apps { + let res = queries::apps::set_default_pot(self.db(), &app, id); + if let Ok(_) = res { + debug!("Set {:?} as default for app {:?}", id, &app); + } + } + Ok(()) + } + else { + Err(Error::msg("Could not fetch list of all apps")) + } + } + fn db(&self) -> &DbInstance { &self.state.db } @@ -88,7 +110,7 @@ mod tests { use super::CommState; use tracing::Level; - use ubisync_lib::types::{ElementContent, ElementId, MessageId}; + use ubisync_lib::types::{ElementContent, ElementId, MessageId, PotId}; #[tokio::test] #[serial_test::serial] @@ -100,11 +122,13 @@ mod tests { let state = CommState::new(State::new("mem").await.unwrap()); let id = ElementId::new(); + let pot_id = PotId::new(); state .add_received_element( &id, &ElementContent::Text("Test-text".to_string()), &MessageId::new(), + &pot_id, ) .unwrap(); let el = state.get_element(&id).unwrap(); @@ -124,11 +148,13 @@ mod tests { let state = CommState::new(State::new("mem").await.unwrap()); let id = ElementId::new(); + let pot_id = PotId::new(); state .add_received_element( &id, &ElementContent::Text("Test-text".to_string()), &MessageId::new(), + &pot_id, ) .unwrap(); state diff --git a/ubisync/src/state/queries/apps.rs b/ubisync/src/state/queries/apps.rs index de38294..b12f0f8 100644 --- a/ubisync/src/state/queries/apps.rs +++ b/ubisync/src/state/queries/apps.rs @@ -3,9 +3,13 @@ use std::collections::BTreeMap; use anyhow::{bail, Error}; use cozo::{DataValue, DbInstance, Num, ScriptMutability}; use serde_with::chrono::{DateTime, Utc}; -use ubisync_lib::types::PotId; +use tracing::warn; +use ubisync_lib::types::{Pot, PotId}; -use crate::{api::v0::app::{AppId, App}, run_query}; +use crate::{ + api::v0::app::{App, AppId}, + run_query, +}; pub fn add( db: &DbInstance, @@ -61,6 +65,30 @@ pub fn exists(db: &DbInstance, id: &AppId) -> anyhow::Result { Err(Error::msg("Could not check whether app is registered")) } +pub fn get_all_ids(db: &DbInstance) -> anyhow::Result> { + let result = db.run_script( + "?[id] := *apps{id}", + BTreeMap::new(), + ScriptMutability::Immutable, + ); + + match result { + Ok(named_rows) => Ok(named_rows + .rows + .iter() + .filter_map(|row| { + if let [DataValue::Str(app_id)] = row.as_slice() { + serde_json::from_str::(app_id).ok() + } + else { + None + } + }) + .collect()), + Err(e) => bail!(e), + } +} + pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result { let mut params = BTreeMap::new(); params.insert( @@ -76,13 +104,23 @@ pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result { if let Ok(rows) = result { if let Some(firstrow) = rows.rows.first() { - if let [DataValue::Num(Num::Int(ts)), DataValue::Str(app_type), DataValue::Str(name), DataValue::Str(desc), default_pot] = firstrow.as_slice() { - let last_access = DateTime::from_timestamp(ts.to_owned(), 0).ok_or(Error::msg("Failed to deserialize timestamp"))?; + if let [DataValue::Num(Num::Int(ts)), DataValue::Str(app_type), DataValue::Str(name), DataValue::Str(desc), default_pot] = + firstrow.as_slice() + { + let last_access = DateTime::from_timestamp(ts.to_owned(), 0) + .ok_or(Error::msg("Failed to deserialize timestamp"))?; let pot_id = match default_pot { DataValue::Str(dpid) => Some(serde_json::from_str(dpid)?), _ => None, }; - return Ok(App {id: id.clone(), app_type: app_type.to_string(), last_access, name: name.to_string(), description: desc.to_string(), default_pot: pot_id}); + return Ok(App { + id: id.clone(), + app_type: app_type.to_string(), + last_access, + name: name.to_string(), + description: desc.to_string(), + default_pot: pot_id, + }); } } } @@ -93,35 +131,124 @@ pub fn get(db: &DbInstance, id: &AppId) -> anyhow::Result { pub fn set_default_pot(db: &DbInstance, id: &AppId, pot: &PotId) -> anyhow::Result<()> { let params = vec![ ("id", DataValue::Str(serde_json::to_string(id)?.into())), - ("default_pot", DataValue::Str(serde_json::to_string(pot)?.into())) + ( + "default_pot", + DataValue::Str(serde_json::to_string(pot)?.into()), + ), ]; match run_query!( - &db, - ":update apps {id => default_pot}", - params, + &db, ":update apps {id => default_pot}", + params.clone(), ScriptMutability::Mutable ) { - Ok(_) => Ok(()), + Ok(_) => match run_query!(&db, ":put pot_memberships {pot_id = default_pot, app_id = id}", params, ScriptMutability::Mutable) { + Ok(_) => Ok(()), + Err(report) => bail!(report), + }, Err(report) => bail!(report), } } -pub fn create_pot(db: &DbInstance, pot_id: &PotId, app_id: &AppId, app_type: &str) -> anyhow::Result<()> { +pub fn get_default_pot(db: &DbInstance, app: &AppId) -> anyhow::Result { + let mut params = BTreeMap::new(); + params.insert( + "app_id".to_string(), + DataValue::Str(serde_json::to_string(&app)?.into()), + ); + + let result = db.run_script( + " + default_pot[pot_id] := *apps{id: $app_id, default_pot: pot_id} + ?[pot_id, app_type] := default_pot[pot_id], *pots[pot_id, app_type] + ", + params.clone(), + ScriptMutability::Immutable, + ); + + match result { + Ok(rows) => { + if let Some(firstrow) = rows.rows.first() { + if let [DataValue::Str(pot_id_str), DataValue::Str(app_type)] = firstrow.as_slice() + { + Ok(Pot { + id: serde_json::from_str(&pot_id_str)?, + app_type: app_type.to_string(), + }) + } else { + Err(Error::msg("Failed to deserialize query result")) + } + } else { + Err(Error::msg("App not found")) + } + } + Err(e) => bail!(e), + } +} + +pub fn create_pot( + db: &DbInstance, + pot_id: &PotId, + app_id: &AppId, + app_type: &str, +) -> anyhow::Result<()> { let params = vec![ - ("pot_id", DataValue::Str(serde_json::to_string(pot_id)?.into())), - ("app_id", DataValue::Str(serde_json::to_string(app_id)?.into())), + ( + "pot_id", + DataValue::Str(serde_json::to_string(pot_id)?.into()), + ), + ( + "app_id", + DataValue::Str(serde_json::to_string(app_id)?.into()), + ), ("app_type", DataValue::Str(app_type.into())), ]; match run_query!( &db, - ":insert pots {pot_id => app_type} - :insert pot_memberships {pot_id => app_id}", - params, + ":insert pots {id = pot_id => app_type}" + , + params.clone(), ScriptMutability::Mutable ) { - Ok(_) => Ok(()), - Err(e) => bail!(e), + Ok(_) => match run_query!(&db, ":insert pot_memberships {pot_id => app_id}", params, ScriptMutability::Mutable) { + Ok(_) => Ok(()), + Err(e) => { + warn!("{:?}", e); + bail!(e) + }, + }, + Err(e) => { + warn!("{:?}", e); + bail!(e) + }, + } +} + +#[cfg(test)] +mod tests { + use cozo::DbInstance; + use serde_with::chrono::Utc; + use tracing::{Level, debug}; + use ubisync_lib::types::PotId; + + use crate::{state::{schema, queries::pots}, api::v0::app::AppId}; + + #[test] + pub fn default_pot() { + tracing_subscriber::fmt() + .pretty() + .with_max_level(Level::DEBUG) + .init(); + + let db = DbInstance::new("mem", "", Default::default()).unwrap(); + schema::add_schema(&db).unwrap(); + let app_id = AppId::new(); + let pot_id = PotId::new(); + super::add(&db, &app_id, &Utc::now(), "name", "description", "app_type").unwrap(); + pots::add(&db, &pot_id, "app_type").unwrap(); + super::set_default_pot(&db, &app_id, &pot_id).unwrap(); + + debug!("Result: {:?}", super::get_default_pot(&db, &app_id)); } } \ No newline at end of file diff --git a/ubisync/src/state/queries/elements.rs b/ubisync/src/state/queries/elements.rs index 3afadfc..7b98fea 100644 --- a/ubisync/src/state/queries/elements.rs +++ b/ubisync/src/state/queries/elements.rs @@ -7,10 +7,10 @@ use tracing::{debug, error}; use crate::{ run_query, - state::{Element, ElementContent, ElementId}, + state::{Element, ElementContent, ElementId}, api::v0::app::AppId, }; -use ubisync_lib::types::{MessageId, Tag}; +use ubisync_lib::types::{MessageId, Tag, PotId}; pub fn add( db: &DbInstance, @@ -18,6 +18,7 @@ pub fn add( content: &ElementContent, latest_message: Option, local_changes: bool, + pot: &PotId, ) -> anyhow::Result<()> { let params = vec![ ("id", DataValue::Str(serde_json::to_string(&id)?.into())), @@ -33,11 +34,12 @@ pub fn add( }, ), ("local_changes", DataValue::Bool(local_changes)), + ("pot", DataValue::Str(serde_json::to_string(pot)?.into())), ]; match run_query!( &db, - ":insert elements {id => content, latest_message, local_changes}", + ":insert elements {id => content, latest_message, local_changes, pot}", params, ScriptMutability::Mutable ) { @@ -103,17 +105,18 @@ pub fn get(db: &DbInstance, id: &ElementId) -> anyhow::Result { ); let result = db.run_script(" - ?[content, latest_message, local_changes] := *elements[$id, content, latest_message, local_changes] + ?[content, latest_message, local_changes, pot] := *elements[$id, content, latest_message, local_changes, pot] ", params, cozo::ScriptMutability::Immutable); match result { Ok(val) => { if let Some(firstrow) = val.rows.first() { debug!("db result: {:?}", &firstrow.as_slice()); - if let [DataValue::Json(JsonData(content)), latest_message, DataValue::Bool(local_changes)] = + if let [DataValue::Json(JsonData(content)), latest_message, DataValue::Bool(local_changes), DataValue::Str(pot_id)] = firstrow.as_slice() { return Ok(Element::from(( id.to_owned(), + Some(serde_json::from_str(pot_id)?), serde_json::from_value(content.to_owned())?, match latest_message { DataValue::Str(s) => Some(serde_json::from_str(s)?), @@ -131,6 +134,27 @@ pub fn get(db: &DbInstance, id: &ElementId) -> anyhow::Result { } } +pub fn get_app_access(db: &DbInstance, id: &ElementId, app: &AppId) -> anyhow::Result { + let mut params = BTreeMap::new(); + params.insert( + "id".to_string(), + DataValue::Str(serde_json::to_string(&id)?.into()), + ); + params.insert("app_id".to_string(), DataValue::Str(serde_json::to_string(&app)?.into())); + + let result = db.run_script(" + memberships[pot_id] := *pot_memberships{pot_id, app_id} + ?[id] := memberships[pot_id], *elements{id, pot: pot_id} + ", params.clone(), cozo::ScriptMutability::Immutable); + + match result { + Ok(named_rows) => { + Ok(named_rows.rows.len() > 0) + }, + Err(report) => bail!(report), + } +} + pub fn get_by_tag(db: &DbInstance, tag: &Tag) -> anyhow::Result> { let mut params = BTreeMap::new(); params.insert( diff --git a/ubisync/src/state/schema.rs b/ubisync/src/state/schema.rs index d7d91f7..e950033 100644 --- a/ubisync/src/state/schema.rs +++ b/ubisync/src/state/schema.rs @@ -37,6 +37,7 @@ pub fn add_schema(db: &DbInstance) -> anyhow::Result<()> { content: Json, latest_message: String?, local_changes: Bool, + pot: String, }} {:create tags { tag: String, diff --git a/ubisync/tests/api.rs b/ubisync/tests/api.rs index 80dd09e..c4540d8 100644 --- a/ubisync/tests/api.rs +++ b/ubisync/tests/api.rs @@ -30,17 +30,17 @@ async fn two_nodes_element_creation() { .await .unwrap(); let api_client2 = UbisyncClient::init("localhost", 9982, None, "App", "Long desc", "test-app-type") - .await - .unwrap() - .create_default_pot() .await .unwrap(); + tokio::time::sleep(Duration::from_millis(5000)).await; + let test_element_content = ElementContent::Text("Text".to_string()); let create_resp = api_client1 .send( ElementCreateRequest { content: test_element_content.clone(), + pot: None, }, (), )