mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
refactore manifest and permission
This commit is contained in:
@ -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;
|
||||
|
||||
@ -755,6 +755,7 @@ impl CrdtTransformer {
|
||||
selection: del_stmt.selection.clone(),
|
||||
returning: None,
|
||||
or: None,
|
||||
limit: None,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
311
src-tauri/src/extension/core/manager.rs
Normal file
311
src-tauri/src/extension/core/manager.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
250
src-tauri/src/extension/core/manifest.rs
Normal file
250
src-tauri/src/extension/core/manifest.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
10
src-tauri/src/extension/core/mod.rs
Normal file
10
src-tauri/src/extension/core/mod.rs
Normal 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::*;
|
||||
252
src-tauri/src/extension/core/protocol.rs
Normal file
252
src-tauri/src/extension/core/protocol.rs
Normal 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)
|
||||
}
|
||||
94
src-tauri/src/extension/core/types.rs
Normal file
94
src-tauri/src/extension/core/types.rs
Normal 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(())
|
||||
}
|
||||
973
src-tauri/src/extension/core_old.rs
Normal file
973
src-tauri/src/extension/core_old.rs
Normal 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)
|
||||
}
|
||||
74
src-tauri/src/extension/crypto.rs
Normal file
74
src-tauri/src/extension/crypto.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
153
src-tauri/src/extension/database/executor.rs
Normal file
153
src-tauri/src/extension/database/executor.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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, ¶ms)?;
|
||||
@ -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, ¶ms)?;
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
} */
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
650
src-tauri/src/extension/permissions/manager.rs
Normal file
650
src-tauri/src/extension/permissions/manager.rs
Normal 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, ¶ms)?;
|
||||
}
|
||||
|
||||
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, ¶ms)?;
|
||||
|
||||
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, ¶ms)?;
|
||||
|
||||
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, ¶ms)?;
|
||||
|
||||
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, ¶ms)?;
|
||||
|
||||
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, ¶ms)?;
|
||||
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
3
src-tauri/src/extension/permissions/mod.rs
Normal file
3
src-tauri/src/extension/permissions/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod manager;
|
||||
pub mod types;
|
||||
pub mod validator;
|
||||
156
src-tauri/src/extension/permissions/types.rs
Normal file
156
src-tauri/src/extension/permissions/types.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
201
src-tauri/src/extension/permissions/validator.rs
Normal file
201
src-tauri/src/extension/permissions/validator.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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())?)
|
||||
} */
|
||||
|
||||
Reference in New Issue
Block a user