refactore manifest and permission

This commit is contained in:
2025-10-02 01:42:30 +02:00
parent 56e75977cd
commit fb577a8699
51 changed files with 5634 additions and 2086 deletions

View File

@ -10,8 +10,8 @@ use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tauri::{AppHandle, Wry};
use tauri_plugin_store::{Store, StoreExt};
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
use thiserror::Error;
use uhlc::{HLCBuilder, Timestamp, HLC, ID};
use uuid::Uuid;

View File

@ -755,6 +755,7 @@ impl CrdtTransformer {
selection: del_stmt.selection.clone(),
returning: None,
or: None,
limit: None,
};
}
Ok(())

View File

@ -1,3 +1,4 @@
// src-tauri/src/crdt/trigger.rs
use crate::table_names::TABLE_CRDT_LOGS;
use rusqlite::{Connection, Result as RusqliteResult, Row, Transaction};
use serde::Serialize;
@ -11,7 +12,7 @@ const UPDATE_TRIGGER_TPL: &str = "z_crdt_{TABLE_NAME}_update";
//const SYNC_ACTIVE_KEY: &str = "sync_active";
pub const TOMBSTONE_COLUMN: &str = "haex_tombstone";
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_hlc_timestamp";
pub const HLC_TIMESTAMP_COLUMN: &str = "haex_timestamp";
#[derive(Debug)]
pub enum CrdtSetupError {

View File

@ -1,7 +1,5 @@
// src-tauri/src/database/core.rs
use std::collections::HashMap;
use crate::database::error::DatabaseError;
use crate::database::DbConnection;
use base64::{engine::general_purpose::STANDARD, Engine as _};
@ -14,6 +12,7 @@ use serde_json::Value as JsonValue;
use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject};
use sqlparser::dialect::SQLiteDialect;
use sqlparser::parser::Parser;
use std::collections::HashMap;
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, DatabaseError> {
@ -376,6 +375,7 @@ fn extract_tables_from_set_expr_recursive(set_expr: &SetExpr, tables: &mut Vec<S
| SetExpr::Table(_)
| SetExpr::Insert(_)
| SetExpr::Update(_)
| SetExpr::Merge(_)
| SetExpr::Delete(_) => {}
}
}

View File

@ -1,528 +0,0 @@
/// 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, 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>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
}
impl ExtensionInfoResponse {
pub fn from_extension(extension: &Extension) -> Self {
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
let allowed_origin = get_tauri_origin();
Self {
key_hash: calculate_key_hash(&extension.manifest.id),
name: extension.manifest.name.clone(),
full_id: format!(
"{}/{}@{}",
calculate_key_hash(&extension.manifest.id),
extension.manifest.name,
extension.manifest.version
),
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
}
}
}
fn get_tauri_origin() -> String {
#[cfg(target_os = "windows")]
{
"https://tauri.localhost".to_string()
}
#[cfg(target_os = "macos")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "linux")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "android")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "ios")]
{
"tauri://localhost".to_string()
}
}
// Dummy-Funktion für Key Hash (du implementierst das richtig mit SHA-256)
fn calculate_key_hash(id: &str) -> String {
// TODO: Implementiere SHA-256 Hash vom Public Key
// Für jetzt nur Placeholder
format!("{:0<20}", id.chars().take(20).collect::<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,
version: String,
}
#[derive(Debug)]
enum DataProcessingError {
HexDecoding(hex::FromHexError),
Utf8Conversion(std::string::FromUtf8Error),
JsonParsing(serde_json::Error),
}
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
impl fmt::Display for DataProcessingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
DataProcessingError::Utf8Conversion(e) => {
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
}
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
}
}
}
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
impl std::error::Error for DataProcessingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DataProcessingError::HexDecoding(e) => Some(e),
DataProcessingError::Utf8Conversion(e) => Some(e),
DataProcessingError::JsonParsing(e) => Some(e),
}
}
}
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
impl From<hex::FromHexError> for DataProcessingError {
fn from(err: hex::FromHexError) -> Self {
DataProcessingError::HexDecoding(err)
}
}
impl From<std::string::FromUtf8Error> for DataProcessingError {
fn from(err: std::string::FromUtf8Error) -> Self {
DataProcessingError::Utf8Conversion(err)
}
}
impl From<serde_json::Error> for DataProcessingError {
fn from(err: serde_json::Error) -> Self {
DataProcessingError::JsonParsing(err)
}
}
pub fn copy_directory(source: String, destination: String) -> Result<(), String> {
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
source, destination
);
let source_path = PathBuf::from(&source);
let destination_path = PathBuf::from(&destination);
if !source_path.exists() || !source_path.is_dir() {
return Err(format!(
"Quellverzeichnis '{}' nicht gefunden oder ist kein Verzeichnis.",
source
));
}
// Optionen für fs_extra::dir::copy
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
// Führe die Kopieroperation aus
match fs_extra::dir::copy(&source_path, &destination_path, &options) {
Ok(bytes_copied) => {
println!("Verzeichnis erfolgreich kopiert ({} bytes)", bytes_copied);
Ok(()) // Erfolg signalisieren
}
Err(e) => {
eprintln!("Fehler beim Kopieren des Verzeichnisses: {}", e);
Err(format!("Fehler beim Kopieren: {}", e.to_string())) // Fehler als String zurückgeben
}
}
}
pub fn resolve_secure_extension_asset_path<R: Runtime>(
app_handle: &AppHandle<R>,
extension_id: &str,
extension_version: &str,
requested_asset_path: &str,
) -> Result<PathBuf, String> {
// 1. Validiere die Extension ID
if extension_id.is_empty()
|| !extension_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(format!("Ungültige Extension ID: {}", extension_id));
}
if extension_version.is_empty()
|| !extension_version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return Err(format!(
"Ungültige Extension Version: {}",
extension_version
));
}
// 2. Bestimme das Basisverzeichnis für alle Erweiterungen (Resource Directory)
let base_extensions_dir = app_handle
.path()
.app_data_dir() // Korrekt für Ressourcen
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
.map_err(|e: TauriError| format!("Basis-Verzeichnis nicht gefunden: {}", e))?
.join("extensions");
// 3. Verzeichnis für die spezifische Erweiterung
let specific_extension_dir =
base_extensions_dir.join(format!("{}/{}", extension_id, extension_version));
// 4. Bereinige den angeforderten Asset-Pfad
let clean_relative_path = requested_asset_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
return Err("Leerer oder ungültiger Asset-Pfad".to_string());
}
// 5. Setze den finalen Pfad zusammen
let final_path = specific_extension_dir.join(clean_relative_path);
// 6. SICHERHEITSCHECK (wie vorher)
match final_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = specific_extension_dir.canonicalize().map_err(|e| {
format!(
"Kann Basis-Pfad '{}' nicht kanonisieren: {}",
specific_extension_dir.display(),
e
)
})?;
if canonical_path.starts_with(&canonical_base) {
Ok(canonical_path)
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err("Ungültiger oder nicht erlaubter Asset-Pfad (kanonisch)".to_string())
}
}
Err(_) => {
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
if final_path.starts_with(&specific_extension_dir) {
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err("Ungültiger oder nicht erlaubter Asset-Pfad (nicht kanonisiert)".to_string())
}
}
}
}
pub fn extension_protocol_handler<R: Runtime>(
context: &UriSchemeContext<'_, R>,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri();
println!("Protokoll Handler für: {}", uri_ref);
let host = uri_ref
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
let asset_to_load = if raw_asset_path.is_empty() {
"index.html"
} else {
&raw_asset_path
};
match process_hex_encoded_json(&host) {
Ok(info) => {
println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id);
println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path(
context.app_handle(),
&info.id,
&info.version,
&asset_to_load,
)?;
println!("absolute_secure_path: {}", absolute_secure_path.display());
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
let content_length = content.len();
println!(
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
absolute_secure_path.display(),
mime_type,
content_length
);
Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
// Optional, aber gut für Streaming-Fähigkeit:
.header("Accept-Ranges", "bytes")
.body(content)
.map_err(|e| e.into())
}
Err(e) => {
eprintln!(
"Fehler beim Lesen der Datei {}: {}",
absolute_secure_path.display(),
e
);
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
404
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
403
} else {
500
};
Response::builder()
.status(status_code)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
}
} else {
// Datei nicht gefunden oder es ist keine Datei
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()
);
Response::builder()
.status(404) // HTTP 404 Not Found
.body(Vec::new())
.map_err(|e| e.into())
}
}
Err(e) => {
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
Response::builder()
.status(500)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into())
}
}
}
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
// Schritt 1: Hex-String zu Bytes dekodieren
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
// Schritt 2: Bytes zu UTF-8-String konvertieren
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
// Schritt 3: JSON-String zu Struktur parsen
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
Ok(extension_info)
}

View File

@ -0,0 +1,311 @@
// src-tauri/src/extension/core/manager.rs
use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview};
use crate::extension::core::types::{copy_directory, Extension, ExtensionSource};
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::{ExtensionPermission, PermissionStatus};
use crate::AppState;
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
use tauri::{AppHandle, Manager, State};
use zip::ZipArchive;
#[derive(Debug, Clone)]
pub struct CachedPermission {
pub permissions: Vec<ExtensionPermission>,
pub cached_at: SystemTime,
pub ttl: Duration,
}
#[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 get_base_extension_dir(
&self,
app_handle: &AppHandle,
) -> Result<PathBuf, ExtensionError> {
let path = app_handle
.path()
.app_local_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions");
Ok(path)
}
pub fn get_extension_dir(
&self,
app_handle: &AppHandle,
extension_id: &str,
extension_version: &str,
) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(extension_id)
.join(extension_version);
Ok(specific_extension_dir)
}
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".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(),
});
}
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> {
let dev_extensions = self.dev_extensions.lock().unwrap();
if let Some(extension) = dev_extensions.get(extension_id) {
return Some(extension.clone());
}
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(),
})
}
pub async fn remove_extension_internal(
&self,
app_handle: &AppHandle,
extension_id: String,
extension_version: String,
state: &State<'_, AppState>,
) -> Result<(), ExtensionError> {
PermissionManager::delete_permissions(state, &extension_id).await?;
self.remove_extension(&extension_id)?;
let extension_dir =
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
if extension_dir.exists() {
std::fs::remove_dir_all(&extension_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
}
Ok(())
}
pub async fn preview_extension_internal(
&self,
source_path: String,
) -> Result<ExtensionPreview, ExtensionError> {
let source = PathBuf::from(&source_path);
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let is_valid_signature = ExtensionCrypto::verify_signature(
&manifest.public_key,
&content_hash,
&manifest.signature,
)
.is_ok();
let key_hash = manifest.calculate_key_hash()?;
let editable_permissions = manifest.to_editable_permissions();
std::fs::remove_dir_all(&temp).ok();
Ok(ExtensionPreview {
manifest,
is_valid_signature,
key_hash,
editable_permissions,
})
}
pub async fn install_extension_with_permissions_internal(
&self,
app_handle: AppHandle,
source_path: String,
custom_permissions: EditablePermissions,
state: &State<'_, AppState>,
) -> Result<String, ExtensionError> {
let source = PathBuf::from(&source_path);
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions")
.join(&full_extension_id)
.join(&manifest.version);
std::fs::create_dir_all(&extensions_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
copy_directory(
temp.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(),
)?;
std::fs::remove_dir_all(&temp).ok();
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
let granted_permissions: Vec<_> = permissions
.into_iter()
.filter(|p| p.status == PermissionStatus::Granted)
.collect();
PermissionManager::save_permissions(state, &full_extension_id, &granted_permissions)
.await?;
let extension = Extension {
id: full_extension_id.clone(),
name: manifest.name.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: manifest.version.clone(),
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
self.add_production_extension(extension)?;
Ok(full_extension_id)
}
}
// 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()
}
}

