implemented requsting unlock request

This commit is contained in:
Kyattsukuro 2025-08-22 15:46:38 +02:00
parent 22a6c8818b
commit 230943ba64
17 changed files with 236 additions and 115 deletions

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"vadimcn.vscode-lldb",
"rust-lang.rust-analyzer"
]
}

View File

@ -1,2 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS providers_records;
DROP TABLE IF EXISTS providers_records;

View File

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

View File

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE beggars_records;
ALTER TABLE providers_records RENAME TO sessions;

View File

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

View File

@ -26,6 +26,7 @@ fn get_query_value(query: &Option<Query>, query_key: &str) -> Option<String> {
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.",

5
src/api/notifier.rs Normal file
View File

@ -0,0 +1,5 @@
pub fn send_user_notification(record: ProviderRecord) -> Result<(), AutoDecryptError> {
todo!("Implement user notification logic here");
}

View File

@ -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<String, AutoDecryptError> {
async fn execute_action(action: &Providers) -> Result<String, String> {
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<APIProviderRequest<'_>, HttpResult>) -> (Status, HttpResult) {
pub(super) async fn request_handler(guard: Result<APIProviderRequest<'_>, HttpResult>, db_conn: &State<DbConn>) -> (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<APIProviderRequest<'_>, 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();
}

View File

@ -8,6 +8,7 @@ pub (super) struct TomlAppConfig {
pub (super) addresses: Option<Vec<String>>,
pub (super) platform: Option<Platform>,
pub (super) ssh_known_host_file: Option<String>,
pub (super) user_confirmation_expiration: Option<i64>,
pub (super) db_file: Option<String>,
pub (super) beggars: Option<HashMap<String, Beggars>>,
pub (super) providers: Option<HashMap<String, TomlProviders>>

View File

@ -22,7 +22,7 @@ pub(crate) struct AppConfig {
pub(crate) addresses: Vec<String>,
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<String, Beggars>, // 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),

View File

@ -17,7 +17,7 @@ pub (super) struct TomlProviders {
impl TomlProviders {
pub(super) fn to_internal(mut self, base_config_dir: &Path) -> Result<Providers, AutoDecryptError> {
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)?;
}

View File

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

View File

@ -4,8 +4,10 @@ use std::sync::{Arc, Mutex};
mod schema;
mod handler;
pub mod types;
pub mod structures;
pub struct DbConn(pub Arc<Mutex<SqliteConnection>>);
impl DbConn {

View File

@ -1,8 +1,9 @@
// @generated automatically by Diesel CLI.
diesel::table! {
beggars_records (request_time) {
request_time -> Timestamp,
beggars_records (id) {
id -> Nullable<Integer>,
request_time -> BigInt,
service_name -> Text,
http_result -> Integer,
note -> Nullable<Text>,
@ -14,10 +15,11 @@ diesel::table! {
providers_records (id) {
id -> Nullable<Integer>,
service_name -> Text,
request_time -> Nullable<Timestamp>,
expires_at -> Nullable<Timestamp>,
request_time -> BigInt,
auth_methode -> Text,
expires_at -> Nullable<BigInt>,
state -> Text,
awnsered_at -> Nullable<Timestamp>,
awnsered_at -> Nullable<BigInt>,
awnsered_by -> Nullable<Text>,
}
}

View File

@ -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<Self, Self::Error> {
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<Text, Sqlite> for RecordStates {
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
let t = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
Ok(t.as_str().try_into()?)
}
}
impl ToSql<Text, Sqlite> 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<String>,
state: RecordStates,
awnsered_by: Option<String>,
pub(crate) service_name: String,
pub(crate) expires_at: Option<i64>, // Unix timestamp
pub(crate) auth_methode: AuthMethod,
pub(crate) state: RecordStates,
pub(crate) awnsered_by: Option<String>,
}

118
src/orm/types.rs Normal file
View File

@ -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<Self, Self::Error> {
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<Text, Sqlite> for RecordStates {
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
let t = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
Ok(t.as_str().try_into()?)
}
}
impl ToSql<Text, Sqlite> 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<Self, Self::Error> {
match value {
"None" => Ok(AuthMethod::None),
"Password" => Ok(AuthMethod::Password),
"Confirmation" => Ok(AuthMethod::Confirmation),
_ => Err(format!("Unknown auth method: {}", value)),
}
}
}
impl FromSql<Text, Sqlite> for AuthMethod {
fn from_sql(bytes: SqliteValue) -> diesel::deserialize::Result<Self> {
let t = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
Ok(t.as_str().try_into()?)
}
}
impl ToSql<Text, Sqlite> 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
}
}
}

View File

@ -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<String, String> {
async fn execute(&self) -> Result<String, AutoDecryptError> {
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<String, String> {
async fn execute(&self) -> Result<String, AutoDecryptError> {
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<String>
pub(crate) content: Option<String>,
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<ActionType, AutoDecryptError> {
pub(crate) fn to_executable_action(&self, key: Option<&str>) -> Result<ActionType, AutoDecryptError> {
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<String, String> {
return Err("We are not directly executable!".to_string())
async fn execute(&self) -> Result<String, AutoDecryptError> {
return Err(AutoDecryptError::ConfigurationError { comment: ("We are not directly executable!".to_string()) })
}
}