diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ddcbca4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer" + ] +} \ No newline at end of file diff --git a/migrations/2025-07-17-160443_init/down.sql b/migrations/2025-07-17-160443_init/down.sql index 44b812b..40e8ec7 100644 --- a/migrations/2025-07-17-160443_init/down.sql +++ b/migrations/2025-07-17-160443_init/down.sql @@ -1,2 +1,3 @@ -- This file should undo anything in `up.sql` -DROP TABLE IF EXISTS sessions; \ No newline at end of file +DROP TABLE IF EXISTS providers_records; +DROP TABLE IF EXISTS providers_records; \ No newline at end of file diff --git a/migrations/2025-07-17-160443_init/up.sql b/migrations/2025-07-17-160443_init/up.sql index a604b77..6a9bf8c 100644 --- a/migrations/2025-07-17-160443_init/up.sql +++ b/migrations/2025-07-17-160443_init/up.sql @@ -1,10 +1,20 @@ -- Your SQL goes here -CREATE TABLE sessions ( +CREATE TABLE providers_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, service_name TEXT NOT NULL, - request_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP, + request_time BIGINT UNSIGNED NOT NULL, + auth_methode TEXT NOT NULL, + expires_at BIGINT UNSIGNED, state TEXT NOT NULL, - awnsered_at TIMESTAMP, + awnsered_at BIGINT UNSIGNED, awnsered_by TEXT -) \ No newline at end of file +); + +CREATE TABLE beggars_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_time BIGINT UNSIGNED NOT NULL, + service_name TEXT NOT NULL, + http_result INTEGER UNSIGNED NOT NULL, + note TEXT, + awnsered_by TEXT +); \ No newline at end of file diff --git a/migrations/2025-08-16-200228_add_beggars_record_orm/down.sql b/migrations/2025-08-16-200228_add_beggars_record_orm/down.sql deleted file mode 100644 index ceacee0..0000000 --- a/migrations/2025-08-16-200228_add_beggars_record_orm/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE beggars_records; -ALTER TABLE providers_records RENAME TO sessions; \ No newline at end of file diff --git a/migrations/2025-08-16-200228_add_beggars_record_orm/up.sql b/migrations/2025-08-16-200228_add_beggars_record_orm/up.sql deleted file mode 100644 index ca8bda6..0000000 --- a/migrations/2025-08-16-200228_add_beggars_record_orm/up.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Your SQL goes here -CREATE TABLE beggars_records ( - request_time TIMESTAMP PRIMARY KEY DEFAULT CURRENT_TIMESTAMP NOT NULL, - service_name TEXT NOT NULL, - http_result INTEGER NOT NULL, - note TEXT, - awnsered_by TEXT -); - -ALTER TABLE sessions RENAME TO providers_records; \ No newline at end of file diff --git a/src/api/guards.rs b/src/api/guards.rs index 2e9d989..f2aad0f 100644 --- a/src/api/guards.rs +++ b/src/api/guards.rs @@ -26,6 +26,7 @@ fn get_query_value(query: &Option, query_key: &str) -> Option { pub(super) struct APIProviderRequest<'r> { pub share: &'r Providers, + pub name: &'r str, } #[rocket::async_trait] @@ -49,7 +50,7 @@ impl<'r> FromRequest<'r> for APIProviderRequest<'r> { .iter() .find(|(name, service)| *name == share_name && service.access_key_hash == access_key) { - Outcome::Success(APIProviderRequest { share: service.1 }) + Outcome::Success(APIProviderRequest { share: service.1, name: service.0 }) } else { Outcome::Error(HttpResult::ShareNotFound(format!( "Share '{}' not found.", diff --git a/src/api/notifier.rs b/src/api/notifier.rs new file mode 100644 index 0000000..e0738fd --- /dev/null +++ b/src/api/notifier.rs @@ -0,0 +1,5 @@ + + +pub fn send_user_notification(record: ProviderRecord) -> Result<(), AutoDecryptError> { + todo!("Implement user notification logic here"); +} \ No newline at end of file diff --git a/src/api/request.rs b/src/api/request.rs index e0d605f..c13644d 100644 --- a/src/api/request.rs +++ b/src/api/request.rs @@ -1,20 +1,34 @@ use super::guards::APIProviderRequest; use crate::api::guards; -use crate::services::providers::{ConsentMethode, ProviderAction, Providers}; +use crate::errors::AutoDecryptError; +use crate::main; +use crate::orm::structures::{ProviderRecord}; +use crate::orm::types::{AuthMethod, RecordStates}; +use crate::services::providers::{ConsentMethode, ProviderAction, Providers, ActionType}; use crate::app_config::CONFIG; +use crate::orm::DbConn; +use diesel::expression::is_aggregate::No; use rocket::get; use rocket::http::Status; +use rocket::State; use super::HttpResult; +async fn execute_action(action: &Providers, key: Option<&str>) -> Result { -async fn execute_action(action: &Providers) -> Result { - let action: &dyn ProviderAction = &action.execution_action; - action.execute().await + let exec_action = match &action.execution_action { + ActionType::ForigneAction(forigne_action) => { + &forigne_action.to_executable_action(key)? as &dyn ProviderAction + }, + _ => &action.execution_action as &dyn ProviderAction, + }; + + exec_action.execute().await } + #[get("/<_>", rank = 1)] -pub(super) async fn request_handler(guard: Result, HttpResult>) -> (Status, HttpResult) { +pub(super) async fn request_handler(guard: Result, HttpResult>, db_conn: &State) -> (Status, HttpResult) { // We assume the request guard verified th access_key against the service_name if let Err(err) = guard { // This wrapper is nessesary to avoid needing to write catchers, and recompute the error @@ -22,22 +36,35 @@ pub(super) async fn request_handler(guard: Result, HttpRe return err.into(); } let guard = guard.expect("A rejected request guard was able to perseed!"); - + let mut expires_at = None; + let auth_methode: AuthMethod = AuthMethod::from(&guard.share.consent_methode); let result: HttpResult = match guard.share.consent_methode { ConsentMethode::None => { // We do not wait for constent, direct feedback is impleied - match execute_action(guard.share).await { + match execute_action(guard.share, None).await { Ok(response) => HttpResult::UnlockingSucceeded(response), - Err(err) => HttpResult::UnlockingFailed(err), + Err(err) => HttpResult::UnlockingFailed(err.to_string()), } }, - ConsentMethode::Boolean => { - todo!() - }, - ConsentMethode::PassheyHash(_) => { - todo!() + _ => { + expires_at = Some(CONFIG.wait().user_confirmation_expiration); + HttpResult::AckAuthReq("An auth request has been sent.".to_string()) }, }; + let record_state = match &result { + HttpResult::UnlockingSucceeded(_) => RecordStates::Accepted, + HttpResult::AckAuthReq(_) => RecordStates::Pending, + HttpResult::TimelimitExceeded(_) => RecordStates::Expired, + _ => RecordStates::Rejected, + }; + + db_conn.add_provider_record(&ProviderRecord { + service_name: guard.name.to_string(), + expires_at: expires_at, // Convert to sql timestamp + add used auth methode to record in db + auth_methode: auth_methode, + state: record_state, + awnsered_by: None, + }); return result.into(); } \ No newline at end of file diff --git a/src/app_config/general_config.rs b/src/app_config/general_config.rs index a70811e..2f8cbb9 100644 --- a/src/app_config/general_config.rs +++ b/src/app_config/general_config.rs @@ -8,6 +8,7 @@ pub (super) struct TomlAppConfig { pub (super) addresses: Option>, pub (super) platform: Option, pub (super) ssh_known_host_file: Option, + pub (super) user_confirmation_expiration: Option, pub (super) db_file: Option, pub (super) beggars: Option>, pub (super) providers: Option> diff --git a/src/app_config/mod.rs b/src/app_config/mod.rs index c5c62af..fe62e25 100644 --- a/src/app_config/mod.rs +++ b/src/app_config/mod.rs @@ -22,7 +22,7 @@ pub(crate) struct AppConfig { pub(crate) addresses: Vec, pub(crate) platform: Platform, pub(crate) ssh_known_host_file: String, - + pub(crate) user_confirmation_expiration: i64, pub(crate) db_file: String, pub(crate) beggars: HashMap, // We request to unlock owned services @@ -36,6 +36,7 @@ impl Default for AppConfig { addresses: vec!["*".to_string()], beggars: HashMap::new(), providers: HashMap::new(), + user_confirmation_expiration: 60 * 60 * 24, // 24 hours platform: Platform::OMV, ssh_known_host_file: "".to_string(), db_file: "/var/auto-decrypt/db.sqlite".to_string(), @@ -51,6 +52,7 @@ impl AppConfig { port: toml_config.port.unwrap_or(default.port), addresses: toml_config.addresses.unwrap_or(default.addresses), platform: toml_config.platform.unwrap_or(default.platform), + user_confirmation_expiration: toml_config.user_confirmation_expiration.unwrap_or(default.user_confirmation_expiration), ssh_known_host_file: toml_config.ssh_known_host_file.unwrap_or(default.ssh_known_host_file), db_file: toml_config.db_file.unwrap_or(default.db_file), beggars: toml_config.beggars.unwrap_or(default.beggars), diff --git a/src/app_config/providers.rs b/src/app_config/providers.rs index 013e4e7..c2fbacb 100644 --- a/src/app_config/providers.rs +++ b/src/app_config/providers.rs @@ -17,7 +17,7 @@ pub (super) struct TomlProviders { impl TomlProviders { pub(super) fn to_internal(mut self, base_config_dir: &Path) -> Result { - if let ActionType::EncryptedRequest(ref mut action_type) = self.execution_action { + if let ActionType::ForigneAction(ref mut action_type) = self.execution_action { if action_type.content.is_none() { action_type.load_from_file(base_config_dir)?; } diff --git a/src/orm/handler.rs b/src/orm/handler.rs index 0cc2164..f7d8e21 100644 --- a/src/orm/handler.rs +++ b/src/orm/handler.rs @@ -6,16 +6,17 @@ use super::DbConn; use super::structures::*; use crate::orm::schema::{providers_records, beggars_records}; +use super::types::RecordStates; impl DbConn { - fn add_provider_record(self, record: &ProviderRecord) { + pub(crate) fn add_provider_record(&self, record: &ProviderRecord) { let conn = &mut *self.0.lock().unwrap(); diesel::insert_into(providers_records::table) .values(record) .execute(conn) .expect("Error saving new service record"); } - fn update_provider_state(self, id: i32, new_state: RecordStates) { + pub(crate) fn update_provider_state(&self, id: i32, new_state: RecordStates) { let conn = &mut *self.0.lock().unwrap(); diesel::update(providers_records::table.filter(providers_records::dsl::id.eq(id))) .set(providers_records::state.eq(new_state.to_string())) @@ -23,7 +24,7 @@ impl DbConn { .expect("Error updating service record state"); } - fn add_beggars_record(self, record: &BeggarsRecord) { + pub(crate) fn add_beggars_record(&self, record: &BeggarsRecord) { let conn = &mut *self.0.lock().unwrap(); diesel::insert_into(beggars_records::table) .values(record) diff --git a/src/orm/mod.rs b/src/orm/mod.rs index 3b964eb..eaab916 100644 --- a/src/orm/mod.rs +++ b/src/orm/mod.rs @@ -4,8 +4,10 @@ use std::sync::{Arc, Mutex}; mod schema; mod handler; +pub mod types; pub mod structures; + pub struct DbConn(pub Arc>); impl DbConn { diff --git a/src/orm/schema.rs b/src/orm/schema.rs index 58c5f95..037acf3 100644 --- a/src/orm/schema.rs +++ b/src/orm/schema.rs @@ -1,8 +1,9 @@ // @generated automatically by Diesel CLI. diesel::table! { - beggars_records (request_time) { - request_time -> Timestamp, + beggars_records (id) { + id -> Nullable, + request_time -> BigInt, service_name -> Text, http_result -> Integer, note -> Nullable, @@ -14,10 +15,11 @@ diesel::table! { providers_records (id) { id -> Nullable, service_name -> Text, - request_time -> Nullable, - expires_at -> Nullable, + request_time -> BigInt, + auth_methode -> Text, + expires_at -> Nullable, state -> Text, - awnsered_at -> Nullable, + awnsered_at -> Nullable, awnsered_by -> Nullable, } } diff --git a/src/orm/structures.rs b/src/orm/structures.rs index 9140399..94ab599 100644 --- a/src/orm/structures.rs +++ b/src/orm/structures.rs @@ -10,65 +10,18 @@ use diesel::{ AsExpression, FromSqlRow, }; use std::fmt; - - -#[derive(FromSqlRow, Debug, AsExpression)] -#[diesel(sql_type = Text)] -pub(crate) enum RecordStates { - Pending, - Accepted, - Rejected, - Expired, -} - -impl fmt::Display for RecordStates { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - RecordStates::Pending => write!(f, "Pending"), - RecordStates::Accepted => write!(f, "Accepted"), - RecordStates::Rejected => write!(f, "Rejected"), - RecordStates::Expired => write!(f, "Expired"), - } - } -} - -impl TryFrom<&str> for RecordStates { - type Error = String; - - fn try_from(value: &str) -> Result { - match value { - "Pending" => Ok(RecordStates::Pending), - "Accepted" => Ok(RecordStates::Accepted), - "Rejected" => Ok(RecordStates::Rejected), - "Expired" => Ok(RecordStates::Expired), - _ => Err(format!("Unknown state: {}", value)), - } - } -} - -impl FromSql for RecordStates { - fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result { - let t = >::from_sql(bytes)?; - Ok(t.as_str().try_into()?) - } -} - -impl ToSql for RecordStates { - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { - out.set_value(self.to_string()); - Ok(diesel::serialize::IsNull::No) - } -} +use super::types::{RecordStates, AuthMethod}; #[derive(Debug)] #[derive(Queryable, Selectable, Insertable)] #[diesel(table_name = super::schema::providers_records)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub(crate) struct ProviderRecord{ - service_name: String, - expires_at: Option, - state: RecordStates, - awnsered_by: Option, + pub(crate) service_name: String, + pub(crate) expires_at: Option, // Unix timestamp + pub(crate) auth_methode: AuthMethod, + pub(crate) state: RecordStates, + pub(crate) awnsered_by: Option, } diff --git a/src/orm/types.rs b/src/orm/types.rs new file mode 100644 index 0000000..324e9ef --- /dev/null +++ b/src/orm/types.rs @@ -0,0 +1,118 @@ +use clap::builder::Str; +use diesel::{ + deserialize::FromSql, + serialize::{Output, ToSql}, + sql_types::Text, + Queryable, + Selectable, + Insertable, + sqlite::{Sqlite, SqliteValue}, + AsExpression, FromSqlRow, +}; +use std::fmt; +use diesel::sql_types::BigInt; + +#[derive(FromSqlRow, Debug, AsExpression)] +#[diesel(sql_type = Text)] +pub(crate) enum RecordStates { + Pending, + Accepted, + Rejected, + Expired, +} + +impl fmt::Display for RecordStates { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RecordStates::Pending => write!(f, "Pending"), + RecordStates::Accepted => write!(f, "Accepted"), + RecordStates::Rejected => write!(f, "Rejected"), + RecordStates::Expired => write!(f, "Expired"), + } + } +} + +impl TryFrom<&str> for RecordStates { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "Pending" => Ok(RecordStates::Pending), + "Accepted" => Ok(RecordStates::Accepted), + "Rejected" => Ok(RecordStates::Rejected), + "Expired" => Ok(RecordStates::Expired), + _ => Err(format!("Unknown state: {}", value)), + } + } +} + +impl FromSql for RecordStates { + fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result { + let t = >::from_sql(bytes)?; + Ok(t.as_str().try_into()?) + } +} + +impl ToSql for RecordStates { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { + out.set_value(self.to_string()); + Ok(diesel::serialize::IsNull::No) + } +} + + +#[derive(FromSqlRow, Debug, AsExpression)] +#[diesel(sql_type = Text)] +pub(crate) enum AuthMethod { + None, + Password, + Confirmation, +} +impl fmt::Display for AuthMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AuthMethod::None => write!(f, "None"), + AuthMethod::Password => write!(f, "Password"), + AuthMethod::Confirmation => write!(f, "Confirmation"), + } + } +} + +impl TryFrom<&str> for AuthMethod { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "None" => Ok(AuthMethod::None), + "Password" => Ok(AuthMethod::Password), + "Confirmation" => Ok(AuthMethod::Confirmation), + _ => Err(format!("Unknown auth method: {}", value)), + } + } +} + +impl FromSql for AuthMethod { + fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result { + let t = >::from_sql(bytes)?; + Ok(t.as_str().try_into()?) + } +} + +impl ToSql for AuthMethod { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { + out.set_value(self.to_string()); + Ok(diesel::serialize::IsNull::No) + } +} + +use crate::services::providers::ConsentMethode; + +impl From<&ConsentMethode> for AuthMethod { + fn from(value: &ConsentMethode) -> Self { + match value { + ConsentMethode::Boolean => AuthMethod::Confirmation, + ConsentMethode::PasskeyHash(_) => AuthMethod::Password, + _ => AuthMethod::None + } + } +} \ No newline at end of file diff --git a/src/services/providers.rs b/src/services/providers.rs index eeffa2f..f92697c 100644 --- a/src/services/providers.rs +++ b/src/services/providers.rs @@ -1,3 +1,4 @@ +use std::f32::consts::E; use std::path::Path; use async_ssh2_tokio::Config; @@ -17,15 +18,16 @@ use crate::errors::AutoDecryptError; pub(crate) enum ConsentMethode { None, Boolean, - PassheyHash(String), + PasskeyHash(String), } + #[derive(Debug, Deserialize)] #[enum_dispatch(ProviderAction)] pub(crate) enum ActionType { HTTPRequest(HTTPAction), SSHRequest(SSHAction), - EncryptedRequest(EncryptedAction) + ForigneAction(ForigneAction) } @@ -49,24 +51,24 @@ struct HTTPAction { #[async_trait] impl ProviderAction for HTTPAction { - async fn execute(&self) -> Result { + async fn execute(&self) -> Result { let client = Client::new(); let mut req_builder = client.request( reqwest::Method::from_bytes(self.method.as_bytes()) - .map_err(|e| e.to_string())?, + .map_err(|e| AutoDecryptError::ConfigurationError { comment: e.to_string() })?, &self.uri, ); if let Some(headers) = &self.headers { for (key, value) in headers { req_builder = req_builder.header( - HeaderName::from_bytes(key.as_bytes()).map_err(|e| e.to_string())?, - HeaderValue::from_str(value).map_err(|e| e.to_string())?, + HeaderName::from_bytes(key.as_bytes()).map_err(|e| AutoDecryptError::ConfigurationError { comment: e.to_string() })?, + HeaderValue::from_str(value).map_err(|e| AutoDecryptError::ConfigurationError { comment: e.to_string() })?, ); } } let response = req_builder.send().await - .map_err(|e| e.to_string())?; - Ok(format!("Success! Server replied with: {}", response.text().await.map_err(|e| e.to_string())?)) + .map_err(|e| AutoDecryptError::APIError { comment: e.to_string() })?; + Ok(format!("Success! Server replied with: {}", response.text().await.map_err(|e| AutoDecryptError::APIError { comment: e.to_string() })?)) } } @@ -81,31 +83,34 @@ struct SSHAction { #[async_trait] impl ProviderAction for SSHAction { - async fn execute(&self) -> Result { + async fn execute(&self) -> Result { let auth_method = AuthMethod::PrivateKey { key_data: self.private_key.clone(), key_pass: None }; let client = SSHClient::connect( (&*self.uri, self.port), &self.username, auth_method, ServerCheckMethod::KnownHostsFile(CONFIG.wait().ssh_known_host_file.clone()), - ).await.map_err(|e| e.to_string())?; + ).await.map_err(|e| AutoDecryptError::APIError { comment: e.to_string() })?; - let result = client.execute(&self.cmd).await.map_err(|e| e.to_string())?; + let result = client.execute(&self.cmd).await.map_err(|e| AutoDecryptError::APIError { comment: e.to_string() })?; if result.exit_status == 0 { Ok(result.stdout) } else { - Err(result.stderr) + Err(AutoDecryptError::APIError { + comment: format!("SSH command failed with exit status {}: {}", result.exit_status, result.stderr), + }) } } } #[derive(Debug, Deserialize)] -pub struct EncryptedAction{ +pub struct ForigneAction{ pub(crate) filename: String, - pub(crate) content: Option + pub(crate) content: Option, + pub(crate) is_encrypted: bool, } -impl ProviderAction for EncryptedAction {} -impl EncryptedAction { +impl ProviderAction for ForigneAction {} +impl ForigneAction { pub(crate) fn load_from_file(&mut self, find_in_dir: &Path) -> Result<&mut Self, AutoDecryptError> { let content = std::fs::read_to_string(find_in_dir.join(&self.filename)) .map_err(|err| AutoDecryptError::ConfigurationError { comment: err.to_string() })?; @@ -114,7 +119,7 @@ impl EncryptedAction { } - async fn to_executable_action(&self, key: &str) -> Result { + pub(crate) fn to_executable_action(&self, key: Option<&str>) -> Result { todo!() //let cleartext_toml = //1. get Cleartext toml 2. load tomel as any other 3. return object @@ -127,7 +132,7 @@ impl EncryptedAction { #[async_trait] #[enum_dispatch] pub (crate) trait ProviderAction: std::fmt::Debug + Send + Sync { - async fn execute(&self) -> Result { - return Err("We are not directly executable!".to_string()) + async fn execute(&self) -> Result { + return Err(AutoDecryptError::ConfigurationError { comment: ("We are not directly executable!".to_string()) }) } }