View File

@ -0,0 +1,250 @@
// src-tauri/src/extension/core/manifest.rs
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
};
use serde::{Deserialize, Serialize};
#[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 public_key: String,
pub signature: String,
pub permissions: ExtensionManifestPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
impl ExtensionManifest {
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
ExtensionCrypto::calculate_key_hash(&self.public_key)
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
}
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
let key_hash = self.calculate_key_hash()?;
Ok(format!("{}-{}", key_hash, self.id))
}
pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut permissions = Vec::new();
if let Some(db) = &self.permissions.database {
for resource in &db.read {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "read".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for resource in &db.write {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "write".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
if let Some(fs) = &self.permissions.filesystem {
for path in &fs.read {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "read".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for path in &fs.write {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "write".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
if let Some(http_list) = &self.permissions.http {
for domain in http_list {
permissions.push(EditablePermission {
resource_type: "http".to_string(),
action: "read".to_string(),
target: domain.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
if let Some(shell_list) = &self.permissions.shell {
for command in shell_list {
permissions.push(EditablePermission {
resource_type: "shell".to_string(),
action: "read".to_string(),
target: command.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
EditablePermissions { permissions }
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct ExtensionManifestPermissions {
#[serde(default)]
pub database: Option<DatabaseManifestPermissions>,
#[serde(default)]
pub filesystem: Option<FilesystemManifestPermissions>,
#[serde(default)]
pub http: Option<Vec<String>>,
#[serde(default)]
pub shell: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct DatabaseManifestPermissions {
#[serde(default)]
pub read: Vec<String>,
#[serde(default)]
pub write: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct FilesystemManifestPermissions {
#[serde(default)]
pub read: Vec<String>,
#[serde(default)]
pub write: Vec<String>,
}
// Editable Permissions für UI
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<EditablePermission>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermission {
pub resource_type: String,
pub action: String,
pub target: String,
pub constraints: Option<serde_json::Value>,
pub status: String,
}
impl EditablePermissions {
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
self.permissions
.iter()
.map(|p| ExtensionPermission {
id: uuid::Uuid::new_v4().to_string(),
extension_id: extension_id.to_string(),
resource_type: match p.resource_type.as_str() {
"fs" => ResourceType::Fs,
"http" => ResourceType::Http,
"db" => ResourceType::Db,
"shell" => ResourceType::Shell,
_ => ResourceType::Fs,
},
action: match p.action.as_str() {
"read" => Action::Read,
"write" => Action::Write,
_ => Action::Read,
},
target: p.target.clone(),
constraints: p
.constraints
.as_ref()
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
status: match p.status.as_str() {
"granted" => PermissionStatus::Granted,
"denied" => PermissionStatus::Denied,
"ask" => PermissionStatus::Ask,
_ => PermissionStatus::Denied,
},
haex_timestamp: None,
haex_tombstone: None,
})
.collect()
}
fn parse_constraints(
resource_type: &str,
json_value: &serde_json::Value,
) -> Option<PermissionConstraints> {
match resource_type {
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Database),
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Filesystem),
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Http),
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Shell),
_ => None,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
}
impl ExtensionInfoResponse {
pub fn from_extension(
extension: &crate::extension::core::types::Extension,
) -> Result<Self, ExtensionError> {
use crate::extension::core::types::get_tauri_origin;
let allowed_origin = get_tauri_origin();
let key_hash = extension.manifest.calculate_key_hash()?;
let full_id = extension.manifest.full_extension_id()?;
Ok(Self {
key_hash,
name: extension.manifest.name.clone(),
full_id,
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
})
}
}

View File

@ -0,0 +1,10 @@
// src-tauri/src/extension/core/mod.rs
pub mod manager;
pub mod manifest;
pub mod protocol;
pub mod types;
pub use manager::*;
pub use manifest::*;
pub use protocol::*;

View File

@ -0,0 +1,252 @@
// src-tauri/src/extension/core/protocol.rs
use crate::extension::error::ExtensionError;
use crate::AppState;
use mime;
use serde::Deserialize;
use std::fmt;
use std::fs;
use std::path::PathBuf;
use tauri::http::{Request, Response};
use tauri::{AppHandle, State};
#[derive(Deserialize, Debug)]
struct ExtensionInfo {
id: String,
version: String,
}
#[derive(Debug)]
enum DataProcessingError {
HexDecoding(hex::FromHexError),
Utf8Conversion(std::string::FromUtf8Error),
JsonParsing(serde_json::Error),
}
impl fmt::Display for DataProcessingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
DataProcessingError::Utf8Conversion(e) => {
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
}
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
}
}
}
impl std::error::Error for DataProcessingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DataProcessingError::HexDecoding(e) => Some(e),
DataProcessingError::Utf8Conversion(e) => Some(e),
DataProcessingError::JsonParsing(e) => Some(e),
}
}
}
impl From<hex::FromHexError> for DataProcessingError {
fn from(err: hex::FromHexError) -> Self {
DataProcessingError::HexDecoding(err)
}
}
impl From<std::string::FromUtf8Error> for DataProcessingError {
fn from(err: std::string::FromUtf8Error) -> Self {
DataProcessingError::Utf8Conversion(err)
}
}
impl From<serde_json::Error> for DataProcessingError {
fn from(err: serde_json::Error) -> Self {
DataProcessingError::JsonParsing(err)
}
}
pub fn resolve_secure_extension_asset_path(
app_handle: &AppHandle,
state: State<AppState>,
extension_id: &str,
extension_version: &str,
requested_asset_path: &str,
) -> Result<PathBuf, ExtensionError> {
if extension_id.is_empty()
|| !extension_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension ID: {}", extension_id),
});
}
if extension_version.is_empty()
|| !extension_version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension version: {}", extension_version),
});
}
let specific_extension_dir =
state
.extension_manager
.get_extension_dir(app_handle, extension_id, extension_version)?;
let clean_relative_path = requested_asset_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
return Err(ExtensionError::ValidationError {
reason: "Empty or invalid asset path".to_string(),
});
}
let final_path = specific_extension_dir.join(clean_relative_path);
match final_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = specific_extension_dir
.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
if canonical_path.starts_with(&canonical_base) {
Ok(canonical_path)
} else {
eprintln!(
"SECURITY WARNING: Path traversal attempt blocked: {}",
requested_asset_path
);
Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal attempt: {}", requested_asset_path),
})
}
}
Err(_) => {
if final_path.starts_with(&specific_extension_dir) {
Ok(final_path)
} else {
eprintln!(
"SECURITY WARNING: Invalid asset path: {}",
requested_asset_path
);
Err(ExtensionError::SecurityViolation {
reason: format!("Invalid asset path: {}", requested_asset_path),
})
}
}
}
}
pub fn extension_protocol_handler(
state: State<AppState>,
app_handle: &AppHandle,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri();
println!("Protokoll Handler für: {}", uri_ref);
let host = uri_ref
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
let asset_to_load = if raw_asset_path.is_empty() {
"index.html"
} else {
&raw_asset_path
};
match process_hex_encoded_json(&host) {
Ok(info) => {
println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id);
println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle,
state,
&info.id,
&info.version,
&asset_to_load,
)?;
println!("absolute_secure_path: {}", absolute_secure_path.display());
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
let content_length = content.len();
println!(
"Liefere {} ({}, {} bytes) ",
absolute_secure_path.display(),
mime_type,
content_length
);
Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Content-Length", content_length.to_string())
.header("Accept-Ranges", "bytes")
.body(content)
.map_err(|e| e.into())
}
Err(e) => {
eprintln!(
"Fehler beim Lesen der Datei {}: {}",
absolute_secure_path.display(),
e
);
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
404
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
403
} else {
500
};
Response::builder()
.status(status_code)
.body(Vec::new())
.map_err(|e| e.into())
}
}
} else {
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()
);
Response::builder()
.status(404)
.body(Vec::new())
.map_err(|e| e.into())
}
}
Err(e) => {
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
Response::builder()
.status(500)
.body(Vec::new())
.map_err(|e| e.into())
}
}
}
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
let bytes = hex::decode(hex_input)?;
let json_string = String::from_utf8(bytes)?;
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?;
Ok(extension_info)
}

View File

@ -0,0 +1,94 @@
// src-tauri/src/extension/core/types.rs
use crate::extension::core::manifest::ExtensionManifest;
use std::path::PathBuf;
use std::time::SystemTime;
/// 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,
}
pub fn get_tauri_origin() -> String {
#[cfg(target_os = "windows")]
{
"https://tauri.localhost".to_string()
}
#[cfg(target_os = "macos")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "linux")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "android")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "ios")]
{
"tauri://localhost".to_string()
}
}
pub fn copy_directory(
source: String,
destination: String,
) -> Result<(), crate::extension::error::ExtensionError> {
use crate::extension::error::ExtensionError;
use std::path::PathBuf;
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
source, destination
);
let source_path = PathBuf::from(&source);
let destination_path = PathBuf::from(&destination);
if !source_path.exists() || !source_path.is_dir() {
return Err(ExtensionError::Filesystem {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Source directory '{}' not found", source),
),
});
}
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true;
options.copy_inside = true;
options.buffer_size = 64000;
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
}
})?;
Ok(())
}

View File

