mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
refactored permission system and error handling
This commit is contained in:
@ -67,14 +67,21 @@ impl HlcService {
|
||||
|
||||
/// Factory-Funktion: Erstellt und initialisiert einen neuen HLC-Service aus einer bestehenden DB-Verbindung.
|
||||
/// Dies ist die bevorzugte Methode zur Instanziierung.
|
||||
pub fn new_from_connection(
|
||||
conn: &Connection,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<Self, HlcError> {
|
||||
pub fn try_initialize(conn: &Connection, app_handle: &AppHandle) -> Result<Self, HlcError> {
|
||||
// 1. Hole oder erstelle eine persistente Node-ID
|
||||
let node_id_str = Self::get_or_create_device_id(app_handle)?;
|
||||
|
||||
let node_id = ID::try_from(node_id_str.as_bytes()).map_err(|e| {
|
||||
// Parse den String in ein Uuid-Objekt.
|
||||
let uuid = Uuid::parse_str(&node_id_str).map_err(|e| {
|
||||
HlcError::ParseNodeId(format!(
|
||||
"Stored device ID is not a valid UUID: {}. Error: {}",
|
||||
node_id_str, e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Hol dir die rohen 16 Bytes und erstelle daraus die uhlc::ID.
|
||||
// Das `*` dereferenziert den `&[u8; 16]` zu `[u8; 16]`, was `try_from` erwartet.
|
||||
let node_id = ID::try_from(*uuid.as_bytes()).map_err(|e| {
|
||||
HlcError::ParseNodeId(format!("Invalid node ID format from device store: {:?}", e))
|
||||
})?;
|
||||
|
||||
@ -112,6 +119,7 @@ impl HlcService {
|
||||
if let Some(s) = value.as_str() {
|
||||
// Das ist unser Erfolgsfall. Wir haben einen &str und können
|
||||
// eine Kopie davon zurückgeben.
|
||||
println!("Gefundene und validierte Geräte-ID: {}", s);
|
||||
if Uuid::parse_str(s).is_ok() {
|
||||
// Erfolgsfall: Der Wert ist ein String UND eine gültige UUID.
|
||||
// Wir können die Funktion direkt mit dem Wert verlassen.
|
||||
|
||||
@ -115,12 +115,8 @@ impl CrdtTransformer {
|
||||
Statement::Query(query) => self.transform_query_recursive(query),
|
||||
// Fange alle anderen Fälle ab und gib einen Fehler zurück
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: format!("{:?}", stmt)
|
||||
.split('(')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
description: "This operation only accepts SELECT statements.".to_string(),
|
||||
sql: stmt.to_string(),
|
||||
reason: "This operation only accepts SELECT statements.".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -168,8 +164,8 @@ impl CrdtTransformer {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "DELETE".to_string(),
|
||||
description: "DELETE from non-table source or multiple tables".to_string(),
|
||||
sql: del_stmt.to_string(),
|
||||
reason: "DELETE from non-table source or multiple tables".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -711,15 +707,15 @@ impl CrdtTransformer {
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "INSERT".to_string(),
|
||||
description: "INSERT with unsupported source type".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
reason: "INSERT with unsupported source type".to_string(),
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "INSERT".to_string(),
|
||||
description: "INSERT statement has no source".to_string(),
|
||||
reason: "INSERT statement has no source".to_string(),
|
||||
sql: insert_stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -740,8 +736,8 @@ impl CrdtTransformer {
|
||||
from[0].clone()
|
||||
} else {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "DELETE".to_string(),
|
||||
description: "DELETE with multiple tables not supported".to_string(),
|
||||
reason: "DELETE with multiple tables not supported".to_string(),
|
||||
sql: stmt.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,9 +156,8 @@ pub fn select(
|
||||
|
||||
// Stelle sicher, dass es eine Query ist
|
||||
if !matches!(statement, Statement::Query(_)) {
|
||||
return Err(DatabaseError::UnsupportedStatement {
|
||||
statement_type: "Non-Query".to_string(),
|
||||
description: "Only SELECT statements are allowed in select function".to_string(),
|
||||
return Err(DatabaseError::StatementError {
|
||||
reason: "Only SELECT statements are allowed in select function".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
// src-tauri/src/database/error.rs
|
||||
|
||||
use crate::crdt::trigger::CrdtSetupError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::crdt::trigger::CrdtSetupError;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type", content = "details")]
|
||||
@ -13,14 +12,21 @@ pub enum DatabaseError {
|
||||
/// Der SQL-Code konnte nicht geparst werden.
|
||||
#[error("Failed to parse SQL: {reason} - SQL: {sql}")]
|
||||
ParseError { reason: String, sql: String },
|
||||
|
||||
/// Parameter-Fehler (falsche Anzahl, ungültiger Typ, etc.)
|
||||
#[error("Parameter error: {reason} (expected: {expected}, provided: {provided})")]
|
||||
ParamError {
|
||||
reason: String,
|
||||
#[error("Parameter count mismatch: SQL has {expected} placeholders but {provided} provided. SQL Statement: {sql}")]
|
||||
ParameterMismatchError {
|
||||
expected: usize,
|
||||
provided: usize,
|
||||
sql: String,
|
||||
},
|
||||
|
||||
#[error("No table provided in SQL Statement: {sql}")]
|
||||
NoTableError { sql: String },
|
||||
|
||||
#[error("Statement Error: {reason}")]
|
||||
StatementError { reason: String },
|
||||
|
||||
#[error("Failed to prepare statement: {reason}")]
|
||||
PrepareError { reason: String },
|
||||
|
||||
@ -28,7 +34,7 @@ pub enum DatabaseError {
|
||||
DatabaseError { reason: String },
|
||||
|
||||
/// Ein Fehler ist während der Ausführung in der Datenbank aufgetreten.
|
||||
#[error("Execution error on table {}: {} - SQL: {}", table.as_deref().unwrap_or("unknown"), reason, sql)]
|
||||
#[error("Execution error on table {table:?}: {reason} - SQL: {sql}")]
|
||||
ExecutionError {
|
||||
sql: String,
|
||||
reason: String,
|
||||
@ -37,34 +43,36 @@ pub enum DatabaseError {
|
||||
/// Ein Fehler ist beim Verwalten der Transaktion aufgetreten.
|
||||
#[error("Transaction error: {reason}")]
|
||||
TransactionError { reason: String },
|
||||
|
||||
/// Ein SQL-Statement wird vom Proxy nicht unterstützt.
|
||||
#[error("Unsupported statement type '{statement_type}': {description}")]
|
||||
UnsupportedStatement {
|
||||
statement_type: String,
|
||||
description: String,
|
||||
},
|
||||
#[error("Unsupported statement. '{reason}'. - SQL: {sql}")]
|
||||
UnsupportedStatement { reason: String, sql: String },
|
||||
|
||||
/// Fehler im HLC-Service
|
||||
#[error("HLC error: {reason}")]
|
||||
HlcError { reason: String },
|
||||
|
||||
/// Fehler beim Sperren der Datenbankverbindung
|
||||
#[error("Lock error: {reason}")]
|
||||
LockError { reason: String },
|
||||
|
||||
/// Fehler bei der Datenbankverbindung
|
||||
#[error("Connection error: {reason}")]
|
||||
ConnectionError { reason: String },
|
||||
|
||||
/// Fehler bei der JSON-Serialisierung
|
||||
#[error("Serialization error: {reason}")]
|
||||
SerializationError { reason: String },
|
||||
|
||||
#[error("Permission error for extension '{extension_id}': {reason} (operation: {}, resource: {})",
|
||||
operation.as_deref().unwrap_or("unknown"),
|
||||
resource.as_deref().unwrap_or("unknown"))]
|
||||
/// Permission-bezogener Fehler für Extensions
|
||||
#[error("Permission error for extension '{extension_id}': {reason} (operation: {operation:?}, resource: {resource:?})")]
|
||||
PermissionError {
|
||||
extension_id: String,
|
||||
operation: Option<String>,
|
||||
resource: Option<String>,
|
||||
reason: String,
|
||||
},
|
||||
|
||||
#[error("Query error: {reason}")]
|
||||
QueryError { reason: String },
|
||||
|
||||
@ -111,7 +119,43 @@ impl From<CrdtSetupError> for DatabaseError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
|
||||
impl DatabaseError {
|
||||
/// Extract extension ID if this error is related to an extension
|
||||
pub fn extension_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
DatabaseError::PermissionError { extension_id, .. } => Some(extension_id.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a permission-related error
|
||||
pub fn is_permission_error(&self) -> bool {
|
||||
matches!(self, DatabaseError::PermissionError { .. })
|
||||
}
|
||||
|
||||
/// Get operation if available
|
||||
pub fn operation(&self) -> Option<&str> {
|
||||
match self {
|
||||
DatabaseError::PermissionError {
|
||||
operation: Some(op),
|
||||
..
|
||||
} => Some(op.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get resource if available
|
||||
pub fn resource(&self) -> Option<&str> {
|
||||
match self {
|
||||
DatabaseError::PermissionError {
|
||||
resource: Some(res),
|
||||
..
|
||||
} => Some(res.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/* impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError {
|
||||
fn from(err: crate::extension::database::ExtensionDatabaseError) -> Self {
|
||||
match err {
|
||||
crate::extension::database::ExtensionDatabaseError::Permission { source } => {
|
||||
@ -156,4 +200,4 @@ impl From<crate::extension::database::ExtensionDatabaseError> for DatabaseError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
@ -13,13 +13,10 @@ use tauri::{path::BaseDirectory, AppHandle, Manager, State};
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::table_names::TABLE_CRDT_CONFIGS;
|
||||
use crate::AppState;
|
||||
pub struct DbConnection(pub Arc<Mutex<Option<Connection>>>);
|
||||
|
||||
pub struct AppState {
|
||||
pub db: DbConnection,
|
||||
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sql_select(
|
||||
sql: String,
|
||||
@ -166,25 +163,33 @@ pub fn create_encrypted_database(
|
||||
reason: format!("Fehler beim Schließen der Quelldatenbank: {}", e),
|
||||
})?;
|
||||
|
||||
let new_conn = core::open_and_init_db(&path, &key, false)?;
|
||||
initialize_session(&app_handle, &path, &key, &state)?;
|
||||
|
||||
/* let new_conn = core::open_and_init_db(&path, &key, false)?;
|
||||
|
||||
// Aktualisieren der Datenbankverbindung im State
|
||||
let mut db = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
*db = Some(new_conn);
|
||||
*db = Some(new_conn); */
|
||||
|
||||
Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_encrypted_database(
|
||||
//app_handle: AppHandle,
|
||||
app_handle: AppHandle,
|
||||
path: String,
|
||||
key: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, DatabaseError> {
|
||||
if !Path::new(&path).exists() {
|
||||
return Err(DatabaseError::IoError {
|
||||
path,
|
||||
reason: "Database file not found.".to_string(),
|
||||
});
|
||||
}
|
||||
/* let vault_path = app_handle
|
||||
.path()
|
||||
.resolve(format!("vaults/{}", path), BaseDirectory::AppLocalData)
|
||||
@ -196,12 +201,48 @@ pub fn open_encrypted_database(
|
||||
return Err(format!("File not found {}", path).into());
|
||||
} */
|
||||
|
||||
let conn = core::open_and_init_db(&path, &key, false)
|
||||
/* let conn = core::open_and_init_db(&path, &key, false)
|
||||
.map_err(|e| format!("Error during open: {}", e))?;
|
||||
|
||||
let mut db = state.db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
*db = Some(conn);
|
||||
*db = Some(conn); */
|
||||
|
||||
initialize_session(&app_handle, &path, &key, &state)?;
|
||||
|
||||
Ok(format!("success"))
|
||||
}
|
||||
|
||||
/// Opens the DB, initializes the HLC service, and stores both in the AppState.
|
||||
fn initialize_session(
|
||||
app_handle: &AppHandle,
|
||||
path: &str,
|
||||
key: &str,
|
||||
state: &State<'_, AppState>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
// 1. Establish the raw database connection
|
||||
let conn = core::open_and_init_db(path, key, false)?;
|
||||
|
||||
// 2. Initialize the HLC service
|
||||
let hlc_service = HlcService::try_initialize(&conn, app_handle).map_err(|e| {
|
||||
// We convert the HlcError into a DatabaseError
|
||||
DatabaseError::ExecutionError {
|
||||
sql: "HLC Initialization".to_string(),
|
||||
reason: e.to_string(),
|
||||
table: Some(TABLE_CRDT_CONFIGS.to_string()),
|
||||
}
|
||||
})?;
|
||||
|
||||
// 3. Store everything in the global AppState
|
||||
let mut db_guard = state.db.0.lock().map_err(|e| DatabaseError::LockError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
*db_guard = Some(conn);
|
||||
|
||||
let mut hlc_guard = state.hlc.lock().map_err(|e| DatabaseError::LockError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
*hlc_guard = hlc_service;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,14 +1,178 @@
|
||||
/// src-tauri/src/extension/core.rs
|
||||
use crate::extension::database::permissions::DbExtensionPermission;
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::extension::permission_manager::ExtensionPermissions;
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{
|
||||
http::{Request, Response},
|
||||
AppHandle, Error as TauriError, Manager, Runtime, UriSchemeContext,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Extension source type (production vs development)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub source: ExtensionSource,
|
||||
pub manifest: ExtensionManifest,
|
||||
pub enabled: bool,
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data for performance
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
pub cached_at: SystemTime,
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
}
|
||||
*/
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate filesystem permissions
|
||||
/* if let Some(fs_perms) = &extension.manifest.permissions.filesystem {
|
||||
fs_perms.validate()?;
|
||||
} */
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// Dev extensions take priority
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ExtensionInfo {
|
||||
id: String,
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
// src-tauri/src/extension/database/mod.rs
|
||||
|
||||
pub mod permissions;
|
||||
|
||||
use crate::crdt::hlc::HlcService;
|
||||
use crate::crdt::transformer::CrdtTransformer;
|
||||
use crate::crdt::trigger;
|
||||
use crate::database::core::{parse_sql_statements, with_connection, ValueConverter};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission, PermissionError};
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::AppState;
|
||||
use permissions::{check_read_permission, check_write_permission};
|
||||
use rusqlite::params_from_iter;
|
||||
use rusqlite::types::Value as SqlValue;
|
||||
use rusqlite::Transaction;
|
||||
@ -17,36 +17,6 @@ use serde_json::Value as JsonValue;
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use std::collections::HashSet;
|
||||
use tauri::State;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Combined error type für Extension-Database operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionDatabaseError {
|
||||
#[error("Permission denied: {source}")]
|
||||
Permission {
|
||||
#[from]
|
||||
source: PermissionError,
|
||||
},
|
||||
#[error("Database error: {source}")]
|
||||
Database {
|
||||
#[from]
|
||||
source: DatabaseError,
|
||||
},
|
||||
#[error("Parameter validation failed: {reason}")]
|
||||
ParameterValidation { reason: String },
|
||||
#[error("Statement execution failed: {reason}")]
|
||||
StatementExecution { reason: String },
|
||||
}
|
||||
|
||||
// Für Tauri Command Serialization
|
||||
impl serde::Serialize for ExtensionDatabaseError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Führt Statements mit korrekter Parameter-Bindung aus
|
||||
pub struct StatementExecutor<'a> {
|
||||
@ -67,30 +37,27 @@ impl<'a> StatementExecutor<'a> {
|
||||
&self,
|
||||
statement: &Statement,
|
||||
params: &[SqlValue],
|
||||
) -> Result<(), ExtensionDatabaseError> {
|
||||
) -> Result<(), DatabaseError> {
|
||||
let sql = statement.to_string();
|
||||
let expected_params = count_sql_placeholders(&sql);
|
||||
|
||||
if expected_params != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch for statement: {} (expected: {}, provided: {})",
|
||||
truncate_sql(&sql, 100),
|
||||
expected_params,
|
||||
params.len()
|
||||
),
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: expected_params,
|
||||
provided: params.len(),
|
||||
sql,
|
||||
});
|
||||
}
|
||||
|
||||
self.transaction
|
||||
.execute(&sql, params_from_iter(params.iter()))
|
||||
.map_err(|e| ExtensionDatabaseError::StatementExecution {
|
||||
reason: format!(
|
||||
"Failed to execute statement on table {}: {}",
|
||||
.map_err(|e| DatabaseError::ExecutionError {
|
||||
sql,
|
||||
table: Some(
|
||||
self.extract_table_name_from_statement(statement)
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
e
|
||||
),
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@ -147,7 +114,7 @@ pub async fn extension_sql_execute(
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
hlc_service: State<'_, HlcService>,
|
||||
) -> Result<Vec<String>, ExtensionDatabaseError> {
|
||||
) -> Result<Vec<String>, ExtensionError> {
|
||||
// Permission check
|
||||
check_write_permission(&state.db, &extension_id, sql).await?;
|
||||
|
||||
@ -208,7 +175,7 @@ pub async fn extension_sql_execute(
|
||||
|
||||
Ok(modified_schema_tables.into_iter().collect())
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -217,7 +184,7 @@ pub async fn extension_sql_select(
|
||||
params: Vec<JsonValue>,
|
||||
extension_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<JsonValue>, ExtensionDatabaseError> {
|
||||
) -> Result<Vec<JsonValue>, ExtensionError> {
|
||||
// Permission check
|
||||
check_read_permission(&state.db, &extension_id, sql).await?;
|
||||
|
||||
@ -234,8 +201,13 @@ pub async fn extension_sql_select(
|
||||
// Validate that all statements are queries
|
||||
for stmt in &ast_vec {
|
||||
if !matches!(stmt, Statement::Query(_)) {
|
||||
return Err(ExtensionDatabaseError::StatementExecution {
|
||||
reason: "Only SELECT statements are allowed in extension_sql_select".to_string(),
|
||||
return Err(ExtensionError::Database {
|
||||
source: DatabaseError::ExecutionError {
|
||||
sql: sql.to_string(),
|
||||
reason: "Only SELECT statements are allowed in extension_sql_select"
|
||||
.to_string(),
|
||||
table: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -285,7 +257,7 @@ pub async fn extension_sql_select(
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
.map_err(ExtensionDatabaseError::from)
|
||||
.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
/// Konvertiert eine SQLite-Zeile zu JSON
|
||||
@ -309,16 +281,14 @@ fn row_to_json_value(
|
||||
}
|
||||
|
||||
/// Validiert Parameter gegen SQL-Platzhalter
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), ExtensionDatabaseError> {
|
||||
fn validate_params(sql: &str, params: &[JsonValue]) -> Result<(), DatabaseError> {
|
||||
let total_placeholders = count_sql_placeholders(sql);
|
||||
|
||||
if total_placeholders != params.len() {
|
||||
return Err(ExtensionDatabaseError::ParameterValidation {
|
||||
reason: format!(
|
||||
"Parameter count mismatch: SQL has {} placeholders but {} parameters provided",
|
||||
total_placeholders,
|
||||
params.len()
|
||||
),
|
||||
return Err(DatabaseError::ParameterMismatchError {
|
||||
expected: total_placeholders,
|
||||
provided: params.len(),
|
||||
sql: sql.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -5,52 +5,18 @@ use crate::database::core::{
|
||||
};
|
||||
use crate::database::error::DatabaseError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::models::DbExtensionPermission;
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlparser::ast::{Statement, TableFactor, TableObject};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Serialize, Deserialize)]
|
||||
pub enum PermissionError {
|
||||
#[error("Extension '{extension_id}' has no {operation} permission for {resource}: {reason}")]
|
||||
AccessDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: String,
|
||||
reason: String,
|
||||
},
|
||||
#[error("Database error while checking permissions: {source}")]
|
||||
Database {
|
||||
#[from]
|
||||
source: DatabaseError,
|
||||
},
|
||||
#[error("SQL parsing error: {reason}")]
|
||||
SqlParse { reason: String },
|
||||
#[error("Invalid SQL statement: {reason}")]
|
||||
InvalidStatement { reason: String },
|
||||
#[error("No SQL statement found")]
|
||||
NoStatement,
|
||||
#[error("Unsupported statement type for permission check")]
|
||||
UnsupportedStatement,
|
||||
#[error("No table specified in {statement_type} statement")]
|
||||
NoTableSpecified { statement_type: String },
|
||||
}
|
||||
|
||||
// Hilfsfunktion für bessere Lesbarkeit
|
||||
impl PermissionError {
|
||||
pub fn access_denied(
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
resource: &str,
|
||||
reason: &str,
|
||||
) -> Self {
|
||||
Self::AccessDenied {
|
||||
extension_id: extension_id.to_string(),
|
||||
operation: operation.to_string(),
|
||||
resource: resource.to_string(),
|
||||
reason: reason.to_string(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DbExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Prüft Leseberechtigungen für eine Extension
|
||||
@ -58,9 +24,10 @@ pub async fn check_read_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match statement {
|
||||
@ -68,9 +35,11 @@ pub async fn check_read_permission(
|
||||
let tables = extract_table_names_from_sql(&query.to_string())?;
|
||||
check_table_permissions(connection, extension_id, &tables, "read").await
|
||||
}
|
||||
_ => Err(PermissionError::InvalidStatement {
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "Only SELECT statements are allowed for read operations".to_string(),
|
||||
}),
|
||||
sql: sql.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,9 +48,10 @@ pub async fn check_write_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
sql: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| PermissionError::SqlParse {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
|
||||
reason: e.to_string(),
|
||||
sql: sql.to_string(),
|
||||
})?;
|
||||
|
||||
match statement {
|
||||
@ -111,38 +81,44 @@ pub async fn check_write_permission(
|
||||
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
|
||||
check_table_permissions(connection, extension_id, &table_names, "drop").await
|
||||
}
|
||||
_ => Err(PermissionError::UnsupportedStatement),
|
||||
_ => Err(DatabaseError::UnsupportedStatement {
|
||||
reason: "SQL Statement is not allowed".to_string(),
|
||||
sql: sql.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus INSERT-Statement
|
||||
fn extract_table_name_from_insert(
|
||||
insert: &sqlparser::ast::Insert,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
match &insert.table {
|
||||
TableObject::TableName(name) => Ok(name.to_string()),
|
||||
_ => Err(PermissionError::NoTableSpecified {
|
||||
statement_type: "INSERT".to_string(),
|
||||
}),
|
||||
_ => Err(DatabaseError::NoTableError {
|
||||
sql: insert.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus TableFactor
|
||||
fn extract_table_name_from_table_factor(
|
||||
table_factor: &TableFactor,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
match table_factor {
|
||||
TableFactor::Table { name, .. } => Ok(name.to_string()),
|
||||
_ => Err(PermissionError::InvalidStatement {
|
||||
_ => Err(DatabaseError::StatementError {
|
||||
reason: "Complex table references not supported".to_string(),
|
||||
}),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrahiert Tabellenname aus DELETE-Statement
|
||||
fn extract_table_name_from_delete(
|
||||
delete: &sqlparser::ast::Delete,
|
||||
) -> Result<String, PermissionError> {
|
||||
) -> Result<String, ExtensionError> {
|
||||
use sqlparser::ast::FromTable;
|
||||
|
||||
let table_name = match &delete.from {
|
||||
@ -152,9 +128,10 @@ fn extract_table_name_from_delete(
|
||||
} else if !delete.tables.is_empty() {
|
||||
delete.tables[0].to_string()
|
||||
} else {
|
||||
return Err(PermissionError::NoTableSpecified {
|
||||
statement_type: "DELETE".to_string(),
|
||||
});
|
||||
return Err(DatabaseError::NoTableError {
|
||||
sql: delete.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -168,7 +145,7 @@ async fn check_single_table_permission(
|
||||
extension_id: &str,
|
||||
table_name: &str,
|
||||
operation: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
) -> Result<(), ExtensionError> {
|
||||
check_table_permissions(
|
||||
connection,
|
||||
extension_id,
|
||||
@ -184,7 +161,7 @@ async fn check_table_permissions(
|
||||
extension_id: &str,
|
||||
table_names: &[String],
|
||||
operation: &str,
|
||||
) -> Result<(), PermissionError> {
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions =
|
||||
get_extension_permissions(connection, extension_id, "database", operation).await?;
|
||||
|
||||
@ -194,11 +171,10 @@ async fn check_table_permissions(
|
||||
.any(|perm| perm.path.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(PermissionError::access_denied(
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("table '{}'", table_name),
|
||||
"Table not in permitted resources",
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -207,7 +183,7 @@ async fn check_table_permissions(
|
||||
}
|
||||
|
||||
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
|
||||
async fn get_extension_permissions(
|
||||
pub async fn get_extension_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
resource: &str,
|
||||
@ -240,10 +216,7 @@ async fn get_extension_permissions(
|
||||
|
||||
let mut permissions = Vec::new();
|
||||
for row_result in rows {
|
||||
let permission = row_result.map_err(|e| DatabaseError::PermissionError {
|
||||
extension_id: extension_id.to_string(),
|
||||
operation: Some(operation.to_string()),
|
||||
resource: Some(resource.to_string()),
|
||||
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
|
||||
reason: e.to_string(),
|
||||
})?;
|
||||
permissions.push(permission);
|
||||
@ -255,6 +228,8 @@ async fn get_extension_permissions(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -269,7 +244,7 @@ mod tests {
|
||||
fn test_parse_invalid_sql() {
|
||||
let sql = "INVALID SQL";
|
||||
let result = parse_single_statement(sql);
|
||||
// parse_single_statement gibt DatabaseError zurück, nicht PermissionError
|
||||
// parse_single_statement gibt DatabaseError zurück, nicht DatabaseError
|
||||
assert!(result.is_err());
|
||||
// Wenn du spezifischer sein möchtest, kannst du den DatabaseError-Typ prüfen:
|
||||
match result {
|
||||
@ -284,11 +259,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/* #[test]
|
||||
fn test_permission_error_access_denied() {
|
||||
let error = PermissionError::access_denied("ext1", "read", "table1", "not allowed");
|
||||
let error = ExtensionError::access_denied("ext1", "read", "table1", "not allowed");
|
||||
match error {
|
||||
PermissionError::AccessDenied {
|
||||
ExtensionError::AccessDenied {
|
||||
extension_id,
|
||||
operation,
|
||||
resource,
|
||||
@ -301,5 +276,5 @@ mod tests {
|
||||
}
|
||||
_ => panic!("Expected AccessDenied error"),
|
||||
}
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
214
src-tauri/src/extension/error.rs
Normal file
214
src-tauri/src/extension/error.rs
Normal file
@ -0,0 +1,214 @@
|
||||
/// src-tauri/src/extension/error.rs
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::database::error::DatabaseError;
|
||||
|
||||
/// Comprehensive error type for extension operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
#[error("Security violation: {reason}")]
|
||||
SecurityViolation { reason: String },
|
||||
|
||||
#[error("Extension not found: {id}")]
|
||||
NotFound { id: String },
|
||||
|
||||
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||
PermissionDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: String,
|
||||
},
|
||||
|
||||
#[error("Database operation failed: {source}")]
|
||||
Database {
|
||||
#[from]
|
||||
source: DatabaseError,
|
||||
},
|
||||
|
||||
#[error("Filesystem operation failed: {source}")]
|
||||
Filesystem {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
// oder: source: FilesystemError,
|
||||
},
|
||||
|
||||
#[error("HTTP request failed: {reason}")]
|
||||
Http {
|
||||
reason: String,
|
||||
#[source]
|
||||
source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||
},
|
||||
|
||||
#[error("Shell command failed: {reason}")]
|
||||
Shell {
|
||||
reason: String,
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
|
||||
/* #[error("IO error: {source}")]
|
||||
Io {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
}, */
|
||||
#[error("Manifest error: {reason}")]
|
||||
ManifestError { reason: String },
|
||||
|
||||
#[error("Validation error: {reason}")]
|
||||
ValidationError { reason: String },
|
||||
|
||||
#[error("Dev server error: {reason}")]
|
||||
DevServerError { reason: String },
|
||||
|
||||
#[error("Serialization error: {reason}")]
|
||||
SerializationError { reason: String },
|
||||
|
||||
#[error("Configuration error: {reason}")]
|
||||
ConfigError { reason: String },
|
||||
}
|
||||
|
||||
impl ExtensionError {
|
||||
/// Convenience constructor for permission denied errors
|
||||
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
|
||||
Self::PermissionDenied {
|
||||
extension_id: extension_id.to_string(),
|
||||
operation: operation.to_string(),
|
||||
resource: resource.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for HTTP errors
|
||||
pub fn http_error(reason: &str) -> Self {
|
||||
Self::Http {
|
||||
reason: reason.to_string(),
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for HTTP errors with source
|
||||
pub fn http_error_with_source(
|
||||
reason: &str,
|
||||
source: Box<dyn std::error::Error + Send + Sync>,
|
||||
) -> Self {
|
||||
Self::Http {
|
||||
reason: reason.to_string(),
|
||||
source: Some(source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience constructor for shell errors
|
||||
pub fn shell_error(reason: &str, exit_code: Option<i32>) -> Self {
|
||||
Self::Shell {
|
||||
reason: reason.to_string(),
|
||||
exit_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this error is related to permissions
|
||||
pub fn is_permission_error(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ExtensionError::PermissionDenied { .. } | ExtensionError::SecurityViolation { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract extension ID if available
|
||||
pub fn extension_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
ExtensionError::PermissionDenied { extension_id, .. } => Some(extension_id),
|
||||
ExtensionError::Database { source } => source.extension_id(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExtensionError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
|
||||
|
||||
// Error type as discriminator
|
||||
let error_type = match self {
|
||||
ExtensionError::SecurityViolation { .. } => "SecurityViolation",
|
||||
ExtensionError::NotFound { .. } => "NotFound",
|
||||
ExtensionError::PermissionDenied { .. } => "PermissionDenied",
|
||||
ExtensionError::Database { .. } => "Database",
|
||||
ExtensionError::Filesystem { .. } => "Filesystem",
|
||||
ExtensionError::Http { .. } => "Http",
|
||||
ExtensionError::Shell { .. } => "Shell",
|
||||
//ExtensionError::Io { .. } => "Io",
|
||||
ExtensionError::ManifestError { .. } => "ManifestError",
|
||||
ExtensionError::ValidationError { .. } => "ValidationError",
|
||||
ExtensionError::DevServerError { .. } => "DevServerError",
|
||||
ExtensionError::SerializationError { .. } => "SerializationError",
|
||||
ExtensionError::ConfigError { .. } => "ConfigError",
|
||||
};
|
||||
|
||||
state.serialize_field("type", error_type)?;
|
||||
state.serialize_field("message", &self.to_string())?;
|
||||
|
||||
// Add extension_id if available
|
||||
if let Some(ext_id) = self.extension_id() {
|
||||
state.serialize_field("extension_id", ext_id)?;
|
||||
} else {
|
||||
state.serialize_field("extension_id", &Option::<String>::None)?;
|
||||
}
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
// For Tauri command serialization
|
||||
impl From<serde_json::Error> for ExtensionError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ExtensionError::SerializationError {
|
||||
reason: err.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::database::error::DatabaseError;
|
||||
|
||||
/* #[test]
|
||||
fn test_database_error_conversion() {
|
||||
let db_error = DatabaseError::access_denied("ext1", "read", "users", "no permission");
|
||||
let ext_error: ExtensionError = db_error.into();
|
||||
|
||||
assert!(ext_error.is_permission_error());
|
||||
assert_eq!(ext_error.extension_id(), Some("ext1"));
|
||||
} */
|
||||
|
||||
#[test]
|
||||
fn test_permission_denied_constructor() {
|
||||
let error = ExtensionError::permission_denied("ext1", "write", "config.json");
|
||||
|
||||
match error {
|
||||
ExtensionError::PermissionDenied {
|
||||
extension_id,
|
||||
operation,
|
||||
resource,
|
||||
} => {
|
||||
assert_eq!(extension_id, "ext1");
|
||||
assert_eq!(operation, "write");
|
||||
assert_eq!(resource, "config.json");
|
||||
}
|
||||
_ => panic!("Expected PermissionDenied error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
let error = ExtensionError::permission_denied("ext1", "read", "database");
|
||||
let serialized = serde_json::to_string(&error).unwrap();
|
||||
|
||||
// Basic check that it serializes properly
|
||||
assert!(serialized.contains("PermissionDenied"));
|
||||
assert!(serialized.contains("ext1"));
|
||||
}
|
||||
}
|
||||
186
src-tauri/src/extension/filesystem/core.rs
Normal file
186
src-tauri/src/extension/filesystem/core.rs
Normal file
@ -0,0 +1,186 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
/// Simple filesystem permissions using path patterns with environment-style variables
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FilesystemPermissions {
|
||||
/// Read access to files and directories
|
||||
/// Examples: ["$DOCUMENT/**", "$PICTURE/*.jpg", "$APPDATA/my-extension/*"]
|
||||
pub read: Option<Vec<String>>,
|
||||
/// Write access to files and directories (includes create/delete)
|
||||
/// Examples: ["$APPDATA/my-extension/**", "$DOWNLOAD/*.pdf"]
|
||||
pub write: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FilesystemPermissions {
|
||||
/// Helper to create common permission patterns
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
read: None,
|
||||
write: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add read permission for a path pattern
|
||||
pub fn add_read(&mut self, pattern: &str) {
|
||||
match &mut self.read {
|
||||
Some(patterns) => patterns.push(pattern.to_string()),
|
||||
None => self.read = Some(vec![pattern.to_string()]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add write permission for a path pattern
|
||||
pub fn add_write(&mut self, pattern: &str) {
|
||||
match &mut self.write {
|
||||
Some(patterns) => patterns.push(pattern.to_string()),
|
||||
None => self.write = Some(vec![pattern.to_string()]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Add extension's own data directory permissions
|
||||
pub fn extension_data(extension_id: &str) -> Self {
|
||||
Self {
|
||||
read: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
|
||||
write: Some(vec![format!("$APPDATA/extensions/{}/**", extension_id)]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Add document access permissions
|
||||
pub fn documents_read_only() -> Self {
|
||||
Self {
|
||||
read: Some(vec!["$DOCUMENT/**".to_string()]),
|
||||
write: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: Add picture access permissions
|
||||
pub fn pictures_read_only() -> Self {
|
||||
Self {
|
||||
read: Some(vec!["$PICTURE/**".to_string()]),
|
||||
write: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate all path patterns
|
||||
pub fn validate(&self) -> Result<(), ExtensionError> {
|
||||
if let Some(read_patterns) = &self.read {
|
||||
for pattern in read_patterns {
|
||||
validate_path_pattern(pattern)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(write_patterns) = &self.write {
|
||||
for pattern in write_patterns {
|
||||
validate_path_pattern(pattern)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates a filesystem path pattern
|
||||
fn validate_path_pattern(pattern: &str) -> Result<(), ExtensionError> {
|
||||
if pattern.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Path pattern cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check if pattern starts with valid base directory variable
|
||||
let valid_bases = [
|
||||
"$APPDATA",
|
||||
"$APPCACHE",
|
||||
"$APPCONFIG",
|
||||
"$APPLOCALDATA",
|
||||
"$APPLOG",
|
||||
"$AUDIO",
|
||||
"$CACHE",
|
||||
"$CONFIG",
|
||||
"$DATA",
|
||||
"$LOCALDATA",
|
||||
"$DESKTOP",
|
||||
"$DOCUMENT",
|
||||
"$DOWNLOAD",
|
||||
"$EXECUTABLE",
|
||||
"$FONT",
|
||||
"$HOME",
|
||||
"$PICTURE",
|
||||
"$PUBLIC",
|
||||
"$RUNTIME",
|
||||
"$TEMPLATE",
|
||||
"$VIDEO",
|
||||
"$RESOURCE",
|
||||
"$TEMP",
|
||||
];
|
||||
|
||||
let starts_with_valid_base = valid_bases.iter().any(|&base| {
|
||||
pattern.starts_with(base)
|
||||
&& (pattern.len() == base.len() || pattern.chars().nth(base.len()) == Some('/'))
|
||||
});
|
||||
|
||||
if !starts_with_valid_base {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!(
|
||||
"Path pattern '{}' must start with a valid base directory: {}",
|
||||
pattern,
|
||||
valid_bases.join(", ")
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if pattern.contains("../") || pattern.contains("..\\") {
|
||||
return Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal detected in pattern: {}", pattern),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves a path pattern to actual filesystem paths using Tauri's BaseDirectory
|
||||
pub fn resolve_path_pattern(
|
||||
pattern: &str,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> Result<(String, String), ExtensionError> {
|
||||
let (base_var, relative_path) = if let Some(slash_pos) = pattern.find('/') {
|
||||
(&pattern[..slash_pos], &pattern[slash_pos + 1..])
|
||||
} else {
|
||||
(pattern, "")
|
||||
};
|
||||
|
||||
let base_directory = match base_var {
|
||||
"$APPDATA" => "AppData",
|
||||
"$APPCACHE" => "AppCache",
|
||||
"$APPCONFIG" => "AppConfig",
|
||||
"$APPLOCALDATA" => "AppLocalData",
|
||||
"$APPLOG" => "AppLog",
|
||||
"$AUDIO" => "Audio",
|
||||
"$CACHE" => "Cache",
|
||||
"$CONFIG" => "Config",
|
||||
"$DATA" => "Data",
|
||||
"$LOCALDATA" => "LocalData",
|
||||
"$DESKTOP" => "Desktop",
|
||||
"$DOCUMENT" => "Document",
|
||||
"$DOWNLOAD" => "Download",
|
||||
"$EXECUTABLE" => "Executable",
|
||||
"$FONT" => "Font",
|
||||
"$HOME" => "Home",
|
||||
"$PICTURE" => "Picture",
|
||||
"$PUBLIC" => "Public",
|
||||
"$RUNTIME" => "Runtime",
|
||||
"$TEMPLATE" => "Template",
|
||||
"$VIDEO" => "Video",
|
||||
"$RESOURCE" => "Resource",
|
||||
"$TEMP" => "Temp",
|
||||
_ => {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: format!("Unknown base directory variable: {}", base_var),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok((base_directory.to_string(), relative_path.to_string()))
|
||||
}
|
||||
2
src-tauri/src/extension/filesystem/mod.rs
Normal file
2
src-tauri/src/extension/filesystem/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod core;
|
||||
pub mod permissions;
|
||||
101
src-tauri/src/extension/filesystem/permissions.rs
Normal file
101
src-tauri/src/extension/filesystem/permissions.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FilesystemPermissions {
|
||||
/// Read access to files and directories
|
||||
pub read: Option<Vec<FilesystemPath>>,
|
||||
/// Write access to files and directories (includes create/delete)
|
||||
pub write: Option<Vec<FilesystemPath>>,
|
||||
}
|
||||
|
||||
/// Cross-platform filesystem path specification
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FilesystemPath {
|
||||
/// The type of path (determines base directory)
|
||||
pub path_type: FilesystemPathType,
|
||||
/// Relative path from the base directory
|
||||
pub relative_path: String,
|
||||
/// Whether subdirectories are included (recursive)
|
||||
pub recursive: bool,
|
||||
}
|
||||
|
||||
/// Platform-agnostic path types that map to appropriate directories on each OS
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum FilesystemPathType {
|
||||
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
|
||||
AppData,
|
||||
/// App's cache directory
|
||||
AppCache,
|
||||
/// App's configuration directory
|
||||
AppConfig,
|
||||
/// User's documents directory
|
||||
Documents,
|
||||
/// User's pictures directory
|
||||
Pictures,
|
||||
/// User's downloads directory
|
||||
Downloads,
|
||||
/// Temporary directory
|
||||
Temp,
|
||||
/// Extension's own private directory (always allowed)
|
||||
ExtensionData,
|
||||
/// Shared data between extensions (requires special permission)
|
||||
SharedData,
|
||||
}
|
||||
|
||||
impl FilesystemPath {
|
||||
/// Creates a new filesystem path specification
|
||||
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
|
||||
Self {
|
||||
path_type,
|
||||
relative_path: relative_path.to_string(),
|
||||
recursive,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the path to an actual system path
|
||||
/// This would be implemented in your Tauri backend
|
||||
pub fn resolve_system_path(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> Result<String, ExtensionError> {
|
||||
/* let base_dir = match self.path_type {
|
||||
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
|
||||
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
|
||||
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
|
||||
FilesystemPathType::Documents => app_handle.path().document_dir(),
|
||||
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
|
||||
FilesystemPathType::Downloads => app_handle.path().download_dir(),
|
||||
FilesystemPathType::Temp => app_handle.path().temp_dir(),
|
||||
FilesystemPathType::ExtensionData => app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map(|p| p.join("extensions")),
|
||||
FilesystemPathType::SharedData => {
|
||||
app_handle.path().app_data_dir().map(|p| p.join("shared"))
|
||||
}
|
||||
}
|
||||
.map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to resolve base directory: {}", e),
|
||||
})?;
|
||||
|
||||
let final_path = base_dir.join(&self.relative_path);
|
||||
|
||||
// Security check - ensure the resolved path is still within the base directory
|
||||
if let (Ok(canonical_final), Ok(canonical_base)) =
|
||||
(final_path.canonicalize(), base_dir.canonicalize())
|
||||
{
|
||||
if !canonical_final.starts_with(&canonical_base) {
|
||||
return Err(ExtensionError::SecurityViolation {
|
||||
reason: format!(
|
||||
"Path traversal detected: {} escapes base directory",
|
||||
self.relative_path
|
||||
),
|
||||
});
|
||||
}
|
||||
} */
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,5 @@
|
||||
pub mod core;
|
||||
pub mod database;
|
||||
pub mod error;
|
||||
pub mod filesystem;
|
||||
pub mod permission_manager;
|
||||
|
||||
297
src-tauri/src/extension/permission_manager.rs
Normal file
297
src-tauri/src/extension/permission_manager.rs
Normal file
@ -0,0 +1,297 @@
|
||||
/// src-tauri/src/extension/permission_manager.rs
|
||||
|
||||
use crate::extension::error::ExtensionError;
|
||||
use crate::database::DbConnection;
|
||||
use crate::extension::database::permissions::DbExtensionPermission;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Url;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Vec<DbExtensionPermission>,
|
||||
pub filesystem: Vec<FilesystemPermission>,
|
||||
pub http: Vec<HttpPermission>,
|
||||
pub shell: Vec<ShellPermission>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct FilesystemPermission {
|
||||
pub extension_id: String,
|
||||
pub operation: String, // read, write, create, delete
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct HttpPermission {
|
||||
pub extension_id: String,
|
||||
pub operation: String, // get, post, put, delete
|
||||
pub domain: String,
|
||||
pub path_pattern: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ShellPermission {
|
||||
pub extension_id: String,
|
||||
pub command: String,
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
/// Zentraler Permission Manager
|
||||
pub struct PermissionManager;
|
||||
|
||||
impl PermissionManager {
|
||||
/// Prüft Datenbankberechtigungen
|
||||
pub async fn check_database_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
table_name: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_database_permissions(connection, extension_id, operation).await?;
|
||||
|
||||
let has_permission = permissions
|
||||
.iter()
|
||||
.any(|perm| perm.path.contains(table_name));
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("database table '{}'", table_name),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Dateisystem-Berechtigungen
|
||||
pub async fn check_filesystem_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_filesystem_permissions(connection, extension_id, operation).await?;
|
||||
|
||||
let file_path_str = file_path.to_string_lossy();
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe, ob der Pfad mit einem erlaubten Pfad beginnt oder übereinstimmt
|
||||
file_path_str.starts_with(&perm.path) ||
|
||||
// Oder ob es ein Wildcard-Match gibt
|
||||
Self::matches_path_pattern(&perm.path, &file_path_str)
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
operation,
|
||||
&format!("filesystem path '{}'", file_path_str),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft HTTP-Berechtigungen
|
||||
pub async fn check_http_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
method: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_http_permissions(connection, extension_id, method).await?;
|
||||
|
||||
let url_parsed = Url::parse(url).map_err(|e| {
|
||||
ExtensionError::ValidationError {
|
||||
reason: format!("Invalid URL: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
let domain = url_parsed.host_str().unwrap_or("");
|
||||
let path = url_parsed.path();
|
||||
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe Domain
|
||||
let domain_matches = perm.domain == "*" ||
|
||||
perm.domain == domain ||
|
||||
domain.ends_with(&format!(".{}", perm.domain));
|
||||
|
||||
// Prüfe Pfad (falls spezifiziert)
|
||||
let path_matches = perm.path_pattern.as_ref()
|
||||
.map(|pattern| Self::matches_path_pattern(pattern, path))
|
||||
.unwrap_or(true);
|
||||
|
||||
domain_matches && path_matches
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
method,
|
||||
&format!("HTTP request to '{}'", url),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft Shell-Berechtigungen
|
||||
pub async fn check_shell_permission(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
let permissions = Self::get_shell_permissions(connection, extension_id).await?;
|
||||
|
||||
let has_permission = permissions.iter().any(|perm| {
|
||||
// Prüfe Command
|
||||
if perm.command != command && perm.command != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe Arguments (falls spezifiziert)
|
||||
if !perm.arguments.is_empty() {
|
||||
// Alle erforderlichen Args müssen vorhanden sein
|
||||
perm.arguments.iter().all(|required_arg| {
|
||||
args.iter().any(|actual_arg| {
|
||||
required_arg == actual_arg || required_arg == "*"
|
||||
})
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if !has_permission {
|
||||
return Err(ExtensionError::permission_denied(
|
||||
extension_id,
|
||||
"execute",
|
||||
&format!("shell command '{}' with args {:?}", command, args),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Private Helper-Methoden
|
||||
|
||||
async fn get_database_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
) -> Result<Vec<DbExtensionPermission>, ExtensionError> {
|
||||
// Verwende die bestehende Funktion aus dem permissions.rs
|
||||
crate::extension::database::permissions::get_extension_permissions(
|
||||
connection,
|
||||
extension_id,
|
||||
"database",
|
||||
operation
|
||||
).await.map_err(ExtensionError::from)
|
||||
}
|
||||
|
||||
async fn get_filesystem_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
operation: &str,
|
||||
) -> Result<Vec<FilesystemPermission>, ExtensionError> {
|
||||
// Implementierung für Filesystem-Permissions
|
||||
// Ähnlich wie get_database_permissions, aber für filesystem Tabelle
|
||||
todo!("Implementiere Filesystem-Permission-Loading")
|
||||
}
|
||||
|
||||
async fn get_http_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
method: &str,
|
||||
) -> Result<Vec<HttpPermission>, ExtensionError> {
|
||||
// Implementierung für HTTP-Permissions
|
||||
todo!("Implementiere HTTP-Permission-Loading")
|
||||
}
|
||||
|
||||
async fn get_shell_permissions(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
) -> Result<Vec<ShellPermission>, ExtensionError> {
|
||||
// Implementierung für Shell-Permissions
|
||||
todo!("Implementiere Shell-Permission-Loading")
|
||||
}
|
||||
|
||||
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
|
||||
// Einfache Wildcard-Implementierung
|
||||
if pattern.ends_with('*') {
|
||||
let prefix = &pattern[..pattern.len() - 1];
|
||||
path.starts_with(prefix)
|
||||
} else if pattern.starts_with('*') {
|
||||
let suffix = &pattern[1..];
|
||||
path.ends_with(suffix)
|
||||
} else {
|
||||
pattern == path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience-Funktionen für die verschiedenen Subsysteme
|
||||
impl PermissionManager {
|
||||
/// Convenience für Datei lesen
|
||||
pub async fn can_read_file(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(connection, extension_id, "read", file_path).await
|
||||
}
|
||||
|
||||
/// Convenience für Datei schreiben
|
||||
pub async fn can_write_file(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
file_path: &Path,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_filesystem_permission(connection, extension_id, "write", file_path).await
|
||||
}
|
||||
|
||||
/// Convenience für HTTP GET
|
||||
pub async fn can_http_get(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(connection, extension_id, "GET", url).await
|
||||
}
|
||||
|
||||
/// Convenience für HTTP POST
|
||||
pub async fn can_http_post(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
url: &str,
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_http_permission(connection, extension_id, "POST", url).await
|
||||
}
|
||||
|
||||
/// Convenience für Shell-Befehl
|
||||
pub async fn can_execute_command(
|
||||
connection: &DbConnection,
|
||||
extension_id: &str,
|
||||
command: &str,
|
||||
args: &[String],
|
||||
) -> Result<(), ExtensionError> {
|
||||
Self::check_shell_permission(connection, extension_id, command, args).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_path_pattern_matching() {
|
||||
assert!(PermissionManager::matches_path_pattern("/home/user/*", "/home/user/documents/file.txt"));
|
||||
assert!(PermissionManager::matches_path_pattern("*.txt", "/path/to/file.txt"));
|
||||
assert!(PermissionManager::matches_path_pattern("/exact/path", "/exact/path"));
|
||||
|
||||
assert!(!PermissionManager::matches_path_pattern("/home/user/*", "/etc/passwd"));
|
||||
assert!(!PermissionManager::matches_path_pattern("*.txt", "/path/to/file.pdf"));
|
||||
}
|
||||
}
|
||||
@ -3,27 +3,34 @@
|
||||
mod crdt;
|
||||
mod database;
|
||||
mod extension;
|
||||
mod models;
|
||||
|
||||
//mod models;
|
||||
|
||||
pub mod table_names {
|
||||
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
|
||||
}
|
||||
|
||||
use models::ExtensionState;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionState};
|
||||
|
||||
/* use crate::{
|
||||
crdt::hlc::HlcService,
|
||||
database::{AppState, DbConnection},
|
||||
};
|
||||
extension::core::ExtensionState,
|
||||
}; */
|
||||
|
||||
pub struct AppState {
|
||||
pub db: DbConnection,
|
||||
pub hlc: Mutex<HlcService>, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird.
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let protocol_name = "haex-extension";
|
||||
|
||||
tauri::Builder::default()
|
||||
/* .register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||
.register_uri_scheme_protocol(protocol_name, move |context, request| {
|
||||
match extension::core::extension_protocol_handler(&context, &request) {
|
||||
Ok(response) => response, // Wenn der Handler Ok ist, gib die Response direkt zurück
|
||||
Err(e) => {
|
||||
@ -52,7 +59,7 @@ pub fn run() {
|
||||
})
|
||||
}
|
||||
}
|
||||
}) */
|
||||
})
|
||||
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
|
||||
.manage(crdt::hlc::HlcService::new()) */
|
||||
.manage(AppState {
|
||||
|
||||
@ -1,28 +1,45 @@
|
||||
// models.rs
|
||||
// src-tauri/src/models.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
/* #[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Option<DatabasePermissions>,
|
||||
pub http: Option<Vec<String>>,
|
||||
pub filesystem: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DatabasePermissions {
|
||||
pub read: Option<Vec<String>>,
|
||||
pub write: Option<Vec<String>>,
|
||||
pub create: Option<Vec<String>>,
|
||||
}
|
||||
/// Enum to represent the source of an extension
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
/// Production extension installed in app data
|
||||
Production { path: PathBuf, version: String },
|
||||
/// Development mode extension with live reloading
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
} */
|
||||
|
||||
/*
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<std::collections::HashMap<String, ExtensionManifest>>,
|
||||
@ -48,3 +65,43 @@ pub struct DbExtensionPermission {
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Comprehensive error type for all extension-related operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
/// Security violation detected
|
||||
#[error("Security violation: {reason}")]
|
||||
SecurityViolation { reason: String },
|
||||
|
||||
/// Extension not found
|
||||
#[error("Extension not found: {id}")]
|
||||
NotFound { id: String },
|
||||
|
||||
/// Permission denied
|
||||
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||
PermissionDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: String,
|
||||
},
|
||||
|
||||
/// IO error during extension operations
|
||||
#[error("IO error: {source}")]
|
||||
Io {
|
||||
#[from]
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
/// Error during extension manifest parsing
|
||||
#[error("Manifest error: {reason}")]
|
||||
ManifestError { reason: String },
|
||||
|
||||
/// Input validation error
|
||||
#[error("Validation error: {reason}")]
|
||||
ValidationError { reason: String },
|
||||
|
||||
/// Development server error
|
||||
#[error("Dev server error: {reason}")]
|
||||
DevServerError { reason: String },
|
||||
}
|
||||
*/
|
||||
|
||||
34
src-tauri/src/models_final.rs
Normal file
34
src-tauri/src/models_final.rs
Normal file
@ -0,0 +1,34 @@
|
||||
// models.rs
|
||||
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_path_pattern() {
|
||||
// Valid patterns
|
||||
assert!(validate_path_pattern("$PICTURE/**").is_ok());
|
||||
assert!(validate_path_pattern("$DOCUMENT/myfiles/*").is_ok());
|
||||
assert!(validate_path_pattern("$APPDATA/extensions/my-ext/**").is_ok());
|
||||
|
||||
// Invalid patterns
|
||||
assert!(validate_path_pattern("").is_err());
|
||||
assert!(validate_path_pattern("$INVALID/test").is_err());
|
||||
assert!(validate_path_pattern("$PICTURE/../secret").is_err());
|
||||
assert!(validate_path_pattern("relative/path").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filesystem_permissions() {
|
||||
let mut perms = FilesystemPermissions::new();
|
||||
perms.add_read("$PICTURE/**");
|
||||
perms.add_write("$APPDATA/my-ext/**");
|
||||
|
||||
assert!(perms.validate().is_ok());
|
||||
assert_eq!(perms.read.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(perms.write.as_ref().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
441
src-tauri/src/models_new.rs
Normal file
441
src-tauri/src/models_new.rs
Normal file
@ -0,0 +1,441 @@
|
||||
// models.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, Arc};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionManifest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: Option<String>,
|
||||
pub entry: String,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: ExtensionPermissions,
|
||||
pub homepage: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ExtensionPermissions {
|
||||
pub database: Option<DatabasePermissions>,
|
||||
pub http: Option<Vec<String>>,
|
||||
pub filesystem: Option<FilesystemPermissions>,
|
||||
pub shell: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DatabasePermissions {
|
||||
pub read: Option<Vec<String>>,
|
||||
pub write: Option<Vec<String>>,
|
||||
pub create: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FilesystemPermissions {
|
||||
/// Read access to files and directories
|
||||
pub read: Option<Vec<FilesystemPath>>,
|
||||
/// Write access to files and directories (includes create/delete)
|
||||
pub write: Option<Vec<FilesystemPath>>,
|
||||
}
|
||||
|
||||
/// Cross-platform filesystem path specification
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct FilesystemPath {
|
||||
/// The type of path (determines base directory)
|
||||
pub path_type: FilesystemPathType,
|
||||
/// Relative path from the base directory
|
||||
pub relative_path: String,
|
||||
/// Whether subdirectories are included (recursive)
|
||||
pub recursive: bool,
|
||||
}
|
||||
|
||||
/// Platform-agnostic path types that map to appropriate directories on each OS
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub enum FilesystemPathType {
|
||||
/// App's data directory ($APPDATA on Windows, ~/.local/share on Linux, etc.)
|
||||
AppData,
|
||||
/// App's cache directory
|
||||
AppCache,
|
||||
/// App's configuration directory
|
||||
AppConfig,
|
||||
/// User's documents directory
|
||||
Documents,
|
||||
/// User's pictures directory
|
||||
Pictures,
|
||||
/// User's downloads directory
|
||||
Downloads,
|
||||
/// Temporary directory
|
||||
Temp,
|
||||
/// Extension's own private directory (always allowed)
|
||||
ExtensionData,
|
||||
/// Shared data between extensions (requires special permission)
|
||||
SharedData,
|
||||
}
|
||||
|
||||
impl FilesystemPath {
|
||||
/// Creates a new filesystem path specification
|
||||
pub fn new(path_type: FilesystemPathType, relative_path: &str, recursive: bool) -> Self {
|
||||
Self {
|
||||
path_type,
|
||||
relative_path: relative_path.to_string(),
|
||||
recursive,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the path to an actual system path
|
||||
/// This would be implemented in your Tauri backend
|
||||
pub fn resolve_system_path(&self, app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, ExtensionError> {
|
||||
let base_dir = match self.path_type {
|
||||
FilesystemPathType::AppData => app_handle.path().app_data_dir(),
|
||||
FilesystemPathType::AppCache => app_handle.path().app_cache_dir(),
|
||||
FilesystemPathType::AppConfig => app_handle.path().app_config_dir(),
|
||||
FilesystemPathType::Documents => app_handle.path().document_dir(),
|
||||
FilesystemPathType::Pictures => app_handle.path().picture_dir(),
|
||||
FilesystemPathType::Downloads => app_handle.path().download_dir(),
|
||||
FilesystemPathType::Temp => app_handle.path().temp_dir(),
|
||||
FilesystemPathType::ExtensionData => {
|
||||
app_handle.path().app_data_dir().map(|p| p.join("extensions"))
|
||||
},
|
||||
FilesystemPathType::SharedData => {
|
||||
app_handle.path().app_data_dir().map(|p| p.join("shared"))
|
||||
},
|
||||
}.map_err(|e| ExtensionError::ValidationError {
|
||||
reason: format!("Failed to resolve base directory: {}", e),
|
||||
})?;
|
||||
|
||||
let final_path = base_dir.join(&self.relative_path);
|
||||
|
||||
// Security check - ensure the resolved path is still within the base directory
|
||||
if let (Ok(canonical_final), Ok(canonical_base)) = (final_path.canonicalize(), base_dir.canonicalize()) {
|
||||
if !canonical_final.starts_with(&canonical_base) {
|
||||
return Err(ExtensionError::SecurityViolation {
|
||||
reason: format!("Path traversal detected: {} escapes base directory", self.relative_path),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(final_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum to represent the source of an extension
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExtensionSource {
|
||||
/// Production extension installed in app data
|
||||
Production {
|
||||
path: PathBuf,
|
||||
version: String,
|
||||
},
|
||||
/// Development mode extension with live reloading
|
||||
Development {
|
||||
dev_server_url: String,
|
||||
manifest_path: PathBuf,
|
||||
auto_reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Complete extension data including source and runtime status
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Extension {
|
||||
/// Unique extension ID
|
||||
pub id: String,
|
||||
/// Extension display name
|
||||
pub name: String,
|
||||
/// Source information (production or dev)
|
||||
pub source: ExtensionSource,
|
||||
/// Complete manifest data
|
||||
pub manifest: ExtensionManifest,
|
||||
/// Enabled status
|
||||
pub enabled: bool,
|
||||
/// Last access timestamp
|
||||
pub last_accessed: SystemTime,
|
||||
}
|
||||
|
||||
/// Cached permission data to avoid frequent database lookups
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedPermission {
|
||||
/// The permissions that were fetched
|
||||
pub permissions: Vec<DbExtensionPermission>,
|
||||
/// When this cache entry was created
|
||||
pub cached_at: SystemTime,
|
||||
/// How long this cache entry is valid
|
||||
pub ttl: Duration,
|
||||
}
|
||||
|
||||
/// Enhanced extension manager with production/dev support and caching
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionManager {
|
||||
/// Production extensions loaded from app data directory
|
||||
pub production_extensions: Mutex<HashMap<String, Extension>>,
|
||||
/// Development mode extensions for live-reloading during development
|
||||
pub dev_extensions: Mutex<HashMap<String, Extension>>,
|
||||
/// Cache for extension permissions to improve performance
|
||||
pub permission_cache: Mutex<HashMap<String, CachedPermission>>,
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
/// Creates a new extension manager
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
production_extensions: Mutex::new(HashMap::new()),
|
||||
dev_extensions: Mutex::new(HashMap::new()),
|
||||
permission_cache: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a production extension to the manager
|
||||
pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Production { .. } => {
|
||||
let mut extensions = self.production_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Production source for production extension".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a development mode extension to the manager
|
||||
pub fn add_dev_extension(&self, extension: Extension) -> Result<(), ExtensionError> {
|
||||
if extension.id.is_empty() {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Extension ID cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
match &extension.source {
|
||||
ExtensionSource::Development { .. } => {
|
||||
let mut extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.insert(extension.id.clone(), extension);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(ExtensionError::ValidationError {
|
||||
reason: "Expected Development source for dev extension".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an extension by its ID
|
||||
pub fn get_extension(&self, extension_id: &str) -> Option<Extension> {
|
||||
// First check development extensions (they take priority)
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if let Some(extension) = dev_extensions.get(extension_id) {
|
||||
return Some(extension.clone());
|
||||
}
|
||||
|
||||
// Then check production extensions
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
prod_extensions.get(extension_id).cloned()
|
||||
}
|
||||
|
||||
/// Removes an extension from the manager
|
||||
pub fn remove_extension(&self, extension_id: &str) -> Result<(), ExtensionError> {
|
||||
// Check dev extensions first
|
||||
{
|
||||
let mut dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
if dev_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Then check production extensions
|
||||
{
|
||||
let mut prod_extensions = self.production_extensions.lock().unwrap();
|
||||
if prod_extensions.remove(extension_id).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ExtensionError::NotFound {
|
||||
id: extension_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets cached permissions or indicates they need to be loaded
|
||||
pub fn get_cached_permissions(
|
||||
&self,
|
||||
extension_id: &str,
|
||||
resource: &str,
|
||||
operation: &str,
|
||||
) -> Option<Vec<DbExtensionPermission>> {
|
||||
let cache = self.permission_cache.lock().unwrap();
|
||||
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
|
||||
|
||||
if let Some(cached) = cache.get(&cache_key) {
|
||||
let now = SystemTime::now();
|
||||
if now.duration_since(cached.cached_at).unwrap_or(Duration::from_secs(0)) < cached.ttl {
|
||||
return Some(cached.permissions.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Updates the permission cache
|
||||
pub fn update_permission_cache(
|
||||
&self,
|
||||
extension_id: &str,
|
||||
resource: &str,
|
||||
operation: &str,
|
||||
permissions: Vec<DbExtensionPermission>,
|
||||
) {
|
||||
let mut cache = self.permission_cache.lock().unwrap();
|
||||
let cache_key = format!("{}-{}-{}", extension_id, resource, operation);
|
||||
|
||||
cache.insert(cache_key, CachedPermission {
|
||||
permissions,
|
||||
cached_at: SystemTime::now(),
|
||||
ttl: Duration::from_secs(60), // Cache for 60 seconds
|
||||
});
|
||||
}
|
||||
|
||||
/// Validates a manifest for security concerns
|
||||
pub fn validate_manifest_security(&self, manifest: &ExtensionManifest) -> Result<(), ExtensionError> {
|
||||
// Check for suspicious permission combinations
|
||||
let has_filesystem = manifest.permissions.filesystem.is_some();
|
||||
let has_database = manifest.permissions.database.is_some();
|
||||
let has_shell = manifest.permissions.shell.is_some();
|
||||
|
||||
if has_filesystem && has_database && has_shell {
|
||||
// This is a powerful combination, warn or check user confirmation elsewhere
|
||||
}
|
||||
|
||||
// Validate ID format
|
||||
if !manifest.id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
|
||||
return Err(ExtensionError::ValidationError {
|
||||
reason: "Invalid extension ID format. Must contain only alphanumeric characters, dash or underscore.".to_string()
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all enabled extensions (both dev and production)
|
||||
pub fn list_enabled_extensions(&self) -> Vec<Extension> {
|
||||
let mut extensions = Vec::new();
|
||||
|
||||
// Add enabled dev extensions first (higher priority)
|
||||
{
|
||||
let dev_extensions = self.dev_extensions.lock().unwrap();
|
||||
extensions.extend(
|
||||
dev_extensions
|
||||
.values()
|
||||
.filter(|ext| ext.enabled)
|
||||
.cloned()
|
||||
);
|
||||
}
|
||||
|
||||
// Add enabled production extensions (avoiding duplicates)
|
||||
{
|
||||
let prod_extensions = self.production_extensions.lock().unwrap();
|
||||
let dev_ids: std::collections::HashSet<String> = extensions.iter().map(|e| e.id.clone()).collect();
|
||||
|
||||
extensions.extend(
|
||||
prod_extensions
|
||||
.values()
|
||||
.filter(|ext| ext.enabled && !dev_ids.contains(&ext.id))
|
||||
.cloned()
|
||||
);
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
}
|
||||
|
||||
// For backward compatibility - will be deprecated
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionState {
|
||||
pub extensions: Mutex<HashMap<String, ExtensionManifest>>,
|
||||
}
|
||||
|
||||
impl ExtensionState {
|
||||
pub fn add_extension(&self, path: String, manifest: ExtensionManifest) {
|
||||
let mut extensions = self.extensions.lock().unwrap();
|
||||
extensions.insert(path, manifest);
|
||||
}
|
||||
|
||||
pub fn get_extension(&self, addon_id: &str) -> Option<ExtensionManifest> {
|
||||
let extensions = self.extensions.lock().unwrap();
|
||||
extensions.values().find(|p| p.name == addon_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DbExtensionPermission {
|
||||
pub id: String,
|
||||
pub extension_id: String,
|
||||
pub resource: String,
|
||||
pub operation: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// Comprehensive error type for all extension-related operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExtensionError {
|
||||
/// Security violation detected
|
||||
#[error("Security violation: {reason}")]
|
||||
SecurityViolation {
|
||||
reason: String
|
||||
},
|
||||
|
||||
/// Extension not found
|
||||
#[error("Extension not found: {id}")]
|
||||
NotFound {
|
||||
id: String
|
||||
},
|
||||
|
||||
/// Permission denied
|
||||
#[error("Permission denied: {extension_id} cannot {operation} on {resource}")]
|
||||
PermissionDenied {
|
||||
extension_id: String,
|
||||
operation: String,
|
||||
resource: String
|
||||
},
|
||||
|
||||
/// IO error during extension operations
|
||||
#[error("IO error: {source}")]
|
||||
Io {
|
||||
#[from]
|
||||
source: std::io::Error
|
||||
},
|
||||
|
||||
/// Error during extension manifest parsing
|
||||
#[error("Manifest error: {reason}")]
|
||||
ManifestError {
|
||||
reason: String
|
||||
},
|
||||
|
||||
/// Input validation error
|
||||
#[error("Validation error: {reason}")]
|
||||
ValidationError {
|
||||
reason: String
|
||||
},
|
||||
|
||||
/// Development server error
|
||||
#[error("Dev server error: {reason}")]
|
||||
DevServerError {
|
||||
reason: String
|
||||
},
|
||||
}
|
||||
|
||||
// For Tauri Command Serialization
|
||||
impl serde::Serialize for ExtensionError {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user