refactored permission system and error handling

This commit is contained in:
2025-09-26 15:35:54 +02:00
parent 2cfd6248bc
commit d025819888
26 changed files with 2312 additions and 300 deletions

View File

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

View File

@ -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(),
});
}
}

View File

@ -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(),
});
}

View File

@ -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
}
}
}
}
} */

View File

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

View File

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

View File

@ -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(),
});
}

View File

@ -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"),
}
}
} */
}

View 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"));
}
}

View 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()))
}

View File

@ -0,0 +1,2 @@
pub mod core;
pub mod permissions;

View 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())
}
}

View File

@ -1,2 +1,5 @@
pub mod core;
pub mod database;
pub mod error;
pub mod filesystem;
pub mod permission_manager;

View 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"));
}
}

View File

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

View File

@ -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 },
}
*/

View 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
View 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())
}
}