@ -0,0 +1,973 @@
/// src-tauri/src/extension/core.rs
use crate::extension::crypto::ExtensionCrypto;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::{
Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints,
PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints,
};
use crate::AppState;
use mime;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use sha2::Sha256;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
use tauri::State;
use tauri::{
http::{Request, Response},
AppHandle, Manager, Runtime, UriSchemeContext,
};
use zip::ZipArchive;
#[derive(Serialize, Deserialize)]
pub struct ExtensionPreview {
pub manifest: ExtensionManifest,
pub is_valid_signature: bool,
pub key_hash: String,
pub editable_permissions: EditablePermissions,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<EditablePermission>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermission {
pub resource_type: String,
pub action: String,
pub target: String,
pub constraints: Option<serde_json::Value>,
pub status: String,
}
impl EditablePermissions {
/// Konvertiert EditablePermissions zu internen ExtensionPermissions
pub fn to_internal_permissions(&self, extension_id: &str) -> Vec<ExtensionPermission> {
self.permissions
.iter()
.map(|p| ExtensionPermission {
id: uuid::Uuid::new_v4().to_string(),
extension_id: extension_id.to_string(),
resource_type: match p.resource_type.as_str() {
"fs" => ResourceType::Fs,
"http" => ResourceType::Http,
"db" => ResourceType::Db,
"shell" => ResourceType::Shell,
_ => ResourceType::Fs, // Fallback
},
action: match p.action.as_str() {
"read" => Action::Read,
"write" => Action::Write,
_ => Action::Read, // Fallback
},
target: p.target.clone(),
constraints: p
.constraints
.as_ref()
.and_then(|c| Self::parse_constraints(&p.resource_type, c)),
status: match p.status.as_str() {
"granted" => PermissionStatus::Granted,
"denied" => PermissionStatus::Denied,
"ask" => PermissionStatus::Ask,
_ => PermissionStatus::Denied, // Fallback
},
haex_timestamp: None,
haex_tombstone: None,
})
.collect()
}
fn parse_constraints(
resource_type: &str,
json_value: &serde_json::Value,
) -> Option<PermissionConstraints> {
match resource_type {
"db" => serde_json::from_value::<DbConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Database),
"fs" => serde_json::from_value::<FsConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Filesystem),
"http" => serde_json::from_value::<HttpConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Http),
"shell" => serde_json::from_value::<ShellConstraints>(json_value.clone())
.ok()
.map(PermissionConstraints::Shell),
_ => None,
}
}
/// Filtert nur granted Permissions
pub fn filter_granted(&self) -> Vec<EditablePermission> {
self.permissions
.iter()
.filter(|p| p.status == "granted")
.cloned()
.collect()
}
}
#[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 public_key: String,
pub signature: String,
pub permissions: ExtensionManifestPermissions,
pub homepage: Option<String>,
pub description: Option<String>,
}
impl ExtensionManifest {
/// Berechnet den Key Hash für diese Extension
pub fn calculate_key_hash(&self) -> Result<String, ExtensionError> {
ExtensionCrypto::calculate_key_hash(&self.public_key)
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })
}
/// Generiert die vollständige Extension ID mit Key Hash Prefix
pub fn full_extension_id(&self) -> Result<String, ExtensionError> {
let key_hash = self.calculate_key_hash()?;
Ok(format!("{}-{}", key_hash, self.id))
}
pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut database = Vec::new();
let mut filesystem = Vec::new();
let mut http = Vec::new();
if let Some(db) = &self.permissions.database {
for resource in &db.read {
database.push(EditableDatabasePermission {
operation: "read".to_string(),
resource: resource.clone(),
status: PermissionStatus::Granted,
});
}
for resource in &db.write {
database.push(EditableDatabasePermission {
operation: "write".to_string(),
resource: resource.clone(),
status: PermissionStatus::Granted,
});
}
}
if let Some(fs) = &self.permissions.filesystem {
for path in &fs.read {
filesystem.push(EditableFilesystemPermission {
operation: "read".to_string(),
path: path.clone(),
status: PermissionStatus::Granted,
});
}
for path in &fs.write {
filesystem.push(EditableFilesystemPermission {
operation: "write".to_string(),
path: path.clone(),
status: PermissionStatus::Granted,
});
}
}
if let Some(http_list) = &self.permissions.http {
for domain in http_list {
http.push(EditableHttpPermission {
domain: domain.clone(),
status: PermissionStatus::Granted,
});
}
}
EditablePermissions {
database,
filesystem,
http,
}
}
}
impl ExtensionManifest {
/// Konvertiert Manifest zu EditablePermissions (neue Version)
pub fn to_editable_permissions(&self) -> EditablePermissions {
let mut permissions = Vec::new();
// Database Permissions
if let Some(db) = &self.permissions.database {
for resource in &db.read {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "read".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for resource in &db.write {
permissions.push(EditablePermission {
resource_type: "db".to_string(),
action: "write".to_string(),
target: resource.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// Filesystem Permissions
if let Some(fs) = &self.permissions.filesystem {
for path in &fs.read {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "read".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
for path in &fs.write {
permissions.push(EditablePermission {
resource_type: "fs".to_string(),
action: "write".to_string(),
target: path.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// HTTP Permissions
if let Some(http_list) = &self.permissions.http {
for domain in http_list {
permissions.push(EditablePermission {
resource_type: "http".to_string(),
action: "read".to_string(), // HTTP ist meist read
target: domain.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
// Shell Permissions
if let Some(shell_list) = &self.permissions.shell {
for command in shell_list {
permissions.push(EditablePermission {
resource_type: "shell".to_string(),
action: "read".to_string(), // Shell hat keine action mehr im Schema
target: command.clone(),
constraints: None,
status: "granted".to_string(),
});
}
}
EditablePermissions { permissions }
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionInfoResponse {
pub key_hash: String,
pub name: String,
pub full_id: String,
pub version: String,
pub display_name: Option<String>,
pub namespace: Option<String>,
pub allowed_origin: String,
}
impl ExtensionInfoResponse {
pub fn from_extension(extension: &Extension) -> Result<Self, ExtensionError> {
// Bestimme die allowed_origin basierend auf Tauri-Konfiguration
let allowed_origin = get_tauri_origin();
let key_hash = extension
.manifest
.calculate_key_hash()
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
let full_id = extension
.manifest
.full_extension_id()
.map_err(|e| ExtensionError::InvalidPublicKey { reason: e })?;
Ok(Self {
key_hash,
name: extension.manifest.name.clone(),
full_id,
version: extension.manifest.version.clone(),
display_name: Some(extension.manifest.name.clone()),
namespace: extension.manifest.author.clone(),
allowed_origin,
})
}
}
fn get_tauri_origin() -> String {
#[cfg(target_os = "windows")]
{
"https://tauri.localhost".to_string()
}
#[cfg(target_os = "macos")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "linux")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "android")]
{
"tauri://localhost".to_string()
}
#[cfg(target_os = "ios")]
{
"tauri://localhost".to_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 get_base_extension_dir(&self, app_handle: AppHandle) -> Result<PathBuf, ExtensionError> {
let path = app_handle
.path()
.app_local_data_dir() // Korrekt für Ressourcen
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions");
Ok(path)
}
pub fn get_extension_dir(
&self,
app_handle: AppHandle,
extension_id: &str,
extension_version: &str,
) -> Result<PathBuf, ExtensionError> {
let specific_extension_dir = self
.get_base_extension_dir(app_handle)?
.join(extension_id)
.join(extension_version);
Ok(specific_extension_dir)
}
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(),
})
}
pub async fn remove_extension_internal(
&self,
app_handle: AppHandle,
extension_id: String,
extension_version: String,
state: &State<'_, AppState>,
) -> Result<(), ExtensionError> {
// Permissions löschen (verwendet jetzt die neue Methode)
PermissionManager::delete_permissions(state, &extension_id).await?;
// Extension aus Manager entfernen
self.remove_extension(&extension_id)?;
let extension_dir =
self.get_extension_dir(app_handle, &extension_id, &extension_version)?;
// Dateien löschen
if extension_dir.exists() {
std::fs::remove_dir_all(&extension_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
}
Ok(())
}
pub async fn preview_extension_internal(
&self,
source_path: String,
) -> Result<ExtensionPreview, ExtensionError> {
let source = PathBuf::from(&source_path);
// ZIP in temp entpacken
let temp = std::env::temp_dir().join(format!("haexhub_preview_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// Manifest laden
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
let is_valid_signature = ExtensionCrypto::verify_signature(
&manifest.public_key,
&content_hash,
&manifest.signature,
)
.is_ok();
let key_hash = manifest.calculate_key_hash()?;
// Editable permissions erstellen
let editable_permissions = manifest.to_editable_permissions();
// Cleanup
std::fs::remove_dir_all(&temp).ok();
Ok(ExtensionPreview {
manifest,
is_valid_signature,
key_hash,
editable_permissions,
})
}
pub async fn install_extension_with_permissions_internal(
&self,
app_handle: AppHandle,
source_path: String,
custom_permissions: EditablePermissions,
state: &State<'_, AppState>,
) -> Result<String, ExtensionError> {
let source = PathBuf::from(&source_path);
// 1. ZIP entpacken
let temp = std::env::temp_dir().join(format!("haexhub_ext_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&temp).map_err(|e| ExtensionError::Filesystem { source: e })?;
let file = File::open(&source).map_err(|e| ExtensionError::Filesystem { source: e })?;
let mut archive =
ZipArchive::new(file).map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Invalid ZIP: {}", e),
})?;
archive
.extract(&temp)
.map_err(|e| ExtensionError::InstallationFailed {
reason: format!("Cannot extract ZIP: {}", e),
})?;
// 2. Manifest laden
let manifest_path = temp.join("manifest.json");
let manifest_content =
std::fs::read_to_string(&manifest_path).map_err(|e| ExtensionError::ManifestError {
reason: format!("Cannot read manifest: {}", e),
})?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)?;
// 3. Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&temp)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)
.map_err(|e| ExtensionError::SignatureVerificationFailed { reason: e })?;
// 4. Key Hash berechnen
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
// 5. Zielverzeichnis
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()),
})?
.join("extensions")
.join(&full_extension_id)
.join(&manifest.version);
std::fs::create_dir_all(&extensions_dir)
.map_err(|e| ExtensionError::Filesystem { source: e })?;
// 6. Dateien kopieren
copy_directory(
temp.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(),
)?;
// 7. Temp aufräumen
std::fs::remove_dir_all(&temp).ok();
// 8. Custom Permissions konvertieren und speichern
let permissions = custom_permissions.to_internal_permissions(&full_extension_id);
let granted_permissions = permissions.filter_granted();
PermissionManager::save_permissions(&state.db, &granted_permissions).await?;
// 9. Extension registrieren
let extension = Extension {
id: full_extension_id.clone(),
name: manifest.name.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: manifest.version.clone(),
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
state
.extension_manager
.add_production_extension(extension)?;
Ok(full_extension_id)
}
}
// 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,
version: String,
}
#[derive(Debug)]
enum DataProcessingError {
HexDecoding(hex::FromHexError),
Utf8Conversion(std::string::FromUtf8Error),
JsonParsing(serde_json::Error),
}
// Implementierung von Display für benutzerfreundliche Fehlermeldungen
impl fmt::Display for DataProcessingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataProcessingError::HexDecoding(e) => write!(f, "Hex-Dekodierungsfehler: {}", e),
DataProcessingError::Utf8Conversion(e) => {
write!(f, "UTF-8-Konvertierungsfehler: {}", e)
}
DataProcessingError::JsonParsing(e) => write!(f, "JSON-Parsing-Fehler: {}", e),
}
}
}
// Implementierung von std::error::Error (optional, aber gute Praxis für bibliotheksähnlichen Code)
impl std::error::Error for DataProcessingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DataProcessingError::HexDecoding(e) => Some(e),
DataProcessingError::Utf8Conversion(e) => Some(e),
DataProcessingError::JsonParsing(e) => Some(e),
}
}
}
// Implementierung von From-Traits für einfache Verwendung des '?'-Operators
impl From<hex::FromHexError> for DataProcessingError {
fn from(err: hex::FromHexError) -> Self {
DataProcessingError::HexDecoding(err)
}
}
impl From<std::string::FromUtf8Error> for DataProcessingError {
fn from(err: std::string::FromUtf8Error) -> Self {
DataProcessingError::Utf8Conversion(err)
}
}
impl From<serde_json::Error> for DataProcessingError {
fn from(err: serde_json::Error) -> Self {
DataProcessingError::JsonParsing(err)
}
}
pub fn copy_directory(source: String, destination: String) -> Result<(), ExtensionError> {
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
source, destination
);
let source_path = PathBuf::from(&source);
let destination_path = PathBuf::from(&destination);
if !source_path.exists() || !source_path.is_dir() {
return Err(ExtensionError::Filesystem {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Source directory '{}' not found", source),
),
});
}
// Optionen für fs_extra::dir::copy
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
// Führe die Kopieroperation aus
fs_extra::dir::copy(&source_path, &destination_path, &options).map_err(|e| {
ExtensionError::Filesystem {
source: std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
}
})?;
Ok(())
}
pub fn resolve_secure_extension_asset_path(
app_handle: AppHandle,
state: State<AppState>,
extension_id: &str,
extension_version: &str,
requested_asset_path: &str,
) -> Result<PathBuf, ExtensionError> {
// 1. Validiere die Extension ID
if extension_id.is_empty()
|| !extension_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension ID: {}", extension_id),
});
}
if extension_version.is_empty()
|| !extension_version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.')
{
return Err(ExtensionError::ValidationError {
reason: format!("Invalid extension version: {}", extension_version),
});
}
// 3. Verzeichnis für die spezifische Erweiterung
let specific_extension_dir =
state
.extension_manager
.get_extension_dir(app_handle, extension_id, extension_version)?;
// 4. Bereinige den angeforderten Asset-Pfad
let clean_relative_path = requested_asset_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
return Err(ExtensionError::ValidationError {
reason: "Empty or invalid asset path".to_string(),
});
}
// 5. Setze den finalen Pfad zusammen
let final_path = specific_extension_dir.join(clean_relative_path);
// 6. SICHERHEITSCHECK
match final_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = specific_extension_dir
.canonicalize()
.map_err(|e| ExtensionError::Filesystem { source: e })?;
if canonical_path.starts_with(&canonical_base) {
Ok(canonical_path)
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err(ExtensionError::SecurityViolation {
reason: format!("Path traversal attempt: {}", requested_asset_path),
})
}
}
Err(_) => {
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
if final_path.starts_with(&specific_extension_dir) {
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err(ExtensionError::SecurityViolation {
reason: format!("Invalid asset path: {}", requested_asset_path),
})
}
}
}
}
pub fn extension_protocol_handler<R: Runtime>(
state: State<AppState>,
app_handle: AppHandle,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri();
println!("Protokoll Handler für: {}", uri_ref);
let host = uri_ref
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let path_str = uri_ref.path();
let segments_iter = path_str.split('/').filter(|s| !s.is_empty());
let resource_segments: Vec<&str> = segments_iter.collect();
let raw_asset_path = resource_segments.join("/");
let asset_to_load = if raw_asset_path.is_empty() {
"index.html"
} else {
&raw_asset_path
};
match process_hex_encoded_json(&host) {
Ok(info) => {
println!("Daten erfolgreich verarbeitet:");
println!(" ID: {}", info.id);
println!(" Version: {}", info.version);
let absolute_secure_path = resolve_secure_extension_asset_path(
app_handle,
state,
&info.id,
&info.version,
&asset_to_load,
)?;
println!("absolute_secure_path: {}", absolute_secure_path.display());
if absolute_secure_path.exists() && absolute_secure_path.is_file() {
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
let content_length = content.len();
println!(
"Liefere {} ({}, {} bytes) ", // Content-Length zum Log hinzugefügt
absolute_secure_path.display(),
mime_type,
content_length
);
Response::builder()
.status(200)
.header("Content-Type", mime_type)
.header("Content-Length", content_length.to_string()) // <-- HIER HINZUGEFÜGT
// Optional, aber gut für Streaming-Fähigkeit:
.header("Accept-Ranges", "bytes")
.body(content)
.map_err(|e| e.into())
}
Err(e) => {
eprintln!(
"Fehler beim Lesen der Datei {}: {}",
absolute_secure_path.display(),
e
);
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
404
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
403
} else {
500
};
Response::builder()
.status(status_code)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
}
} else {
// Datei nicht gefunden oder es ist keine Datei
eprintln!(
"Asset nicht gefunden oder ist kein File: {}",
absolute_secure_path.display()
);
Response::builder()
.status(404) // HTTP 404 Not Found
.body(Vec::new())
.map_err(|e| e.into())
}
}
Err(e) => {
eprintln!("Fehler bei der Datenverarbeitung: {}", e);
Response::builder()
.status(500)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into())
}
}
}
fn process_hex_encoded_json(hex_input: &str) -> Result<ExtensionInfo, DataProcessingError> {
// Schritt 1: Hex-String zu Bytes dekodieren
let bytes = hex::decode(hex_input)?; // Konvertiert hex::FromHexError automatisch
// Schritt 2: Bytes zu UTF-8-String konvertieren
let json_string = String::from_utf8(bytes)?; // Konvertiert FromUtf8Error automatisch
// Schritt 3: JSON-String zu Struktur parsen
let extension_info: ExtensionInfo = serde_json::from_str(&json_string)?; // Konvertiert serde_json::Error automatisch
Ok(extension_info)
}

View File

@ -0,0 +1,74 @@
// src-tauri/src/extension/crypto.rs
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
pub struct ExtensionCrypto;
impl ExtensionCrypto {
/// Berechnet Hash vom Public Key (wie im SDK)
pub fn calculate_key_hash(public_key_hex: &str) -> Result<String, String> {
let public_key_bytes =
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key hex: {}", e))?;
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
.map_err(|e| format!("Invalid public key: {}", e))?;
let mut hasher = Sha256::new();
hasher.update(public_key.as_bytes());
let result = hasher.finalize();
// Ersten 20 Hex-Zeichen (10 Bytes) - wie im SDK
Ok(hex::encode(&result[..10]))
}
/// Verifiziert Extension-Signatur
pub fn verify_signature(
public_key_hex: &str,
content_hash_hex: &str,
signature_hex: &str,
) -> Result<(), String> {
let public_key_bytes =
hex::decode(public_key_hex).map_err(|e| format!("Invalid public key: {}", e))?;
let public_key = VerifyingKey::from_bytes(&public_key_bytes.try_into().unwrap())
.map_err(|e| format!("Invalid public key: {}", e))?;
let signature_bytes =
hex::decode(signature_hex).map_err(|e| format!("Invalid signature: {}", e))?;
let signature = Signature::from_bytes(&signature_bytes.try_into().unwrap());
let content_hash =
hex::decode(content_hash_hex).map_err(|e| format!("Invalid content hash: {}", e))?;
public_key
.verify(&content_hash, &signature)
.map_err(|e| format!("Signature verification failed: {}", e))
}
/// Berechnet Hash eines Verzeichnisses (für Verifikation)
pub fn hash_directory(dir: &std::path::Path) -> Result<String, String> {
use std::fs;
let mut hasher = Sha256::new();
let mut entries: Vec<_> = fs::read_dir(dir)
.map_err(|e| format!("Cannot read directory: {}", e))?
.filter_map(|e| e.ok())
.collect();
// Sortieren für deterministische Hashes
entries.sort_by_key(|e| e.path());
for entry in entries {
let path = entry.path();
if path.is_file() {
let content = fs::read(&path)
.map_err(|e| format!("Cannot read file {}: {}", path.display(), e))?;
hasher.update(&content);
} else if path.is_dir() {
let subdir_hash = Self::hash_directory(&path)?;
hasher.update(hex::decode(&subdir_hash).unwrap());
}
}
Ok(hex::encode(hasher.finalize()))
}
}

View File

@ -0,0 +1,153 @@
// src-tauri/src/extension/database/executor.rs (neu)
use crate::crdt::hlc::HlcService;
use crate::crdt::transformer::CrdtTransformer;
use crate::crdt::trigger;
use crate::database::core::{parse_sql_statements, ValueConverter};
use crate::database::error::DatabaseError;
use rusqlite::{params_from_iter, Transaction};
use serde_json::Value as JsonValue;
use sqlparser::ast::Statement;
use std::collections::HashSet;
/// SQL-Executor OHNE Berechtigungsprüfung - für interne Nutzung
pub struct SqlExecutor;
impl SqlExecutor {
/// Führt SQL aus (mit CRDT-Transformation) - OHNE Permission-Check
pub fn execute_internal(
tx: &Transaction,
hlc_service: &HlcService,
sql: &str,
params: &[JsonValue],
) -> Result<HashSet<String>, DatabaseError> {
// Parameter validation
let total_placeholders = sql.matches('?').count();
if total_placeholders != params.len() {
return Err(DatabaseError::ParameterMismatchError {
expected: total_placeholders,
provided: params.len(),
sql: sql.to_string(),
});
}
// SQL parsing
let mut ast_vec = parse_sql_statements(sql)?;
let transformer = CrdtTransformer::new();
// Generate HLC timestamp
let hlc_timestamp =
hlc_service
.new_timestamp_and_persist(tx)
.map_err(|e| DatabaseError::HlcError {
reason: e.to_string(),
})?;
// Transform statements
let mut modified_schema_tables = HashSet::new();
for statement in &mut ast_vec {
if let Some(table_name) =
transformer.transform_execute_statement(statement, &hlc_timestamp)?
{
modified_schema_tables.insert(table_name);
}
}
// Convert parameters
let sql_values = ValueConverter::convert_params(params)?;
// Execute statements
for statement in ast_vec {
let sql_str = statement.to_string();
tx.execute(&sql_str, params_from_iter(sql_values.iter()))
.map_err(|e| DatabaseError::ExecutionError {
sql: sql_str.clone(),
table: None,
reason: e.to_string(),
})?;
if let Statement::CreateTable(create_table_details) = statement {
let table_name_str = create_table_details.name.to_string();
trigger::setup_triggers_for_table(tx, &table_name_str, false)?;
}
}
Ok(modified_schema_tables)
}
/// Führt SELECT aus (mit CRDT-Transformation) - OHNE Permission-Check
pub fn select_internal(
conn: &rusqlite::Connection,
sql: &str,
params: &[JsonValue],
) -> Result<Vec<JsonValue>, DatabaseError> {
// Parameter validation
let total_placeholders = sql.matches('?').count();
if total_placeholders != params.len() {
return Err(DatabaseError::ParameterMismatchError {
expected: total_placeholders,
provided: params.len(),
sql: sql.to_string(),
});
}
let mut ast_vec = parse_sql_statements(sql)?;
if ast_vec.is_empty() {
return Ok(vec![]);
}
// Validate that all statements are queries
for stmt in &ast_vec {
if !matches!(stmt, Statement::Query(_)) {
return Err(DatabaseError::ExecutionError {
sql: sql.to_string(),
reason: "Only SELECT statements are allowed".to_string(),
table: None,
});
}
}
let sql_params = ValueConverter::convert_params(params)?;
let transformer = CrdtTransformer::new();
let last_statement = ast_vec.pop().unwrap();
let mut stmt_to_execute = last_statement;
transformer.transform_select_statement(&mut stmt_to_execute)?;
let transformed_sql = stmt_to_execute.to_string();
let mut prepared_stmt =
conn.prepare(&transformed_sql)
.map_err(|e| DatabaseError::ExecutionError {
sql: transformed_sql.clone(),
reason: e.to_string(),
table: None,
})?;
let column_names: Vec<String> = prepared_stmt
.column_names()
.into_iter()
.map(|s| s.to_string())
.collect();
let rows = prepared_stmt
.query_map(params_from_iter(sql_params.iter()), |row| {
crate::extension::database::row_to_json_value(row, &column_names)
})
.map_err(|e| DatabaseError::QueryError {
reason: e.to_string(),
})?;
let mut results = Vec::new();
for row_result in rows {
results.push(row_result.map_err(|e| DatabaseError::RowProcessingError {
reason: e.to_string(),
})?);
}
Ok(results)
}
}

View File

@ -1,14 +1,15 @@
// src-tauri/src/extension/database/mod.rs
pub mod permissions;
pub mod executor;
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::extension::error::ExtensionError;
use crate::extension::permissions::validator::SqlPermissionValidator;
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;
@ -116,7 +117,7 @@ pub async fn extension_sql_execute(
hlc_service: State<'_, HlcService>,
) -> Result<Vec<String>, ExtensionError> {
// Permission check
check_write_permission(&state.db, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;
@ -186,7 +187,7 @@ pub async fn extension_sql_select(
state: State<'_, AppState>,
) -> Result<Vec<JsonValue>, ExtensionError> {
// Permission check
check_read_permission(&state.db, &extension_id, sql).await?;
SqlPermissionValidator::validate_sql(&state, &extension_id, sql).await?;
// Parameter validation
validate_params(sql, &params)?;

View File

@ -1,278 +0,0 @@
// src-tauri/src/extension/database/permissions.rs
use crate::database::core::{
extract_table_names_from_sql, parse_single_statement, with_connection,
};
use crate::database::error::DatabaseError;
use crate::database::DbConnection;
use crate::extension::error::ExtensionError;
use serde::{Deserialize, Serialize};
use sqlparser::ast::{Statement, TableFactor, TableObject};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DbExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource: String,
pub operation: String,
}
/// Prüft Leseberechtigungen für eine Extension
pub async fn check_read_permission(
connection: &DbConnection,
extension_id: &str,
sql: &str,
) -> Result<(), ExtensionError> {
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
match statement {
Statement::Query(query) => {
let tables = extract_table_names_from_sql(&query.to_string())?;
check_table_permissions(connection, extension_id, &tables, "read").await
}
_ => Err(DatabaseError::UnsupportedStatement {
reason: "Only SELECT statements are allowed for read operations".to_string(),
sql: sql.to_string(),
}
.into()),
}
}
/// Prüft Schreibberechtigungen für eine Extension
pub async fn check_write_permission(
connection: &DbConnection,
extension_id: &str,
sql: &str,
) -> Result<(), ExtensionError> {
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
match statement {
Statement::Insert(insert) => {
let table_name = extract_table_name_from_insert(&insert)?;
check_single_table_permission(connection, extension_id, &table_name, "write").await
}
Statement::Update { table, .. } => {
let table_name = extract_table_name_from_table_factor(&table.relation)?;
check_single_table_permission(connection, extension_id, &table_name, "write").await
}
Statement::Delete(delete) => {
// DELETE wird durch CRDT-Transform zu UPDATE mit tombstone = 1
let table_name = extract_table_name_from_delete(&delete)?;
check_single_table_permission(connection, extension_id, &table_name, "write").await
}
Statement::CreateTable(create_table) => {
let table_name = create_table.name.to_string();
check_single_table_permission(connection, extension_id, &table_name, "create").await
}
Statement::AlterTable { name, .. } => {
let table_name = name.to_string();
check_single_table_permission(connection, extension_id, &table_name, "alter").await
}
Statement::Drop { names, .. } => {
// Für DROP können mehrere Tabellen angegeben sein
let table_names: Vec<String> = names.iter().map(|name| name.to_string()).collect();
check_table_permissions(connection, extension_id, &table_names, "drop").await
}
_ => 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, ExtensionError> {
match &insert.table {
TableObject::TableName(name) => Ok(name.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, ExtensionError> {
match table_factor {
TableFactor::Table { name, .. } => Ok(name.to_string()),
_ => 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, ExtensionError> {
use sqlparser::ast::FromTable;
let table_name = match &delete.from {
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
if !tables.is_empty() {
extract_table_name_from_table_factor(&tables[0].relation)?
} else if !delete.tables.is_empty() {
delete.tables[0].to_string()
} else {
return Err(DatabaseError::NoTableError {
sql: delete.to_string(),
}
.into());
}
}
};
Ok(table_name)
}
/// Prüft Berechtigung für eine einzelne Tabelle
async fn check_single_table_permission(
connection: &DbConnection,
extension_id: &str,
table_name: &str,
operation: &str,
) -> Result<(), ExtensionError> {
check_table_permissions(
connection,
extension_id,
&[table_name.to_string()],
operation,
)
.await
}
/// Prüft Berechtigungen für mehrere Tabellen
async fn check_table_permissions(
connection: &DbConnection,
extension_id: &str,
table_names: &[String],
operation: &str,
) -> Result<(), ExtensionError> {
let permissions =
get_extension_permissions(connection, extension_id, "database", operation).await?;
for table_name in table_names {
let has_permission = permissions
.iter()
.any(|perm| perm.resource.contains(table_name));
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
operation,
&format!("table '{}'", table_name),
));
}
}
Ok(())
}
/// Ruft die Berechtigungen einer Extension aus der Datenbank ab
pub async fn get_extension_permissions(
connection: &DbConnection,
extension_id: &str,
resource: &str,
operation: &str,
) -> Result<Vec<DbExtensionPermission>, DatabaseError> {
with_connection(connection, |conn| {
let mut stmt = conn
.prepare(
"SELECT id, extension_id, resource, operation, path
FROM haex_vault_extension_permissions
WHERE extension_id = ?1 AND resource = ?2 AND operation = ?3",
)
.map_err(|e| DatabaseError::PrepareError {
reason: e.to_string(),
})?;
let rows = stmt
.query_map([extension_id, resource, operation], |row| {
Ok(DbExtensionPermission {
id: row.get(0)?,
extension_id: row.get(1)?,
resource: row.get(2)?,
operation: row.get(3)?,
})
})
.map_err(|e| DatabaseError::QueryError {
reason: e.to_string(),
})?;
let mut permissions = Vec::new();
for row_result in rows {
let permission = row_result.map_err(|e| DatabaseError::DatabaseError {
reason: e.to_string(),
})?;
permissions.push(permission);
}
Ok(permissions)
})
}
#[cfg(test)]
mod tests {
use crate::extension::error::ExtensionError;
use super::*;
#[test]
fn test_parse_single_statement() {
let sql = "SELECT * FROM users";
let result = parse_single_statement(sql);
assert!(result.is_ok());
assert!(matches!(result.unwrap(), Statement::Query(_)));
}
#[test]
fn test_parse_invalid_sql() {
let sql = "INVALID SQL";
let result = parse_single_statement(sql);
// 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 {
Err(DatabaseError::ParseError { .. }) => {
// Test erfolgreich - wir haben einen ParseError erhalten
}
Err(other) => {
// Andere DatabaseError-Varianten sind auch akzeptabel für ungültiges SQL
println!("Received other DatabaseError: {:?}", other);
}
Ok(_) => panic!("Expected error for invalid SQL"),
}
}
/* #[test]
fn test_permission_error_access_denied() {
let error = ExtensionError::access_denied("ext1", "read", "table1", "not allowed");
match error {
ExtensionError::AccessDenied {
extension_id,
operation,
resource,
reason,
} => {
assert_eq!(extension_id, "ext1");
assert_eq!(operation, "read");
assert_eq!(resource, "table1");
assert_eq!(reason, "not allowed");
}
_ => panic!("Expected AccessDenied error"),
}
} */
}

View File

@ -1,9 +1,36 @@
/// src-tauri/src/extension/error.rs
// src-tauri/src/extension/error.rs
use thiserror::Error;
use crate::database::error::DatabaseError;
/// Comprehensive error type for extension operations
/// Error codes for frontend handling
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtensionErrorCode {
SecurityViolation = 1000,
NotFound = 1001,
PermissionDenied = 1002,
Database = 2000,
Filesystem = 2001,
Http = 2002,
Shell = 2003,
Manifest = 3000,
Validation = 3001,
InvalidPublicKey = 4000,
InvalidSignature = 4001,
SignatureVerificationFailed = 4002,
CalculateHash = 4003,
Installation = 5000,
}
impl serde::Serialize for ExtensionErrorCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u16(*self as u16)
}
}
#[derive(Error, Debug)]
pub enum ExtensionError {
#[error("Security violation: {reason}")]
@ -29,15 +56,10 @@ pub enum ExtensionError {
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>>,
},
Http { reason: String },
#[error("Shell command failed: {reason}")]
Shell {
@ -45,29 +67,51 @@ pub enum ExtensionError {
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("Invalid Public Key: {reason}")]
InvalidPublicKey { reason: String },
#[error("Serialization error: {reason}")]
SerializationError { reason: String },
#[error("Invalid Signature: {reason}")]
InvalidSignature { reason: String },
#[error("Configuration error: {reason}")]
ConfigError { reason: String },
#[error("Error during hash calculation: {reason}")]
CalculateHashError { reason: String },
#[error("Signature verification failed: {reason}")]
SignatureVerificationFailed { reason: String },
#[error("Extension installation failed: {reason}")]
InstallationFailed { reason: String },
}
impl ExtensionError {
/// Convenience constructor for permission denied errors
/// Get error code for this error
pub fn code(&self) -> ExtensionErrorCode {
match self {
ExtensionError::SecurityViolation { .. } => ExtensionErrorCode::SecurityViolation,
ExtensionError::NotFound { .. } => ExtensionErrorCode::NotFound,
ExtensionError::PermissionDenied { .. } => ExtensionErrorCode::PermissionDenied,
ExtensionError::Database { .. } => ExtensionErrorCode::Database,
ExtensionError::Filesystem { .. } => ExtensionErrorCode::Filesystem,
ExtensionError::Http { .. } => ExtensionErrorCode::Http,
ExtensionError::Shell { .. } => ExtensionErrorCode::Shell,
ExtensionError::ManifestError { .. } => ExtensionErrorCode::Manifest,
ExtensionError::ValidationError { .. } => ExtensionErrorCode::Validation,
ExtensionError::InvalidPublicKey { .. } => ExtensionErrorCode::InvalidPublicKey,
ExtensionError::InvalidSignature { .. } => ExtensionErrorCode::InvalidSignature,
ExtensionError::SignatureVerificationFailed { .. } => {
ExtensionErrorCode::SignatureVerificationFailed
}
ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation,
ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash,
}
}
pub fn permission_denied(extension_id: &str, operation: &str, resource: &str) -> Self {
Self::PermissionDenied {
extension_id: extension_id.to_string(),
@ -76,34 +120,6 @@ impl ExtensionError {
}
}
/// 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,
@ -111,11 +127,9 @@ impl ExtensionError {
)
}
/// 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,
}
}
@ -128,29 +142,12 @@ impl serde::Serialize for ExtensionError {
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ExtensionError", 3)?;
let mut state = serializer.serialize_struct("ExtensionError", 4)?;
// 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("code", &self.code())?;
state.serialize_field("type", &format!("{:?}", self))?;
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 {
@ -161,54 +158,16 @@ impl serde::Serialize for ExtensionError {
}
}
// For Tauri command serialization
impl From<ExtensionError> for String {
fn from(error: ExtensionError) -> Self {
serde_json::to_string(&error).unwrap_or_else(|_| error.to_string())
}
}
impl From<serde_json::Error> for ExtensionError {
fn from(err: serde_json::Error) -> Self {
ExtensionError::SerializationError {
ExtensionError::ManifestError {
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

@ -1,19 +1,184 @@
use crate::extension::core::{ExtensionInfoResponse, ExtensionManager};
use tauri::State;
/// src-tauri/src/extension/mod.rs
use crate::{
extension::{
core::{EditablePermissions, ExtensionInfoResponse, ExtensionPreview},
error::ExtensionError,
},
AppState,
};
use tauri::{AppHandle, State};
pub mod core;
pub mod crypto;
pub mod database;
pub mod error;
pub mod filesystem;
pub mod permission_manager;
pub mod permissions;
#[tauri::command]
pub fn get_extension_info(
extension_id: String,
extension_manager: State<ExtensionManager>,
state: State<AppState>,
) -> Result<ExtensionInfoResponse, String> {
let extension = extension_manager
let extension = state
.extension_manager
.get_extension(&extension_id)
.ok_or_else(|| format!("Extension nicht gefunden: {}", extension_id))?;
Ok(ExtensionInfoResponse::from_extension(&extension))
ExtensionInfoResponse::from_extension(&extension).map_err(|e| format!("{:?}", e))
}
#[tauri::command]
pub fn get_all_extensions(state: State<AppState>) -> Result<Vec<ExtensionInfoResponse>, String> {
let mut extensions = Vec::new();
// Production Extensions
{
let prod_exts = state
.extension_manager
.production_extensions
.lock()
.unwrap();
for ext in prod_exts.values() {
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
}
}
// Dev Extensions
{
let dev_exts = state.extension_manager.dev_extensions.lock().unwrap();
for ext in dev_exts.values() {
extensions.push(ExtensionInfoResponse::from_extension(ext)?);
}
}
Ok(extensions)
}
#[tauri::command]
pub async fn preview_extension(
state: State<'_, AppState>,
source_path: String,
) -> Result<ExtensionPreview, ExtensionError> {
state
.extension_manager
.preview_extension_internal(source_path)
.await
}
#[tauri::command]
pub async fn install_extension_with_permissions(
app_handle: AppHandle,
source_path: String,
custom_permissions: EditablePermissions,
state: State<'_, AppState>,
) -> Result<String, ExtensionError> {
state
.extension_manager
.install_extension_with_permissions_internal(
app_handle,
source_path,
custom_permissions,
&state,
)
.await
}
/* #[tauri::command]
pub async fn install_extension(
app_handle: AppHandle,
source_path: String,
state: State<'_, AppState>,
) -> Result<String, String> {
let source = PathBuf::from(&source_path);
// Manifest laden
let manifest_path = source.join("manifest.json");
let manifest_content = std::fs::read_to_string(&manifest_path)
.map_err(|e| format!("Manifest konnte nicht gelesen werden: {}", e))?;
let manifest: ExtensionManifest = serde_json::from_str(&manifest_content)
.map_err(|e| format!("Manifest ist ungültig: {}", e))?;
// Signatur verifizieren
let content_hash = ExtensionCrypto::hash_directory(&source)?;
ExtensionCrypto::verify_signature(&manifest.public_key, &content_hash, &manifest.signature)?;
// Key Hash berechnen
let key_hash = manifest.calculate_key_hash()?;
let full_extension_id = format!("{}-{}", key_hash, manifest.id);
// Zielverzeichnis mit Key Hash Prefix
let extensions_dir = app_handle
.path()
.app_data_dir()
.map_err(|e| format!("App-Datenverzeichnis nicht gefunden: {}", e))?
.join("extensions")
.join(&full_extension_id) // <- z.B. "a3f5b9c2d1e8f4-haex-pass"
.join(&manifest.version);
// Extension-Dateien kopieren
std::fs::create_dir_all(&extensions_dir)
.map_err(|e| format!("Verzeichnis konnte nicht erstellt werden: {}", e))?;
let source_to_copy = if source.join("dist").exists() {
source.join("dist") // Kopiere aus dist/
} else {
source.clone() // Kopiere direkt
};
copy_directory(
source_to_copy.to_string_lossy().to_string(),
extensions_dir.to_string_lossy().to_string(),
)?;
// Permissions speichern
let permissions = manifest.to_internal_permissions();
PermissionManager::save_permissions(&state.db, &permissions)
.await
.map_err(|e| format!("Fehler beim Speichern der Permissions: {:?}", e))?;
// Extension registrieren
let extension = Extension {
id: full_extension_id.clone(),
name: manifest.name.clone(),
source: ExtensionSource::Production {
path: extensions_dir.clone(),
version: manifest.version.clone(),
},
manifest: manifest.clone(),
enabled: true,
last_accessed: SystemTime::now(),
};
state
.extension_manager
.add_production_extension(extension)
.map_err(|e| format!("Extension konnte nicht hinzugefügt werden: {:?}", e))?;
Ok(full_extension_id)
}
*/
#[tauri::command]
pub async fn remove_extension(
app_handle: AppHandle,
extension_id: String,
extension_version: String,
state: State<'_, AppState>,
) -> Result<(), ExtensionError> {
state
.extension_manager
.remove_extension_internal(&app_handle, extension_id, extension_version, &state)
.await
}
#[tauri::command]
pub fn is_extension_installed(
extension_id: String,
extension_version: String,
state: State<'_, AppState>,
) -> Result<bool, String> {
if let Some(ext) = state.extension_manager.get_extension(&extension_id) {
Ok(ext.manifest.version == extension_version)
} else {
Ok(false)
}
}

View File

@ -1,297 +0,0 @@
/// 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.resource.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

@ -0,0 +1,650 @@
use crate::AppState;
use crate::database::core::with_connection;
use crate::database::error::DatabaseError;
use crate::extension::database::executor::SqlExecutor;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::types::{Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints};
use serde_json;
use serde_json::json;
use std::path::Path;
use tauri::State;
use url::Url;
pub struct PermissionManager;
impl PermissionManager {
/// Speichert alle Permissions einer Extension
pub async fn save_permissions(
app_state: &State<'_, AppState>,
extension_id: &str,
permissions: &[ExtensionPermission],
) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state
.hlc
.lock()
.map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
for perm in permissions {
let resource_type_str = format!("{:?}", perm.resource_type).to_lowercase();
let action_str = format!("{:?}", perm.action).to_lowercase();
let constraints_json = perm
.constraints
.as_ref()
.map(|c| serde_json::to_string(c).ok())
.flatten();
let sql = "INSERT INTO haex_extension_permissions
(id, extension_id, resource_type, action, target, constraints, status)
VALUES (?, ?, ?, ?, ?, ?, ?)";
let params = vec![
json!(perm.id),
json!(extension_id),
json!(resource_type_str),
json!(action_str),
json!(perm.target),
json!(constraints_json),
json!(perm.status.as_str()),
];
SqlExecutor::execute_internal(&tx, &hlc_service, sql, &params)?;
}
tx.commit().map_err(DatabaseError::from)?;
Ok(())
})
.map_err(ExtensionError::from)
}
/// Aktualisiert eine Permission
pub async fn update_permission(
app_state: &State<'_, AppState>,
permission: &ExtensionPermission,
) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state
.hlc
.lock()
.map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
let resource_type_str = format!("{:?}", permission.resource_type).to_lowercase();
let action_str = format!("{:?}", permission.action).to_lowercase();
let constraints_json = permission
.constraints
.as_ref()
.map(|c| serde_json::to_string(c).ok())
.flatten();
let sql = "UPDATE haex_extension_permissions
SET resource_type = ?, action = ?, target = ?, constraints = ?, status = ?
WHERE id = ?";
let params = vec![
json!(resource_type_str),
json!(action_str),
json!(permission.target),
json!(constraints_json),
json!(permission.status.as_str()),
json!(permission.id),
];
SqlExecutor::execute_internal(&tx, &hlc_service, sql, &params)?;
tx.commit().map_err(DatabaseError::from)?;
Ok(())
})
.map_err(ExtensionError::from)
}
/// Ändert den Status einer Permission
pub async fn update_permission_status(
app_state: &State<'_, AppState>,
permission_id: &str,
new_status: PermissionStatus,
) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state
.hlc
.lock()
.map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
let sql = "UPDATE haex_extension_permissions
SET status = ?
WHERE id = ?";
let params = vec![json!(new_status.as_str()), json!(permission_id)];
SqlExecutor::execute_internal(&tx, &hlc_service, sql, &params)?;
tx.commit().map_err(DatabaseError::from)?;
Ok(())
})
.map_err(ExtensionError::from)
}
/// Löscht alle Permissions einer Extension
pub async fn delete_permission(
app_state: &State<'_, AppState>,
permission_id: &str,
) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state.hlc.lock()
.map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
let sql = "DELETE FROM haex_extension_permissions WHERE id = ?";
let params = vec![json!(permission_id)];
SqlExecutor::execute_internal(&tx, &hlc_service, sql, &params)?;
tx.commit().map_err(DatabaseError::from)?;
Ok(())
}).map_err(ExtensionError::from)
}
/// Löscht alle Permissions einer Extension (Soft-Delete)
pub async fn delete_permissions(
app_state: &State<'_, AppState>,
extension_id: &str,
) -> Result<(), ExtensionError> {
with_connection(&app_state.db, |conn| {
let tx = conn.transaction().map_err(DatabaseError::from)?;
let hlc_service = app_state.hlc.lock()
.map_err(|_| DatabaseError::MutexPoisoned {
reason: "Failed to lock HLC service".to_string(),
})?;
// Echtes DELETE - wird vom CrdtTransformer zu UPDATE umgewandelt
let sql = "DELETE FROM haex_extension_permissions WHERE extension_id = ?";
let params = vec![json!(extension_id)];
SqlExecutor::execute_internal(&tx, &hlc_service, sql, &params)?;
tx.commit().map_err(DatabaseError::from)?;
Ok(())
}).map_err(ExtensionError::from)
}
/// Lädt alle Permissions einer Extension
pub async fn get_permissions(
app_state: &State<'_, AppState>,
extension_id: &str,
) -> Result<Vec<ExtensionPermission>, ExtensionError> {
with_connection(&app_state.db, |conn| {
let sql = "SELECT id, extension_id, resource_type, action, target, constraints, status, haex_timestamp, haex_tombstone
FROM haex_extension_permissions
WHERE extension_id = ?";
let params = vec![json!(extension_id)];
// SELECT nutzt select_internal
let results = SqlExecutor::select_internal(conn, sql, &params)?;
// Parse JSON results zu ExtensionPermission
let permissions = results
.into_iter()
.map(|row| Self::parse_permission_from_json(row))
.collect::<Result<Vec<_>, _>>()?;
Ok(permissions)
}).map_err(ExtensionError::from)
}
// Helper für JSON -> ExtensionPermission Konvertierung
fn parse_permission_from_json(json: serde_json::Value) -> Result<ExtensionPermission, DatabaseError> {
let obj = json.as_object().ok_or_else(|| DatabaseError::SerializationError {
reason: "Expected JSON object".to_string(),
})?;
let resource_type = Self::parse_resource_type(
obj.get("resource_type")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing resource_type".to_string(),
})?
)?;
let action = Self::parse_action(
obj.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing action".to_string(),
})?
)?;
let status = PermissionStatus::from_str(
obj.get("status")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing status".to_string(),
})?
)?; // Jetzt funktioniert das ?
let constraints = obj.get("constraints")
.and_then(|v| v.as_str())
.map(|json_str| Self::parse_constraints(&resource_type, json_str))
.transpose()?;
Ok(ExtensionPermission {
id: obj.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing id".to_string(),
})?
.to_string(),
extension_id: obj.get("extension_id")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing extension_id".to_string(),
})?
.to_string(),
resource_type,
action,
target: obj.get("target")
.and_then(|v| v.as_str())
.ok_or_else(|| DatabaseError::SerializationError {
reason: "Missing target".to_string(),
})?
.to_string(),
constraints,
status,
haex_timestamp: obj.get("haex_timestamp")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
haex_tombstone: obj.get("haex_tombstone")
.and_then(|v| v.as_i64())
.map(|i| i == 1),
})
}
/// Prüft Datenbankberechtigungen
pub async fn check_database_permission(
app_state: &State<'_, AppState>,
extension_id: &str,
action: Action,
table_name: &str,
) -> Result<(), ExtensionError> {
let permissions = Self::get_permissions(app_state, extension_id).await?;
let has_permission = permissions
.iter()
.filter(|perm| perm.status == PermissionStatus::Granted) // NUR granted!
.filter(|perm| perm.resource_type == ResourceType::Db)
.filter(|perm| perm.action == action) // action ist nicht mehr Option
.any(|perm| {
if perm.target != "*" && perm.target != table_name {
return false;
}
true
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
&format!("{:?}", action),
&format!("database table '{}'", table_name),
));
}
Ok(())
}
/// Prüft Dateisystem-Berechtigungen
pub async fn check_filesystem_permission(
app_state: &State<'_, AppState>,
extension_id: &str,
action: Action,
file_path: &Path,
) -> Result<(), ExtensionError> {
let permissions = Self::get_permissions(app_state, extension_id).await?;
let file_path_str = file_path.to_string_lossy();
let has_permission = permissions
.iter()
.filter(|perm| perm.status == PermissionStatus::Granted)
.filter(|perm| perm.resource_type == ResourceType::Fs)
.filter(|perm| perm.action == action)
.any(|perm| {
if !Self::matches_path_pattern(&perm.target, &file_path_str) {
return false;
}
if let Some(PermissionConstraints::Filesystem(constraints)) = &perm.constraints {
if let Some(allowed_ext) = &constraints.allowed_extensions {
if let Some(ext) = file_path.extension() {
let ext_str = format!(".{}", ext.to_string_lossy());
if !allowed_ext.contains(&ext_str) {
return false;
}
} else {
return false;
}
}
}
true
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
&format!("{:?}", action),
&format!("filesystem path '{}'", file_path_str),
));
}
Ok(())
}
/// Prüft HTTP-Berechtigungen
pub async fn check_http_permission(
app_state: &State<'_, AppState>,
extension_id: &str,
method: &str,
url: &str,
) -> Result<(), ExtensionError> {
let permissions = Self::get_permissions(app_state, extension_id).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 has_permission = permissions
.iter()
.filter(|perm| perm.status == PermissionStatus::Granted)
.filter(|perm| perm.resource_type == ResourceType::Http)
.any(|perm| {
let domain_matches = perm.target == "*"
|| perm.target == domain
|| domain.ends_with(&format!(".{}", perm.target));
if !domain_matches {
return false;
}
if let Some(PermissionConstraints::Http(constraints)) = &perm.constraints {
if let Some(methods) = &constraints.methods {
if !methods.iter().any(|m| m.eq_ignore_ascii_case(method)) {
return false;
}
}
}
true
});
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(
app_state: &State<'_, AppState>,
extension_id: &str,
command: &str,
args: &[String],
) -> Result<(), ExtensionError> {
let permissions = Self::get_permissions(app_state, extension_id).await?;
let has_permission = permissions
.iter()
.filter(|perm| perm.status == PermissionStatus::Granted)
.filter(|perm| perm.resource_type == ResourceType::Shell)
.any(|perm| {
if perm.target != command && perm.target != "*" {
return false;
}
if let Some(PermissionConstraints::Shell(constraints)) = &perm.constraints {
if let Some(allowed_subcommands) = &constraints.allowed_subcommands {
if !args.is_empty() {
if !allowed_subcommands.contains(&args[0])
&& !allowed_subcommands.contains(&"*".to_string())
{
return false;
}
}
}
if let Some(forbidden) = &constraints.forbidden_args {
if args.iter().any(|arg| forbidden.contains(arg)) {
return false;
}
}
if let Some(allowed_flags) = &constraints.allowed_flags {
let user_flags: Vec<_> =
args.iter().filter(|arg| arg.starts_with('-')).collect();
for flag in user_flags {
if !allowed_flags.contains(flag)
&& !allowed_flags.contains(&"*".to_string())
{
return false;
}
}
}
}
true
});
if !has_permission {
return Err(ExtensionError::permission_denied(
extension_id,
"execute",
&format!("shell command '{}' with args {:?}", command, args),
));
}
Ok(())
}
// Helper-Methoden - müssen DatabaseError statt ExtensionError zurückgeben
fn parse_resource_type(s: &str) -> Result<ResourceType, DatabaseError> {
match s {
"fs" => Ok(ResourceType::Fs),
"http" => Ok(ResourceType::Http),
"db" => Ok(ResourceType::Db),
"shell" => Ok(ResourceType::Shell),
_ => Err(DatabaseError::SerializationError {
reason: format!("Unknown resource type: {}", s),
}),
}
}
fn parse_action(s: &str) -> Result<Action, DatabaseError> {
match s {
"read" => Ok(Action::Read),
"write" => Ok(Action::Write),
_ => Err(DatabaseError::SerializationError {
reason: format!("Unknown action: {}", s),
}),
}
}
fn parse_constraints(
resource_type: &ResourceType,
json: &str,
) -> Result<PermissionConstraints, DatabaseError> {
match resource_type {
ResourceType::Db => {
let constraints: DbConstraints = serde_json::from_str(json)
.map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse DB constraints: {}", e),
})?;
Ok(PermissionConstraints::Database(constraints))
}
ResourceType::Fs => {
let constraints: FsConstraints = serde_json::from_str(json)
.map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse FS constraints: {}", e),
})?;
Ok(PermissionConstraints::Filesystem(constraints))
}
ResourceType::Http => {
let constraints: HttpConstraints = serde_json::from_str(json)
.map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse HTTP constraints: {}", e),
})?;
Ok(PermissionConstraints::Http(constraints))
}
ResourceType::Shell => {
let constraints: ShellConstraints = serde_json::from_str(json)
.map_err(|e| DatabaseError::SerializationError {
reason: format!("Failed to parse Shell constraints: {}", e),
})?;
Ok(PermissionConstraints::Shell(constraints))
}
}
}
fn matches_path_pattern(pattern: &str, path: &str) -> bool {
if pattern.ends_with("/*") {
let prefix = &pattern[..pattern.len() - 2];
return path.starts_with(prefix);
}
if pattern.starts_with("*.") {
let suffix = &pattern[1..];
return path.ends_with(suffix);
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
return path.starts_with(parts[0]) && path.ends_with(parts[1]);
}
}
pattern == path || pattern == "*"
}
}
// Convenience-Funktionen für die verschiedenen Subsysteme
impl PermissionManager {
// Convenience-Methoden
pub async fn can_read_file(
app_state: &State<'_, AppState>,
extension_id: &str,
file_path: &Path,
) -> Result<(), ExtensionError> {
Self::check_filesystem_permission(app_state, extension_id, Action::Read, file_path).await
}
pub async fn can_write_file(
app_state: &State<'_, AppState>,
extension_id: &str,
file_path: &Path,
) -> Result<(), ExtensionError> {
Self::check_filesystem_permission(app_state, extension_id, Action::Write, file_path).await
}
pub async fn can_read_table(
app_state: &State<'_, AppState>,
extension_id: &str,
table_name: &str,
) -> Result<(), ExtensionError> {
Self::check_database_permission(app_state, extension_id, Action::Read, table_name).await
}
pub async fn can_write_table(
app_state: &State<'_, AppState>,
extension_id: &str,
table_name: &str,
) -> Result<(), ExtensionError> {
Self::check_database_permission(app_state, extension_id, Action::Write, table_name).await
}
pub async fn can_http_get(
app_state: &State<'_, AppState>,
extension_id: &str,
url: &str,
) -> Result<(), ExtensionError> {
Self::check_http_permission(app_state, extension_id, "GET", url).await
}
pub async fn can_http_post(
app_state: &State<'_, AppState>,
extension_id: &str,
url: &str,
) -> Result<(), ExtensionError> {
Self::check_http_permission(app_state, extension_id, "POST", url).await
}
pub async fn can_execute_command(
app_state: &State<'_, AppState>,
extension_id: &str,
command: &str,
args: &[String],
) -> Result<(), ExtensionError> {
Self::check_shell_permission(app_state, extension_id, command, args).await
}
pub async fn grant_permission(
app_state: &State<'_, AppState>,
permission_id: &str,
) -> Result<(), ExtensionError> {
Self::update_permission_status(app_state, permission_id, PermissionStatus::Granted).await
}
pub async fn deny_permission(
app_state: &State<'_, AppState>,
permission_id: &str,
) -> Result<(), ExtensionError> {
Self::update_permission_status(app_state, permission_id, PermissionStatus::Denied).await
}
pub async fn ask_permission(
app_state: &State<'_, AppState>,
permission_id: &str,
) -> Result<(), ExtensionError> {
Self::update_permission_status(app_state, permission_id, PermissionStatus::Ask).await
}
pub async fn get_ask_permissions(
app_state: &State<'_, AppState>,
extension_id: &str,
) -> Result<Vec<ExtensionPermission>, ExtensionError> {
let all_permissions = Self::get_permissions(app_state, extension_id).await?;
Ok(all_permissions
.into_iter()
.filter(|perm| perm.status == PermissionStatus::Ask)
.collect())
}
}

View File

@ -0,0 +1,3 @@
pub mod manager;
pub mod types;
pub mod validator;

View File

@ -0,0 +1,156 @@
use serde::{Deserialize, Serialize};
use crate::database::error::DatabaseError;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ExtensionPermission {
pub id: String,
pub extension_id: String,
pub resource_type: ResourceType,
pub action: Action,
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraints: Option<PermissionConstraints>,
pub status: PermissionStatus,
// CRDT Felder
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_tombstone: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub haex_timestamp: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Fs,
Http,
Db,
Shell,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Action {
Read,
Write,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionStatus {
Ask,
Granted,
Denied,
}
impl PermissionStatus {
pub fn as_str(&self) -> &str {
match self {
PermissionStatus::Ask => "ask",
PermissionStatus::Granted => "granted",
PermissionStatus::Denied => "denied",
}
}
pub fn from_str(s: &str) -> Result<Self, DatabaseError> {
match s {
"ask" => Ok(PermissionStatus::Ask),
"granted" => Ok(PermissionStatus::Granted),
"denied" => Ok(PermissionStatus::Denied),
_ => Err(DatabaseError::SerializationError {
reason: format!("Unknown permission status: {}", s),
}),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum PermissionConstraints {
Database(DbConstraints),
Filesystem(FsConstraints),
Http(HttpConstraints),
Shell(ShellConstraints),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DbConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub where_clause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub columns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FsConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_file_size: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_extensions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recursive: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct HttpConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub methods: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<RateLimit>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RateLimit {
pub requests: u32,
pub per_minutes: u32,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ShellConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_subcommands: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_flags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub forbidden_args: Option<Vec<String>>,
}
// Wenn du weiterhin gruppierte Permissions brauchst:
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EditablePermissions {
pub permissions: Vec<ExtensionPermission>,
}
// Oder gruppiert nach Typ:
impl EditablePermissions {
pub fn database_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Db)
.collect()
}
pub fn filesystem_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Fs)
.collect()
}
pub fn http_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Http)
.collect()
}
pub fn shell_permissions(&self) -> Vec<&ExtensionPermission> {
self.permissions
.iter()
.filter(|p| p.resource_type == ResourceType::Shell)
.collect()
}
}

View File

@ -0,0 +1,201 @@
// src-tauri/src/extension/permissions/validator.rs
use crate::database::core::{extract_table_names_from_sql, parse_single_statement};
use crate::database::error::DatabaseError;
use crate::extension::error::ExtensionError;
use crate::extension::permissions::manager::PermissionManager;
use crate::extension::permissions::types::Action;
use crate::AppState;
use sqlparser::ast::{Statement, TableFactor, TableObject};
use tauri::State;
pub struct SqlPermissionValidator;
impl SqlPermissionValidator {
/// Validiert ein SQL-Statement gegen die Permissions einer Extension
pub async fn validate_sql(
app_state: &State<'_, AppState>,
extension_id: &str,
sql: &str,
) -> Result<(), ExtensionError> {
let statement = parse_single_statement(sql).map_err(|e| DatabaseError::ParseError {
reason: e.to_string(),
sql: sql.to_string(),
})?;
match &statement {
Statement::Query(_) => {
Self::validate_read_statement(app_state, extension_id, sql).await
}
Statement::Insert(_) | Statement::Update { .. } | Statement::Delete(_) => {
Self::validate_write_statement(app_state, extension_id, &statement).await
}
Statement::CreateTable(_) => {
Self::validate_create_statement(app_state, extension_id, &statement).await
}
Statement::AlterTable { .. } | Statement::Drop { .. } => {
Self::validate_schema_statement(app_state, extension_id, &statement).await
}
_ => Err(ExtensionError::ValidationError {
reason: format!("Statement type not allowed: {}", sql),
}),
}
}
/// Validiert READ-Operationen (SELECT)
async fn validate_read_statement(
app_state: &State<'_, AppState>,
extension_id: &str,
sql: &str,
) -> Result<(), ExtensionError> {
let tables = extract_table_names_from_sql(sql)?;
for table_name in tables {
PermissionManager::check_database_permission(
app_state,
extension_id,
Action::Read,
&table_name,
)
.await?;
}
Ok(())
}
/// Validiert WRITE-Operationen (INSERT, UPDATE, DELETE)
async fn validate_write_statement(
app_state: &State<'_, AppState>,
extension_id: &str,
statement: &Statement,
) -> Result<(), ExtensionError> {
let table_names = Self::extract_table_names_from_statement(statement)?;
for table_name in table_names {
PermissionManager::check_database_permission(
app_state,
extension_id,
Action::Write,
&table_name,
)
.await?;
}
Ok(())
}
/// Validiert CREATE TABLE
async fn validate_create_statement(
app_state: &State<'_, AppState>,
extension_id: &str,
statement: &Statement,
) -> Result<(), ExtensionError> {
if let Statement::CreateTable(create_table) = statement {
let table_name = create_table.name.to_string();
// Prüfe ob Extension überhaupt CREATE-Rechte hat (z.B. auf "*")
PermissionManager::check_database_permission(
app_state,
extension_id,
Action::Write,
&table_name,
)
.await?;
}
Ok(())
}
/// Validiert Schema-Änderungen (ALTER, DROP)
async fn validate_schema_statement(
app_state: &State<'_, AppState>,
extension_id: &str,
statement: &Statement,
) -> Result<(), ExtensionError> {
let table_names = Self::extract_table_names_from_statement(statement)?;
for table_name in table_names {
// ALTER/DROP benötigen WRITE-Rechte
PermissionManager::check_database_permission(
app_state,
extension_id,
Action::Write,
&table_name,
)
.await?;
}
Ok(())
}
/// Extrahiert alle Tabellennamen aus einem Statement
fn extract_table_names_from_statement(
statement: &Statement,
) -> Result<Vec<String>, ExtensionError> {
match statement {
Statement::Insert(insert) => Ok(vec![Self::extract_table_name_from_insert(insert)?]),
Statement::Update { table, .. } => {
Ok(vec![Self::extract_table_name_from_table_factor(
&table.relation,
)?])
}
Statement::Delete(delete) => Ok(vec![Self::extract_table_name_from_delete(delete)?]),
Statement::CreateTable(create_table) => Ok(vec![create_table.name.to_string()]),
Statement::AlterTable { name, .. } => Ok(vec![name.to_string()]),
Statement::Drop { names, .. } => {
Ok(names.iter().map(|name| name.to_string()).collect())
}
_ => Ok(vec![]),
}
}
/// Extrahiert Tabellenname aus INSERT
fn extract_table_name_from_insert(
insert: &sqlparser::ast::Insert,
) -> Result<String, ExtensionError> {
match &insert.table {
TableObject::TableName(name) => Ok(name.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, ExtensionError> {
match table_factor {
TableFactor::Table { name, .. } => Ok(name.to_string()),
_ => Err(DatabaseError::StatementError {
reason: "Complex table references not supported".to_string(),
}
.into()),
}
}
/// Extrahiert Tabellenname aus DELETE
fn extract_table_name_from_delete(
delete: &sqlparser::ast::Delete,
) -> Result<String, ExtensionError> {
use sqlparser::ast::FromTable;
let table_name = match &delete.from {
FromTable::WithFromKeyword(tables) | FromTable::WithoutKeyword(tables) => {
if !tables.is_empty() {
Self::extract_table_name_from_table_factor(&tables[0].relation)?
} else if !delete.tables.is_empty() {
delete.tables[0].to_string()
} else {
return Err(DatabaseError::NoTableError {
sql: delete.to_string(),
}
.into());
}
}
};
Ok(table_name)
}
}

View File

@ -1,28 +1,22 @@
//mod browser;
//mod android_storage;
mod crdt;
mod database;
mod extension;
//mod models;
use crate::{
crdt::hlc::HlcService,
database::DbConnection,
extension::core::{ExtensionManager, ExtensionState},
};
use std::sync::{Arc, Mutex};
use tauri::Manager;
pub mod table_names {
include!(concat!(env!("OUT_DIR"), "/tableNames.rs"));
}
use std::sync::{Arc, Mutex};
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.
pub hlc: Mutex<HlcService>,
pub extension_manager: ExtensionManager,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -31,26 +25,27 @@ pub fn run() {
tauri::Builder::default()
.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
// Hole den AppState aus dem Context
let app_handle = context.app_handle();
let state = app_handle.state::<AppState>();
// Rufe den Handler mit allen benötigten Parametern auf
match extension::core::extension_protocol_handler(state, &app_handle, &request) {
Ok(response) => response,
Err(e) => {
// Wenn der Handler einen Fehler zurückgibt, logge ihn und erstelle eine Fehler-Response
eprintln!(
"Fehler im Custom Protocol Handler für URI '{}': {}",
request.uri(),
e
);
// Erstelle eine HTTP 500 Fehler-Response
// Du kannst hier auch spezifischere Fehler-Responses bauen, falls gewünscht.
tauri::http::Response::builder()
.status(500)
.header("Content-Type", "text/plain") // Optional, aber gut für Klarheit
.header("Content-Type", "text/plain")
.body(Vec::from(format!(
"Interner Serverfehler im Protokollhandler: {}",
e
)))
.unwrap_or_else(|build_err| {
// Fallback, falls selbst das Erstellen der Fehler-Response fehlschlägt
eprintln!("Konnte Fehler-Response nicht erstellen: {}", build_err);
tauri::http::Response::builder()
.status(500)
@ -60,11 +55,10 @@ pub fn run() {
}
}
})
/* .manage(database::DbConnection(Arc::new(Mutex::new(None))))
.manage(crdt::hlc::HlcService::new()) */
.manage(AppState {
db: DbConnection(Arc::new(Mutex::new(None))),
hlc: Mutex::new(HlcService::new()), // Starte mit einem uninitialisierten HLC
hlc: Mutex::new(HlcService::new()),
extension_manager: ExtensionManager::new(),
})
.manage(ExtensionState::default())
.plugin(tauri_plugin_dialog::init())
@ -75,7 +69,6 @@ pub fn run() {
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_persisted_scope::init())
.plugin(tauri_plugin_store::Builder::new().build())
//.plugin(tauri_plugin_android_fs::init())
.invoke_handler(tauri::generate_handler![
database::create_encrypted_database,
database::delete_vault,
@ -86,36 +79,13 @@ pub fn run() {
database::vault_exists,
extension::database::extension_sql_execute,
extension::database::extension_sql_select,
//database::update_hlc_from_remote,
/* extension::copy_directory,
extension::database::extension_sql_select, */
extension::get_all_extensions,
extension::get_extension_info,
extension::install_extension_with_permissions,
extension::is_extension_installed,
extension::preview_extension,
extension::remove_extension,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
/* fn extension_protocol_handler(
app_handle: &tauri::AppHandle, // Beachten Sie die Signaturänderung in neueren Tauri-Versionen
request: &tauri::http::Request<Vec<u8>>,
) -> Result<tauri::http::Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync>> {
let uri_str = request.uri().to_string();
let parsed_url = match Url::parse(&uri_str) {
Ok(url) => url,
Err(e) => {
eprintln!("Fehler beim Parsen der URL '{}': {}", uri_str, e);
return Ok(tauri::http::ResponseBuilder::new().status(400).body(Vec::from("Ungültige URL"))?);
}
};
let plugin_id = parsed_url.host_str().ok_or_else(|| "Fehlende Plugin-ID in der URL".to_string())?;
let path_segments: Vec<&str> = parsed_url.path_segments().ok_or_else(|| "URL hat keinen Pfad".to_string())?.collect();
if path_segments.len() < 2 {
eprintln!("Unvollständiger Pfad in URL: {}", uri_str);
return Ok(tauri::http::Response::new().status(400).body(Vec::from("Unvollständiger Pfad"))?);
}
let version = path_segments;
let file_path = path_segments[1..].join("/");
Ok(tauri::http::Response::builder()::new().status(404).body(Vec::new())?)
} */