From c8c3a5c73f3c4da1b0a38b106d94c3c3b451970d Mon Sep 17 00:00:00 2001 From: haex Date: Tue, 7 Oct 2025 00:41:21 +0200 Subject: [PATCH] refactored install dialog --- nuxt.config.ts | 8 + package.json | 15 +- src-tauri/Cargo.lock | 4 +- src-tauri/Cargo.toml | 4 +- src-tauri/bindings/Action.ts | 10 + src-tauri/bindings/DatabaseError.ts | 3 + src-tauri/bindings/DbAction.ts | 6 + src-tauri/bindings/DbConstraints.ts | 3 + src-tauri/bindings/ExtensionInfoResponse.ts | 3 + src-tauri/bindings/ExtensionManifest.ts | 4 + src-tauri/bindings/ExtensionPermissions.ts | 7 + src-tauri/bindings/ExtensionPreview.ts | 5 + src-tauri/bindings/FsAction.ts | 6 + src-tauri/bindings/FsConstraints.ts | 3 + src-tauri/bindings/HttpAction.ts | 6 + src-tauri/bindings/HttpConstraints.ts | 4 + src-tauri/bindings/PermissionConstraints.ts | 7 + src-tauri/bindings/PermissionEntry.ts | 19 + src-tauri/bindings/PermissionStatus.ts | 3 + src-tauri/bindings/RateLimit.ts | 3 + src-tauri/bindings/ResourceType.ts | 3 + src-tauri/bindings/ShellAction.ts | 6 + src-tauri/bindings/ShellConstraints.ts | 3 + src-tauri/bindings/VaultInfo.ts | 3 + src-tauri/src/database/core.rs | 36 +- src-tauri/src/extension/core/manager.rs | 372 ++++++++---- src-tauri/src/extension/core/manifest.rs | 313 +++++----- src-tauri/src/extension/core/protocol.rs | 28 +- src-tauri/src/extension/crypto.rs | 69 ++- src-tauri/src/extension/error.rs | 13 + .../src/extension/filesystem/permissions.rs | 2 +- src-tauri/src/extension/mod.rs | 17 +- .../src/extension/permissions/manager.rs | 28 +- src-tauri/src/extension/permissions/types.rs | 540 +++++++++++------- .../src/extension/permissions/validator.rs | 8 +- src-tauri/src/lib.rs | 8 +- .../haex/extension/dialog/install.vue | 345 ++++++----- .../haex/extension/permission-item.vue | 128 +++++ .../haex/extension/permission-list.vue | 30 + src/components/ui/input/index.vue | 2 - .../vault/[vaultId]/extensions/index.vue | 48 +- src/stores/extensions/index.ts | 10 + src/stores/vault/notifications.ts | 15 +- tsconfig.json | 6 + 44 files changed, 1426 insertions(+), 730 deletions(-) create mode 100644 src-tauri/bindings/Action.ts create mode 100644 src-tauri/bindings/DatabaseError.ts create mode 100644 src-tauri/bindings/DbAction.ts create mode 100644 src-tauri/bindings/DbConstraints.ts create mode 100644 src-tauri/bindings/ExtensionInfoResponse.ts create mode 100644 src-tauri/bindings/ExtensionManifest.ts create mode 100644 src-tauri/bindings/ExtensionPermissions.ts create mode 100644 src-tauri/bindings/ExtensionPreview.ts create mode 100644 src-tauri/bindings/FsAction.ts create mode 100644 src-tauri/bindings/FsConstraints.ts create mode 100644 src-tauri/bindings/HttpAction.ts create mode 100644 src-tauri/bindings/HttpConstraints.ts create mode 100644 src-tauri/bindings/PermissionConstraints.ts create mode 100644 src-tauri/bindings/PermissionEntry.ts create mode 100644 src-tauri/bindings/PermissionStatus.ts create mode 100644 src-tauri/bindings/RateLimit.ts create mode 100644 src-tauri/bindings/ResourceType.ts create mode 100644 src-tauri/bindings/ShellAction.ts create mode 100644 src-tauri/bindings/ShellConstraints.ts create mode 100644 src-tauri/bindings/VaultInfo.ts create mode 100644 src/components/haex/extension/permission-item.vue create mode 100644 src/components/haex/extension/permission-list.vue diff --git a/nuxt.config.ts b/nuxt.config.ts index babc32a..ed6cb08 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,5 +1,7 @@ //import tailwindcss from '@tailwindcss/vite' +import { fileURLToPath } from 'node:url' + // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: '2025-07-15', @@ -7,6 +9,12 @@ export default defineNuxtConfig({ srcDir: './src', + alias: { + '@bindings': fileURLToPath( + new URL('./src-tauri/bindings', import.meta.url), + ), + }, + app: { pageTransition: { name: 'fade', diff --git a/package.json b/package.json index 5e94637..7890255 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,16 @@ "scripts": { "build": "nuxt build", "dev": "nuxt dev", - "generate": "nuxt generate", - "preview": "nuxt preview", - "postinstall": "nuxt prepare", - "tauri": "tauri", - "tauri:build:debug": "tauri build --debug", - "generate:rust-types": "tsx ./src-tauri/database/generate-rust-types.ts", "drizzle:generate": "drizzle-kit generate", "drizzle:migrate": "drizzle-kit migrate", - "eslint:fix": "eslint --fix" + "eslint:fix": "eslint --fix", + "generate:rust-types": "tsx ./src-tauri/database/generate-rust-types.ts", + "generate:ts-types": "cd src-tauri && cargo test", + "generate": "nuxt generate", + "postinstall": "nuxt prepare", + "preview": "nuxt preview", + "tauri:build:debug": "tauri build --debug", + "tauri": "tauri" }, "dependencies": { "@nuxt/eslint": "1.9.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 609aa93..221919a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5264,9 +5264,9 @@ dependencies = [ [[package]] name = "uhlc" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bbb93b0c2258fe1e81a84d8de5391f2577b039decabf75a6441ea1ebbf4cb5" +checksum = "b62a645e3e4e6c85b7abe49b086aa3204119431f42b6123b0070419fb6e9d24e" dependencies = [ "humantime", "lazy_static", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01c2d76..6575f83 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,8 +48,8 @@ tauri-plugin-os = "2.3" tauri-plugin-persisted-scope = "2.3.2" tauri-plugin-store = "2.4.0" thiserror = "2.0.17" -ts-rs = "11.0.1" -uhlc = "0.8" +ts-rs = { version = "11.0.1", features = ["serde-compat"] } +uhlc = "0.8.2" uuid = { version = "1.18.1", features = ["v4"] } zip = "5.1.1" url = "2.5.7" diff --git a/src-tauri/bindings/Action.ts b/src-tauri/bindings/Action.ts new file mode 100644 index 0000000..54c8d82 --- /dev/null +++ b/src-tauri/bindings/Action.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DbAction } from "./DbAction"; +import type { FsAction } from "./FsAction"; +import type { HttpAction } from "./HttpAction"; +import type { ShellAction } from "./ShellAction"; + +/** + * Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält. + */ +export type Action = { "Database": DbAction } | { "Filesystem": FsAction } | { "Http": HttpAction } | { "Shell": ShellAction }; diff --git a/src-tauri/bindings/DatabaseError.ts b/src-tauri/bindings/DatabaseError.ts new file mode 100644 index 0000000..f5b0358 --- /dev/null +++ b/src-tauri/bindings/DatabaseError.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DatabaseError = { "type": "ParseError", "details": { reason: string, sql: string, } } | { "type": "ParameterMismatchError", "details": { expected: number, provided: number, sql: string, } } | { "type": "NoTableError", "details": { sql: string, } } | { "type": "StatementError", "details": { reason: string, } } | { "type": "PrepareError", "details": { reason: string, } } | { "type": "DatabaseError", "details": { reason: string, } } | { "type": "ExecutionError", "details": { sql: string, reason: string, table: string | null, } } | { "type": "TransactionError", "details": { reason: string, } } | { "type": "UnsupportedStatement", "details": { reason: string, sql: string, } } | { "type": "HlcError", "details": { reason: string, } } | { "type": "LockError", "details": { reason: string, } } | { "type": "ConnectionError", "details": { reason: string, } } | { "type": "SerializationError", "details": { reason: string, } } | { "type": "PermissionError", "details": { extension_id: string, operation: string | null, resource: string | null, reason: string, } } | { "type": "QueryError", "details": { reason: string, } } | { "type": "RowProcessingError", "details": { reason: string, } } | { "type": "MutexPoisoned", "details": { reason: string, } } | { "type": "ConnectionFailed", "details": { path: string, reason: string, } } | { "type": "PragmaError", "details": { pragma: string, reason: string, } } | { "type": "PathResolutionError", "details": { reason: string, } } | { "type": "IoError", "details": { path: string, reason: string, } } | { "type": "CrdtSetup", "details": string }; diff --git a/src-tauri/bindings/DbAction.ts b/src-tauri/bindings/DbAction.ts new file mode 100644 index 0000000..4782511 --- /dev/null +++ b/src-tauri/bindings/DbAction.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Definiert Aktionen, die auf eine Datenbank angewendet werden können. + */ +export type DbAction = "read" | "readwrite" | "create" | "delete" | "alterdrop"; diff --git a/src-tauri/bindings/DbConstraints.ts b/src-tauri/bindings/DbConstraints.ts new file mode 100644 index 0000000..c124589 --- /dev/null +++ b/src-tauri/bindings/DbConstraints.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DbConstraints = { where_clause: string | null, columns: Array | null, limit: number | null, }; diff --git a/src-tauri/bindings/ExtensionInfoResponse.ts b/src-tauri/bindings/ExtensionInfoResponse.ts new file mode 100644 index 0000000..c794c02 --- /dev/null +++ b/src-tauri/bindings/ExtensionInfoResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExtensionInfoResponse = { key_hash: string, name: string, full_id: string, version: string, display_name: string | null, namespace: string | null, allowed_origin: string, }; diff --git a/src-tauri/bindings/ExtensionManifest.ts b/src-tauri/bindings/ExtensionManifest.ts new file mode 100644 index 0000000..66d12eb --- /dev/null +++ b/src-tauri/bindings/ExtensionManifest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExtensionPermissions } from "./ExtensionPermissions"; + +export type ExtensionManifest = { id: string, name: string, version: string, author: string | null, entry: string, icon: string | null, public_key: string, signature: string, permissions: ExtensionPermissions, homepage: string | null, description: string | null, }; diff --git a/src-tauri/bindings/ExtensionPermissions.ts b/src-tauri/bindings/ExtensionPermissions.ts new file mode 100644 index 0000000..adcd758 --- /dev/null +++ b/src-tauri/bindings/ExtensionPermissions.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PermissionEntry } from "./PermissionEntry"; + +/** + * Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI. + */ +export type ExtensionPermissions = { database: Array | null, filesystem: Array | null, http: Array | null, shell: Array | null, }; diff --git a/src-tauri/bindings/ExtensionPreview.ts b/src-tauri/bindings/ExtensionPreview.ts new file mode 100644 index 0000000..52a217e --- /dev/null +++ b/src-tauri/bindings/ExtensionPreview.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExtensionManifest } from "./ExtensionManifest"; +import type { ExtensionPermissions } from "./ExtensionPermissions"; + +export type ExtensionPreview = { manifest: ExtensionManifest, is_valid_signature: boolean, key_hash: string, editable_permissions: ExtensionPermissions, }; diff --git a/src-tauri/bindings/FsAction.ts b/src-tauri/bindings/FsAction.ts new file mode 100644 index 0000000..b42ac58 --- /dev/null +++ b/src-tauri/bindings/FsAction.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Definiert Aktionen, die auf das Dateisystem angewendet werden können. + */ +export type FsAction = "read" | "readwrite"; diff --git a/src-tauri/bindings/FsConstraints.ts b/src-tauri/bindings/FsConstraints.ts new file mode 100644 index 0000000..1fccda8 --- /dev/null +++ b/src-tauri/bindings/FsConstraints.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FsConstraints = { max_file_size: bigint | null, allowed_extensions: Array | null, recursive: boolean | null, }; diff --git a/src-tauri/bindings/HttpAction.ts b/src-tauri/bindings/HttpAction.ts new file mode 100644 index 0000000..39cfbc3 --- /dev/null +++ b/src-tauri/bindings/HttpAction.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können. + */ +export type HttpAction = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*"; diff --git a/src-tauri/bindings/HttpConstraints.ts b/src-tauri/bindings/HttpConstraints.ts new file mode 100644 index 0000000..a683ead --- /dev/null +++ b/src-tauri/bindings/HttpConstraints.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RateLimit } from "./RateLimit"; + +export type HttpConstraints = { methods: Array | null, rate_limit: RateLimit | null, }; diff --git a/src-tauri/bindings/PermissionConstraints.ts b/src-tauri/bindings/PermissionConstraints.ts new file mode 100644 index 0000000..775bdf4 --- /dev/null +++ b/src-tauri/bindings/PermissionConstraints.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DbConstraints } from "./DbConstraints"; +import type { FsConstraints } from "./FsConstraints"; +import type { HttpConstraints } from "./HttpConstraints"; +import type { ShellConstraints } from "./ShellConstraints"; + +export type PermissionConstraints = DbConstraints | FsConstraints | HttpConstraints | ShellConstraints; diff --git a/src-tauri/bindings/PermissionEntry.ts b/src-tauri/bindings/PermissionEntry.ts new file mode 100644 index 0000000..258ef92 --- /dev/null +++ b/src-tauri/bindings/PermissionEntry.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PermissionStatus } from "./PermissionStatus"; + +/** + * Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell. + */ +export type PermissionEntry = { target: string, +/** + * Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute"). + */ +operation?: string | null, +/** + * Optionale, spezifische Einschränkungen für diese Berechtigung. + */ +constraints?: Record, +/** + * Der Status der Berechtigung (wird nur im UI-Modell verwendet). + */ +status?: PermissionStatus | null, }; diff --git a/src-tauri/bindings/PermissionStatus.ts b/src-tauri/bindings/PermissionStatus.ts new file mode 100644 index 0000000..192119a --- /dev/null +++ b/src-tauri/bindings/PermissionStatus.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PermissionStatus = "ask" | "granted" | "denied"; diff --git a/src-tauri/bindings/RateLimit.ts b/src-tauri/bindings/RateLimit.ts new file mode 100644 index 0000000..3006cea --- /dev/null +++ b/src-tauri/bindings/RateLimit.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RateLimit = { requests: number, per_minutes: number, }; diff --git a/src-tauri/bindings/ResourceType.ts b/src-tauri/bindings/ResourceType.ts new file mode 100644 index 0000000..07df7e5 --- /dev/null +++ b/src-tauri/bindings/ResourceType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ResourceType = "fs" | "http" | "db" | "shell"; diff --git a/src-tauri/bindings/ShellAction.ts b/src-tauri/bindings/ShellAction.ts new file mode 100644 index 0000000..0450ddc --- /dev/null +++ b/src-tauri/bindings/ShellAction.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Definiert Aktionen, die auf Shell-Befehle angewendet werden können. + */ +export type ShellAction = "execute"; diff --git a/src-tauri/bindings/ShellConstraints.ts b/src-tauri/bindings/ShellConstraints.ts new file mode 100644 index 0000000..5fc9720 --- /dev/null +++ b/src-tauri/bindings/ShellConstraints.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ShellConstraints = { allowed_subcommands: Array | null, allowed_flags: Array | null, forbidden_args: Array | null, }; diff --git a/src-tauri/bindings/VaultInfo.ts b/src-tauri/bindings/VaultInfo.ts new file mode 100644 index 0000000..02ece7a --- /dev/null +++ b/src-tauri/bindings/VaultInfo.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type VaultInfo = { name: string, lastAccess: bigint, path: string, }; diff --git a/src-tauri/src/database/core.rs b/src-tauri/src/database/core.rs index 8a92ba8..30abd75 100644 --- a/src-tauri/src/database/core.rs +++ b/src-tauri/src/database/core.rs @@ -9,7 +9,7 @@ use rusqlite::{ Connection, OpenFlags, ToSql, }; use serde_json::Value as JsonValue; -use sqlparser::ast::{Query, Select, SetExpr, Statement, TableFactor, TableObject}; +use sqlparser::ast::{Expr, Query, Select, SetExpr, Statement, TableFactor, TableObject}; use sqlparser::dialect::SQLiteDialect; use sqlparser::parser::Parser; use std::collections::HashMap; @@ -328,8 +328,42 @@ fn extract_tables_from_select(select: &Select, tables: &mut Vec) { extract_tables_from_table_factor(&join.relation, tables); } } + if let Some(selection) = &select.selection { + extract_tables_from_expr_recursive(selection, tables); + } } +fn extract_tables_from_expr_recursive(expr: &Expr, tables: &mut Vec) { + match expr { + // This is the key: we found a subquery! + Expr::Subquery(subquery) => { + extract_tables_from_query_recursive(subquery, tables); + } + // These expressions can contain other expressions + Expr::BinaryOp { left, right, .. } => { + extract_tables_from_expr_recursive(left, tables); + extract_tables_from_expr_recursive(right, tables); + } + Expr::UnaryOp { expr, .. } => { + extract_tables_from_expr_recursive(expr, tables); + } + Expr::InSubquery { expr, subquery, .. } => { + extract_tables_from_expr_recursive(expr, tables); + extract_tables_from_query_recursive(subquery, tables); + } + Expr::Between { + expr, low, high, .. + } => { + extract_tables_from_expr_recursive(expr, tables); + extract_tables_from_expr_recursive(low, tables); + extract_tables_from_expr_recursive(high, tables); + } + // ... other expression types can be added here if needed + _ => { + // Other expressions (like literals, column names, etc.) don't contain tables. + } + } +} /// Extrahiert Tabellennamen aus TableFactor-Strukturen fn extract_tables_from_table_factor(table_factor: &TableFactor, tables: &mut Vec) { match table_factor { diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs index 923f068..183d3d0 100644 --- a/src-tauri/src/extension/core/manager.rs +++ b/src-tauri/src/extension/core/manager.rs @@ -1,14 +1,17 @@ -// src-tauri/src/extension/core/manager.rs - +use crate::database::core::with_connection; +use crate::database::error::DatabaseError; use crate::extension::core::manifest::{EditablePermissions, ExtensionManifest, ExtensionPreview}; use crate::extension::core::types::{copy_directory, Extension, ExtensionSource}; +use crate::extension::core::ExtensionPermissions; use crate::extension::crypto::ExtensionCrypto; +use crate::extension::database::executor::SqlExecutor; use crate::extension::error::ExtensionError; use crate::extension::permissions::manager::PermissionManager; -use crate::extension::permissions::types::{ExtensionPermission, PermissionStatus}; +use crate::extension::permissions::types::ExtensionPermission; +use crate::table_names::TABLE_EXTENSIONS; use crate::AppState; use std::collections::HashMap; -use std::fs::File; +use std::fs::{self, File}; use std::path::PathBuf; use std::sync::Mutex; use std::time::{Duration, SystemTime}; @@ -22,11 +25,36 @@ pub struct CachedPermission { pub ttl: Duration, } +#[derive(Debug, Clone)] +pub struct MissingExtension { + pub full_extension_id: String, + pub name: String, + pub version: String, +} + +struct ExtensionDataFromDb { + manifest: ExtensionManifest, + enabled: bool, +} + #[derive(Default)] pub struct ExtensionManager { pub production_extensions: Mutex>, pub dev_extensions: Mutex>, pub permission_cache: Mutex>, + pub missing_extensions: Mutex>, +} + +struct ExtractedExtension { + temp_dir: PathBuf, + manifest: ExtensionManifest, + content_hash: String, +} + +impl Drop for ExtractedExtension { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.temp_dir).ok(); + } } impl ExtensionManager { @@ -34,6 +62,49 @@ impl ExtensionManager { Self::default() } + /// Extrahiert eine Extension-ZIP-Datei und validiert das Manifest + fn extract_and_validate_extension( + source_path: &str, + temp_prefix: &str, + ) -> Result { + let source = PathBuf::from(source_path); + let temp = std::env::temp_dir().join(format!("{}_{}", temp_prefix, 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.to_string(), + } + })?; + + Ok(ExtractedExtension { + temp_dir: temp, + manifest, + content_hash, + }) + } + pub fn get_base_extension_dir( &self, app_handle: &AppHandle, @@ -45,23 +116,42 @@ impl ExtensionManager { source: std::io::Error::new(std::io::ErrorKind::NotFound, e.to_string()), })? .join("extensions"); + + // Sicherstellen, dass das Basisverzeichnis existiert + if !path.exists() { + fs::create_dir_all(&path).map_err(|e| ExtensionError::Filesystem { source: e })?; + } Ok(path) } pub fn get_extension_dir( &self, app_handle: &AppHandle, - extension_id: &str, + key_hash: &str, + extension_name: &str, extension_version: &str, ) -> Result { let specific_extension_dir = self .get_base_extension_dir(app_handle)? - .join(extension_id) + .join(key_hash) + .join(extension_name) .join(extension_version); Ok(specific_extension_dir) } + pub fn get_extension_path_by_full_extension_id( + &self, + app_handle: &AppHandle, + full_extension_id: &str, + ) -> Result { + let specific_extension_dir = self + .get_base_extension_dir(app_handle)? + .join(full_extension_id); + + Ok(specific_extension_dir) + } + pub fn add_production_extension(&self, extension: Extension) -> Result<(), ExtensionError> { if extension.id.is_empty() { return Err(ExtensionError::ValidationError { @@ -133,15 +223,40 @@ impl ExtensionManager { pub async fn remove_extension_internal( &self, app_handle: &AppHandle, - extension_id: String, - extension_version: String, + key_hash: &str, + extension_id: &str, + extension_version: &str, state: &State<'_, AppState>, ) -> Result<(), ExtensionError> { - PermissionManager::delete_permissions(state, &extension_id).await?; + // Lösche Permissions und Extension-Eintrag in einer Transaktion + with_connection(&state.db, |conn| { + let tx = conn.transaction().map_err(DatabaseError::from)?; + + let hlc_service = state.hlc.lock().map_err(|_| DatabaseError::MutexPoisoned { + reason: "Failed to lock HLC service".to_string(), + })?; + + // Lösche alle Permissions + PermissionManager::delete_permissions_in_transaction(&tx, &hlc_service, extension_id)?; + + // Lösche Extension-Eintrag + let sql = format!("DELETE FROM {} WHERE id = ?", TABLE_EXTENSIONS); + SqlExecutor::execute_internal_typed( + &tx, + &hlc_service, + &sql, + rusqlite::params![extension_id], + )?; + + tx.commit().map_err(DatabaseError::from) + })?; + + // Entferne aus dem In-Memory-Manager self.remove_extension(&extension_id)?; + // Lösche Dateien vom Dateisystem let extension_dir = - self.get_extension_dir(app_handle, &extension_id, &extension_version)?; + self.get_extension_dir(app_handle, key_hash, extension_id, extension_version)?; if extension_dir.exists() { std::fs::remove_dir_all(&extension_dir) @@ -155,48 +270,20 @@ impl ExtensionManager { &self, source_path: String, ) -> Result { - 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 extracted = Self::extract_and_validate_extension(&source_path, "haexhub_preview")?; let is_valid_signature = ExtensionCrypto::verify_signature( - &manifest.public_key, - &content_hash, - &manifest.signature, + &extracted.manifest.public_key, + &extracted.content_hash, + &extracted.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(); + let key_hash = extracted.manifest.calculate_key_hash()?; + let editable_permissions = extracted.manifest.to_editable_permissions(); Ok(ExtensionPreview { - manifest, + manifest: extracted.manifest.clone(), is_valid_signature, key_hash, editable_permissions, @@ -210,78 +297,45 @@ impl ExtensionManager { custom_permissions: EditablePermissions, state: &State<'_, AppState>, ) -> Result { - let source = PathBuf::from(&source_path); + let extracted = Self::extract_and_validate_extension(&source_path, "haexhub_ext")?; - 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 })?; + // Signatur verifizieren (bei Installation wird ein Fehler geworfen, nicht nur geprüft) + ExtensionCrypto::verify_signature( + &extracted.manifest.public_key, + &extracted.content_hash, + &extracted.manifest.signature, + ) + .map_err(|e| ExtensionError::SignatureVerificationFailed { reason: 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), - })?; + let full_extension_id = extracted.manifest.full_extension_id()?; - 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); + let extensions_dir = self.get_extension_dir( + &app_handle, + &extracted.manifest.calculate_key_hash()?, + &extracted.manifest.name, + &extracted.manifest.version, + )?; std::fs::create_dir_all(&extensions_dir) .map_err(|e| ExtensionError::Filesystem { source: e })?; copy_directory( - temp.to_string_lossy().to_string(), + extracted.temp_dir.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?; + PermissionManager::save_permissions(state, &permissions).await?; let extension = Extension { id: full_extension_id.clone(), - name: manifest.name.clone(), + name: extracted.manifest.name.clone(), source: ExtensionSource::Production { path: extensions_dir.clone(), - version: manifest.version.clone(), + version: extracted.manifest.version.clone(), }, - manifest: manifest.clone(), + manifest: extracted.manifest.clone(), enabled: true, last_accessed: SystemTime::now(), }; @@ -290,22 +344,116 @@ impl ExtensionManager { Ok(full_extension_id) } -} -// Backward compatibility -#[derive(Default)] -pub struct ExtensionState { - pub extensions: Mutex>, -} + /// Scannt das Dateisystem beim Start und lädt alle installierten Erweiterungen. + pub async fn load_installed_extensions( + &self, + app_handle: &AppHandle, + state: &State<'_, AppState>, + ) -> Result, ExtensionError> { + self.production_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .clear(); + self.permission_cache + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .clear(); + self.missing_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .clear(); -impl ExtensionState { - pub fn add_extension(&self, path: String, manifest: ExtensionManifest) { - let mut extensions = self.extensions.lock().unwrap(); - extensions.insert(path, manifest); - } + // Schritt 1: Alle Daten aus der Datenbank in einem Rutsch laden. + let extensions = with_connection(&state.db, |conn| { + let sql = "SELECT id, name, version, author, entry, icon, public_key, signature, homepage, description, enabled FROM haexExtensions"; + let results = SqlExecutor::select_internal(conn, sql, &[])?; - pub fn get_extension(&self, addon_id: &str) -> Option { - let extensions = self.extensions.lock().unwrap(); - extensions.values().find(|p| p.name == addon_id).cloned() + let mut data = Vec::new(); + for result in results { + let manifest = ExtensionManifest { + id: result["id"] + .as_str() + .ok_or_else(|| DatabaseError::SerializationError { + reason: "Missing id field".to_string(), + })? + .to_string(), + name: result["name"] + .as_str() + .ok_or_else(|| DatabaseError::SerializationError { + reason: "Missing name field".to_string(), + })? + .to_string(), + version: result["version"] + .as_str() + .ok_or_else(|| DatabaseError::SerializationError { + reason: "Missing version field".to_string(), + })? + .to_string(), + author: result["author"].as_str().map(String::from), + entry: result["entry"].as_str().unwrap_or("index.html").to_string(), + icon: result["icon"].as_str().map(String::from), + public_key: result["public_key"].as_str().unwrap_or("").to_string(), + signature: result["signature"].as_str().unwrap_or("").to_string(), + permissions: ExtensionPermissions::default(), + homepage: result["homepage"].as_str().map(String::from), + description: result["description"].as_str().map(String::from), + }; + + let enabled = result["enabled"] + .as_bool() + .or_else(|| result["enabled"].as_i64().map(|v| v != 0)) + .unwrap_or(false); + + data.push(ExtensionDataFromDb { manifest, enabled }); + } + Ok(data) + })?; + + // Schritt 2: Die gesammelten Daten verarbeiten (Dateisystem, State-Mutationen). + let mut loaded_extension_ids = Vec::new(); + + for extension in extensions { + let full_extension_id = extension.manifest.full_extension_id()?; + let extension_path = + self.get_extension_path_by_full_extension_id(app_handle, &full_extension_id)?; + + if !extension_path.exists() || !extension_path.join("manifest.json").exists() { + self.missing_extensions + .lock() + .map_err(|e| ExtensionError::MutexPoisoned { + reason: e.to_string(), + })? + .push(MissingExtension { + full_extension_id: full_extension_id.clone(), + name: extension.manifest.name.clone(), + version: extension.manifest.version.clone(), + }); + continue; + } + + let extension = Extension { + id: full_extension_id.clone(), + name: extension.manifest.name.clone(), + source: ExtensionSource::Production { + path: extension_path, + version: extension.manifest.version.clone(), + }, + manifest: extension.manifest, + enabled: extension.enabled, + last_accessed: SystemTime::now(), + }; + + self.add_production_extension(extension)?; + loaded_extension_ids.push(full_extension_id); + } + + Ok(loaded_extension_ids) } } diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs index 2ae35ea..1b1b0fd 100644 --- a/src-tauri/src/extension/core/manifest.rs +++ b/src-tauri/src/extension/core/manifest.rs @@ -1,14 +1,60 @@ -// 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, + Action, DbAction, ExtensionPermission, FsAction, HttpAction, PermissionConstraints, + PermissionStatus, ResourceType, ShellAction, }; use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use ts_rs::TS; -#[derive(Serialize, Deserialize, Clone, Debug)] +/// Repräsentiert einen einzelnen Berechtigungseintrag im Manifest und im UI-Modell. +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct PermissionEntry { + pub target: String, + + /// Die auszuführende Aktion (z.B. "read", "read_write", "GET", "execute"). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub operation: Option, + + /// Optionale, spezifische Einschränkungen für diese Berechtigung. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(type = "Record")] + pub constraints: Option, + + /// Der Status der Berechtigung (wird nur im UI-Modell verwendet). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +#[derive(Serialize, Deserialize, TS)] +#[ts(export)] +pub struct ExtensionPreview { + pub manifest: ExtensionManifest, + pub is_valid_signature: bool, + pub key_hash: String, + pub editable_permissions: EditablePermissions, +} +/// Definiert die einheitliche Struktur für alle Berechtigungsarten im Manifest und UI. +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct ExtensionPermissions { + #[serde(default)] + pub database: Option>, + #[serde(default)] + pub filesystem: Option>, + #[serde(default)] + pub http: Option>, + #[serde(default)] + pub shell: Option>, +} + +/// Typ-Alias für bessere Lesbarkeit, wenn die Struktur als UI-Modell verwendet wird. +pub type EditablePermissions = ExtensionPermissions; + +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +#[ts(export)] pub struct ExtensionManifest { pub id: String, pub name: String, @@ -18,7 +64,7 @@ pub struct ExtensionManifest { pub icon: Option, pub public_key: String, pub signature: String, - pub permissions: ExtensionManifestPermissions, + pub permissions: ExtensionPermissions, pub homepage: Option, pub description: Option, } @@ -31,192 +77,104 @@ impl ExtensionManifest { pub fn full_extension_id(&self) -> Result { let key_hash = self.calculate_key_hash()?; - Ok(format!("{}-{}", key_hash, self.id)) + Ok(format!("{}_{}_{}", key_hash, self.name, self.version)) } + /// Konvertiert die Manifest-Berechtigungen in das bearbeitbare UI-Modell, + /// indem der Standardstatus `Granted` gesetzt wird. pub fn to_editable_permissions(&self) -> EditablePermissions { + let mut editable = self.permissions.clone(); + + let set_status_for_list = |list: Option<&mut Vec>| { + if let Some(entries) = list { + for entry in entries.iter_mut() { + entry.status = Some(PermissionStatus::Granted); + } + } + }; + + set_status_for_list(editable.database.as_mut()); + set_status_for_list(editable.filesystem.as_mut()); + set_status_for_list(editable.http.as_mut()); + set_status_for_list(editable.shell.as_mut()); + + editable + } +} + +impl ExtensionPermissions { + /// Konvertiert das UI-Modell in die flache Liste von internen `ExtensionPermission`-Objekten. + pub fn to_internal_permissions(&self, extension_id: &str) -> Vec { 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(), - }); + if let Some(entries) = &self.database { + for p in entries { + if let Some(perm) = Self::create_internal(extension_id, ResourceType::Db, p) { + permissions.push(perm); + } } - 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(entries) = &self.filesystem { + for p in entries { + if let Some(perm) = Self::create_internal(extension_id, ResourceType::Fs, p) { + permissions.push(perm); + } + } + } + if let Some(entries) = &self.http { + for p in entries { + if let Some(perm) = Self::create_internal(extension_id, ResourceType::Http, p) { + permissions.push(perm); + } + } + } + if let Some(entries) = &self.shell { + for p in entries { + if let Some(perm) = Self::create_internal(extension_id, ResourceType::Shell, p) { + permissions.push(perm); + } } } - 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(), - }); - } - } + 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(), - target: domain.clone(), - constraints: None, - status: "granted".to_string(), - }); - } - } + /// Parst einen einzelnen `PermissionEntry` und wandelt ihn in die interne, typsichere `ExtensionPermission`-Struktur um. + fn create_internal( + extension_id: &str, + resource_type: ResourceType, + p: &PermissionEntry, + ) -> Option { + let operation_str = p.operation.as_deref().unwrap_or_default(); - 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(), - }); - } - } + let action = match resource_type { + ResourceType::Db => DbAction::from_str(operation_str).ok().map(Action::Database), + ResourceType::Fs => FsAction::from_str(operation_str) + .ok() + .map(Action::Filesystem), + ResourceType::Http => HttpAction::from_str(operation_str).ok().map(Action::Http), + ResourceType::Shell => ShellAction::from_str(operation_str).ok().map(Action::Shell), + }; - EditablePermissions { permissions } + action.map(|act| ExtensionPermission { + id: uuid::Uuid::new_v4().to_string(), + extension_id: extension_id.to_string(), + resource_type: resource_type.clone(), + action: act, + target: p.target.clone(), + constraints: p + .constraints + .as_ref() + .and_then(|c| serde_json::from_value::(c.clone()).ok()), + status: p.status.clone().unwrap_or(PermissionStatus::Ask), + haex_timestamp: None, + haex_tombstone: None, + }) } } -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct ExtensionManifestPermissions { - #[serde(default)] - pub database: Option, - #[serde(default)] - pub filesystem: Option, - #[serde(default)] - pub http: Option>, - #[serde(default)] - pub shell: Option>, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct DatabaseManifestPermissions { - #[serde(default)] - pub read: Vec, - #[serde(default)] - pub write: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct FilesystemManifestPermissions { - #[serde(default)] - pub read: Vec, - #[serde(default)] - pub write: Vec, -} - -// Editable Permissions für UI -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct EditablePermissions { - pub permissions: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct EditablePermission { - pub resource_type: String, - pub action: String, - pub target: String, - pub constraints: Option, - pub status: String, -} - -impl EditablePermissions { - pub fn to_internal_permissions(&self, extension_id: &str) -> Vec { - 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 { - match resource_type { - "db" => serde_json::from_value::(json_value.clone()) - .ok() - .map(PermissionConstraints::Database), - "fs" => serde_json::from_value::(json_value.clone()) - .ok() - .map(PermissionConstraints::Filesystem), - "http" => serde_json::from_value::(json_value.clone()) - .ok() - .map(PermissionConstraints::Http), - "shell" => serde_json::from_value::(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)] +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +#[ts(export)] pub struct ExtensionInfoResponse { pub key_hash: String, pub name: String, @@ -231,6 +189,7 @@ impl ExtensionInfoResponse { pub fn from_extension( extension: &crate::extension::core::types::Extension, ) -> Result { + // Annahme: get_tauri_origin ist in deinem `types`-Modul oder woanders definiert use crate::extension::core::types::get_tauri_origin; let allowed_origin = get_tauri_origin(); diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs index 65e91ec..41c26fd 100644 --- a/src-tauri/src/extension/core/protocol.rs +++ b/src-tauri/src/extension/core/protocol.rs @@ -12,7 +12,8 @@ use tauri::{AppHandle, State}; #[derive(Deserialize, Debug)] struct ExtensionInfo { - id: String, + key_hash: String, + name: String, version: String, } @@ -66,17 +67,18 @@ impl From for DataProcessingError { pub fn resolve_secure_extension_asset_path( app_handle: &AppHandle, state: State, - extension_id: &str, + key_hash: &str, + extension_name: &str, extension_version: &str, requested_asset_path: &str, ) -> Result { - if extension_id.is_empty() - || !extension_id + if extension_name.is_empty() + || !extension_name .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-') { return Err(ExtensionError::ValidationError { - reason: format!("Invalid extension ID: {}", extension_id), + reason: format!("Invalid extension name: {}", extension_name), }); } @@ -90,10 +92,12 @@ pub fn resolve_secure_extension_asset_path( }); } - let specific_extension_dir = - state - .extension_manager - .get_extension_dir(app_handle, extension_id, extension_version)?; + let specific_extension_dir = state.extension_manager.get_extension_dir( + app_handle, + key_hash, + extension_name, + extension_version, + )?; let clean_relative_path = requested_asset_path .replace('\\', "/") @@ -169,12 +173,14 @@ pub fn extension_protocol_handler( match process_hex_encoded_json(&host) { Ok(info) => { println!("Daten erfolgreich verarbeitet:"); - println!(" ID: {}", info.id); + println!(" KeyHash: {}", info.key_hash); + println!(" Name: {}", info.name); println!(" Version: {}", info.version); let absolute_secure_path = resolve_secure_extension_asset_path( app_handle, state, - &info.id, + &info.key_hash, + &info.name, &info.version, &asset_to_load, )?; diff --git a/src-tauri/src/extension/crypto.rs b/src-tauri/src/extension/crypto.rs index 9941d32..393afe1 100644 --- a/src-tauri/src/extension/crypto.rs +++ b/src-tauri/src/extension/crypto.rs @@ -1,3 +1,8 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + // src-tauri/src/extension/crypto.rs use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use sha2::{Digest, Sha256}; @@ -45,30 +50,62 @@ impl ExtensionCrypto { } /// Berechnet Hash eines Verzeichnisses (für Verifikation) - pub fn hash_directory(dir: &std::path::Path) -> Result { - use std::fs; + pub fn hash_directory(dir: &Path) -> Result { + // 1. Alle Dateipfade rekursiv sammeln + let mut all_files = Vec::new(); + Self::collect_files_recursively(dir, &mut all_files) + .map_err(|e| format!("Failed to collect files: {}", e))?; + all_files.sort(); 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(); + let manifest_path = dir.join("manifest.json"); - // Sortieren für deterministische Hashes - entries.sort_by_key(|e| e.path()); + // 2. Inhalte der sortierten Dateien hashen + for file_path in all_files { + if file_path == manifest_path { + // FÜR DIE MANIFEST.JSON: + let content_str = fs::read_to_string(&file_path) + .map_err(|e| format!("Cannot read manifest file: {}", e))?; - 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))?; + // Parse zu einem generischen JSON-Wert + let mut manifest: serde_json::Value = serde_json::from_str(&content_str) + .map_err(|e| format!("Cannot parse manifest JSON: {}", e))?; + + // Entferne oder leere das Signaturfeld, um den "kanonischen Inhalt" zu erhalten + if let Some(obj) = manifest.as_object_mut() { + obj.insert( + "signature".to_string(), + serde_json::Value::String("".to_string()), + ); + } + + // Serialisiere das modifizierte Manifest zurück (mit 2 Spaces, wie in JS) + let canonical_manifest_content = serde_json::to_string_pretty(&manifest).unwrap(); + println!("canonical_manifest_content: {}", canonical_manifest_content); + hasher.update(canonical_manifest_content.as_bytes()); + } else { + // FÜR ALLE ANDEREN DATEIEN: + let content = fs::read(&file_path) + .map_err(|e| format!("Cannot read file {}: {}", 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())) } + + fn collect_files_recursively(dir: &Path, file_list: &mut Vec) -> std::io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + Self::collect_files_recursively(&path, file_list)?; + } else { + file_list.push(path); + } + } + } + Ok(()) + } } diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs index ac284d5..67ca8f4 100644 --- a/src-tauri/src/extension/error.rs +++ b/src-tauri/src/extension/error.rs @@ -9,6 +9,7 @@ pub enum ExtensionErrorCode { SecurityViolation = 1000, NotFound = 1001, PermissionDenied = 1002, + MutexPoisoned = 1003, Database = 2000, Filesystem = 2001, Http = 2002, @@ -17,6 +18,7 @@ pub enum ExtensionErrorCode { Validation = 3001, InvalidPublicKey = 4000, InvalidSignature = 4001, + InvalidActionString = 4004, SignatureVerificationFailed = 4002, CalculateHash = 4003, Installation = 5000, @@ -76,6 +78,12 @@ pub enum ExtensionError { #[error("Invalid Public Key: {reason}")] InvalidPublicKey { reason: String }, + #[error("Invalid Action: {input} for resource {resource_type}")] + InvalidActionString { + input: String, + resource_type: String, + }, + #[error("Invalid Signature: {reason}")] InvalidSignature { reason: String }, @@ -87,6 +95,9 @@ pub enum ExtensionError { #[error("Extension installation failed: {reason}")] InstallationFailed { reason: String }, + + #[error("A mutex was poisoned: {reason}")] + MutexPoisoned { reason: String }, } impl ExtensionError { @@ -109,6 +120,8 @@ impl ExtensionError { } ExtensionError::InstallationFailed { .. } => ExtensionErrorCode::Installation, ExtensionError::CalculateHashError { .. } => ExtensionErrorCode::CalculateHash, + ExtensionError::MutexPoisoned { .. } => ExtensionErrorCode::MutexPoisoned, + ExtensionError::InvalidActionString { .. } => ExtensionErrorCode::InvalidActionString, } } diff --git a/src-tauri/src/extension/filesystem/permissions.rs b/src-tauri/src/extension/filesystem/permissions.rs index 3a07613..91e9a7b 100644 --- a/src-tauri/src/extension/filesystem/permissions.rs +++ b/src-tauri/src/extension/filesystem/permissions.rs @@ -58,7 +58,7 @@ impl FilesystemPath { /// This would be implemented in your Tauri backend pub fn resolve_system_path( &self, - app_handle: &tauri::AppHandle, + _app_handle: &tauri::AppHandle, ) -> Result { /* let base_dir = match self.path_type { FilesystemPathType::AppData => app_handle.path().app_data_dir(), diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index c4e484c..59ccc41 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -57,11 +57,11 @@ pub fn get_all_extensions(state: State) -> Result, - source_path: String, + extension_path: String, ) -> Result { state .extension_manager - .preview_extension_internal(source_path) + .preview_extension_internal(extension_path) .await } @@ -160,13 +160,20 @@ pub async fn install_extension( #[tauri::command] pub async fn remove_extension( app_handle: AppHandle, - extension_id: String, - extension_version: String, + key_hash: &str, + extension_id: &str, + extension_version: &str, state: State<'_, AppState>, ) -> Result<(), ExtensionError> { state .extension_manager - .remove_extension_internal(&app_handle, extension_id, extension_version, &state) + .remove_extension_internal( + &app_handle, + key_hash, + extension_id, + extension_version, + &state, + ) .await } diff --git a/src-tauri/src/extension/permissions/manager.rs b/src-tauri/src/extension/permissions/manager.rs index 06b1aba..68df314 100644 --- a/src-tauri/src/extension/permissions/manager.rs +++ b/src-tauri/src/extension/permissions/manager.rs @@ -4,14 +4,10 @@ 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::{parse_constraints, Action, DbConstraints, ExtensionPermission, FsConstraints, HttpConstraints, PermissionConstraints, PermissionStatus, ResourceType, ShellConstraints}; -use serde_json; -use serde_json::json; -use std::path::Path; +use crate::extension::permissions::types::{Action, ExtensionPermission, PermissionStatus, ResourceType}; use tauri::State; -use url::Url; -use crate::database::generated::HaexExtensionPermissions; -use rusqlite::{params, ToSql}; +use crate::database::generated::HaexExtensionPermissions; +use rusqlite::params; pub struct PermissionManager; @@ -19,7 +15,6 @@ 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| { @@ -151,17 +146,28 @@ impl PermissionManager { ) -> 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 = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS); SqlExecutor::execute_internal_typed(&tx, &hlc_service, &sql, params![extension_id])?; tx.commit().map_err(DatabaseError::from) }).map_err(ExtensionError::from) } + + /// Löscht alle Permissions einer Extension innerhalb einer bestehenden Transaktion + pub fn delete_permissions_in_transaction( + tx: &rusqlite::Transaction, + hlc_service: &crate::crdt::hlc::HlcService, + extension_id: &str, + ) -> Result<(), DatabaseError> { + let sql = format!("DELETE FROM {} WHERE extension_id = ?", TABLE_EXTENSION_PERMISSIONS); + SqlExecutor::execute_internal_typed(tx, hlc_service, &sql, params![extension_id])?; + Ok(()) + } /// Lädt alle Permissions einer Extension pub async fn get_permissions( app_state: &State<'_, AppState>, @@ -184,8 +190,6 @@ impl PermissionManager { }).map_err(ExtensionError::from) } - - /// Prüft Datenbankberechtigungen pub async fn check_database_permission( app_state: &State<'_, AppState>, diff --git a/src-tauri/src/extension/permissions/types.rs b/src-tauri/src/extension/permissions/types.rs index debd354..d5aa549 100644 --- a/src-tauri/src/extension/permissions/types.rs +++ b/src-tauri/src/extension/permissions/types.rs @@ -1,13 +1,158 @@ -// src-tauri/src/extension/permissions/types.rs - -use std::str::FromStr; - -use crate::{ - database::{error::DatabaseError, generated::HaexExtensionPermissions}, - extension::permissions::manager::PermissionManager, -}; +use crate::extension::error::ExtensionError; use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use ts_rs::TS; +// --- Spezifische Aktionen --- + +/// Definiert Aktionen, die auf eine Datenbank angewendet werden können. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum DbAction { + Read, + ReadWrite, + Create, + Delete, + AlterDrop, +} + +impl DbAction { + /// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht). + pub fn allows_read(&self) -> bool { + matches!(self, DbAction::Read | DbAction::ReadWrite) + } + + /// Prüft, ob diese Aktion Schreibzugriff gewährt. + pub fn allows_write(&self) -> bool { + matches!( + self, + DbAction::ReadWrite | DbAction::Create | DbAction::Delete + ) + } +} + +impl FromStr for DbAction { + type Err = ExtensionError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "read" => Ok(DbAction::Read), + "read_write" => Ok(DbAction::ReadWrite), + "create" => Ok(DbAction::Create), + "delete" => Ok(DbAction::Delete), + _ => Err(ExtensionError::InvalidActionString { + input: s.to_string(), + resource_type: "database".to_string(), + }), + } + } +} + +/// Definiert Aktionen, die auf das Dateisystem angewendet werden können. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum FsAction { + Read, + ReadWrite, +} + +impl FsAction { + /// Prüft, ob diese Aktion Lesezugriff gewährt (implizites Recht). + pub fn allows_read(&self) -> bool { + matches!(self, FsAction::Read | FsAction::ReadWrite) + } + + /// Prüft, ob diese Aktion Schreibzugriff gewährt. + pub fn allows_write(&self) -> bool { + matches!(self, FsAction::ReadWrite) + } +} + +impl FromStr for FsAction { + type Err = ExtensionError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "read" => Ok(FsAction::Read), + "read_write" => Ok(FsAction::ReadWrite), + _ => Err(ExtensionError::InvalidActionString { + input: s.to_string(), + resource_type: "filesystem".to_string(), + }), + } + } +} + +/// Definiert Aktionen (HTTP-Methoden), die auf HTTP-Anfragen angewendet werden können. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "UPPERCASE")] +#[ts(export)] +pub enum HttpAction { + Get, + Post, + Put, + Patch, + Delete, + #[serde(rename = "*")] + All, +} + +impl FromStr for HttpAction { + type Err = ExtensionError; + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "GET" => Ok(HttpAction::Get), + "POST" => Ok(HttpAction::Post), + "PUT" => Ok(HttpAction::Put), + "PATCH" => Ok(HttpAction::Patch), + "DELETE" => Ok(HttpAction::Delete), + "*" => Ok(HttpAction::All), + _ => Err(ExtensionError::InvalidActionString { + input: s.to_string(), + resource_type: "http".to_string(), + }), + } + } +} + +/// Definiert Aktionen, die auf Shell-Befehle angewendet werden können. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum ShellAction { + Execute, +} + +impl FromStr for ShellAction { + type Err = ExtensionError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "execute" => Ok(ShellAction::Execute), + _ => Err(ExtensionError::InvalidActionString { + input: s.to_string(), + resource_type: "shell".to_string(), + }), + } + } +} + +// --- Haupt-Typen für Berechtigungen --- + +/// Ein typsicherer Container, der die spezifische Aktion für einen Ressourcentyp enthält. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[ts(export)] +pub enum Action { + Database(DbAction), + Filesystem(FsAction), + Http(HttpAction), + Shell(ShellAction), +} + +/// Die interne Repräsentation einer einzelnen, gewährten Berechtigung. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ExtensionPermission { pub id: String, @@ -18,62 +163,15 @@ pub struct ExtensionPermission { #[serde(skip_serializing_if = "Option::is_none")] pub constraints: Option, pub status: PermissionStatus, - - // CRDT Felder #[serde(skip_serializing_if = "Option::is_none")] pub haex_tombstone: Option, #[serde(skip_serializing_if = "Option::is_none")] pub haex_timestamp: Option, } -impl From for ExtensionPermission { - fn from(db_perm: HaexExtensionPermissions) -> Self { - let resource_type = ResourceType::from_str(&db_perm.resource_type.unwrap_or_default()) - .unwrap_or(ResourceType::Db); // Fallback - - let constraints = db_perm - .constraints - .and_then(|json_str| parse_constraints(&resource_type, &json_str).ok()); - - ExtensionPermission { - id: db_perm.id, - extension_id: db_perm.extension_id.unwrap_or_default(), - resource_type, - action: Action::from_str(&db_perm.action.unwrap_or_default()).unwrap_or(Action::Read), - target: db_perm.target.unwrap_or_default(), - status: PermissionStatus::from_str(&db_perm.status).unwrap_or(PermissionStatus::Ask), - constraints, - haex_timestamp: db_perm.haex_timestamp, - haex_tombstone: db_perm.haex_tombstone, - } - } -} - -impl From<&ExtensionPermission> for HaexExtensionPermissions { - fn from(perm: &ExtensionPermission) -> Self { - let constraints_json = perm - .constraints - .as_ref() - .and_then(|c| serde_json::to_string(c).ok()); - - HaexExtensionPermissions { - id: perm.id.clone(), - extension_id: Some(perm.extension_id.clone()), - resource_type: Some(format!("{:?}", perm.resource_type).to_lowercase()), - action: Some(format!("{:?}", perm.action).to_lowercase()), - target: Some(perm.target.clone()), - constraints: constraints_json, - status: perm.status.as_str().to_string(), - created_at: None, // Wird von der DB gesetzt - updated_at: None, // Wird von der DB gesetzt - haex_timestamp: perm.haex_timestamp.clone(), - haex_tombstone: perm.haex_tombstone, - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] #[serde(rename_all = "lowercase")] +#[ts(export)] pub enum ResourceType { Fs, Http, @@ -81,49 +179,140 @@ pub enum ResourceType { Shell, } -impl FromStr for ResourceType { - type Err = DatabaseError; +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum PermissionStatus { + Ask, + Granted, + Denied, +} - fn from_str(s: &str) -> Result { +// --- Constraint-Typen (unverändert) --- + +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +#[serde(untagged)] +#[ts(export)] +pub enum PermissionConstraints { + Database(DbConstraints), + Filesystem(FsConstraints), + Http(HttpConstraints), + Shell(ShellConstraints), +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct DbConstraints { + #[serde(skip_serializing_if = "Option::is_none")] + pub where_clause: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub columns: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct FsConstraints { + #[serde(skip_serializing_if = "Option::is_none")] + pub max_file_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_extensions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub recursive: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct HttpConstraints { + #[serde(skip_serializing_if = "Option::is_none")] + pub methods: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub rate_limit: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, TS)] +#[ts(export)] +pub struct RateLimit { + pub requests: u32, + pub per_minutes: u32, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default, TS)] +#[ts(export)] +pub struct ShellConstraints { + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_subcommands: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_flags: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub forbidden_args: Option>, +} + +// --- Konvertierungen zwischen ExtensionPermission und HaexExtensionPermissions --- + +impl ResourceType { + pub fn as_str(&self) -> &str { + match self { + ResourceType::Fs => "fs", + ResourceType::Http => "http", + ResourceType::Db => "db", + ResourceType::Shell => "shell", + } + } + + pub fn from_str(s: &str) -> Result { match s { "fs" => Ok(ResourceType::Fs), "http" => Ok(ResourceType::Http), "db" => Ok(ResourceType::Db), "shell" => Ok(ResourceType::Shell), - _ => Err(DatabaseError::SerializationError { - reason: format!("Unbekannter Ressourcentyp: {}", s), + _ => Err(ExtensionError::ValidationError { + reason: format!("Unknown resource type: {}", s), }), } } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Action { - Read, - Write, -} - -impl FromStr for Action { - type Err = DatabaseError; - - fn from_str(s: &str) -> Result { - match s { - "read" => Ok(Action::Read), - "write" => Ok(Action::Write), - _ => Err(DatabaseError::SerializationError { - reason: format!("Unbekannte Aktion: {}", s), - }), +impl Action { + pub fn as_str(&self) -> String { + match self { + Action::Database(action) => serde_json::to_string(action) + .unwrap_or_default() + .trim_matches('"') + .to_string(), + Action::Filesystem(action) => serde_json::to_string(action) + .unwrap_or_default() + .trim_matches('"') + .to_string(), + Action::Http(action) => serde_json::to_string(action) + .unwrap_or_default() + .trim_matches('"') + .to_string(), + Action::Shell(action) => serde_json::to_string(action) + .unwrap_or_default() + .trim_matches('"') + .to_string(), } } -} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum PermissionStatus { - Ask, - Granted, - Denied, + pub fn from_str(resource_type: &ResourceType, s: &str) -> Result { + match resource_type { + ResourceType::Db => Ok(Action::Database(DbAction::from_str(s)?)), + ResourceType::Fs => Ok(Action::Filesystem(FsAction::from_str(s)?)), + ResourceType::Http => { + let action: HttpAction = + serde_json::from_str(&format!("\"{}\"", s)).map_err(|_| { + ExtensionError::InvalidActionString { + input: s.to_string(), + resource_type: "http".to_string(), + } + })?; + Ok(Action::Http(action)) + } + ResourceType::Shell => Ok(Action::Shell(ShellAction::from_str(s)?)), + } + } } impl PermissionStatus { @@ -135,140 +324,71 @@ impl PermissionStatus { } } - pub fn from_str(s: &str) -> Result { + pub fn from_str(s: &str) -> Result { match s { "ask" => Ok(PermissionStatus::Ask), "granted" => Ok(PermissionStatus::Granted), "denied" => Ok(PermissionStatus::Denied), - _ => Err(DatabaseError::SerializationError { + _ => Err(ExtensionError::ValidationError { 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, - #[serde(skip_serializing_if = "Option::is_none")] - pub columns: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct FsConstraints { - #[serde(skip_serializing_if = "Option::is_none")] - pub max_file_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub allowed_extensions: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub recursive: Option, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct HttpConstraints { - #[serde(skip_serializing_if = "Option::is_none")] - pub methods: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub rate_limit: Option, -} - -#[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>, - #[serde(skip_serializing_if = "Option::is_none")] - pub allowed_flags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub forbidden_args: Option>, -} - -// Wenn du weiterhin gruppierte Permissions brauchst: -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct EditablePermissions { - pub permissions: Vec, -} - -// 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() - } -} */ - -pub fn parse_constraints( - resource_type: &ResourceType, - json: &str, -) -> Result { - 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)) +impl From<&ExtensionPermission> for crate::database::generated::HaexExtensionPermissions { + fn from(perm: &ExtensionPermission) -> Self { + Self { + id: perm.id.clone(), + extension_id: Some(perm.extension_id.clone()), + resource_type: Some(perm.resource_type.as_str().to_string()), + action: Some(perm.action.as_str()), + target: Some(perm.target.clone()), + constraints: perm + .constraints + .as_ref() + .and_then(|c| serde_json::to_string(c).ok()), + status: perm.status.as_str().to_string(), + created_at: None, + updated_at: None, + haex_tombstone: perm.haex_tombstone, + haex_timestamp: perm.haex_timestamp.clone(), + } + } +} + +impl From for ExtensionPermission { + fn from(db_perm: crate::database::generated::HaexExtensionPermissions) -> Self { + let resource_type = db_perm + .resource_type + .as_deref() + .and_then(|s| ResourceType::from_str(s).ok()) + .unwrap_or(ResourceType::Db); + + let action = db_perm + .action + .as_deref() + .and_then(|s| Action::from_str(&resource_type, s).ok()) + .unwrap_or(Action::Database(DbAction::Read)); + + let status = + PermissionStatus::from_str(db_perm.status.as_str()).unwrap_or(PermissionStatus::Denied); + + let constraints = db_perm + .constraints + .as_deref() + .and_then(|s| serde_json::from_str(s).ok()); + + Self { + id: db_perm.id, + extension_id: db_perm.extension_id.unwrap_or_default(), + resource_type, + action, + target: db_perm.target.unwrap_or_default(), + constraints, + status, + haex_tombstone: db_perm.haex_tombstone, + haex_timestamp: db_perm.haex_timestamp, } } } diff --git a/src-tauri/src/extension/permissions/validator.rs b/src-tauri/src/extension/permissions/validator.rs index bba9dc6..e2fd737 100644 --- a/src-tauri/src/extension/permissions/validator.rs +++ b/src-tauri/src/extension/permissions/validator.rs @@ -54,7 +54,7 @@ impl SqlPermissionValidator { PermissionManager::check_database_permission( app_state, extension_id, - Action::Read, + Action::Database(super::types::DbAction::Read), &table_name, ) .await?; @@ -75,7 +75,7 @@ impl SqlPermissionValidator { PermissionManager::check_database_permission( app_state, extension_id, - Action::Write, + Action::Database(super::types::DbAction::ReadWrite), &table_name, ) .await?; @@ -97,7 +97,7 @@ impl SqlPermissionValidator { PermissionManager::check_database_permission( app_state, extension_id, - Action::Write, + Action::Database(super::types::DbAction::Create), &table_name, ) .await?; @@ -119,7 +119,7 @@ impl SqlPermissionValidator { PermissionManager::check_database_permission( app_state, extension_id, - Action::Write, + Action::Database(super::types::DbAction::AlterDrop), &table_name, ) .await?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fcfde1e..cd0cde1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,7 @@ mod crdt; mod database; mod extension; -use crate::{ - crdt::hlc::HlcService, - database::DbConnection, - extension::core::{ExtensionManager, ExtensionState}, -}; +use crate::{crdt::hlc::HlcService, database::DbConnection, extension::core::ExtensionManager}; use std::sync::{Arc, Mutex}; use tauri::Manager; @@ -60,7 +56,7 @@ pub fn run() { hlc: Mutex::new(HlcService::new()), extension_manager: ExtensionManager::new(), }) - .manage(ExtensionState::default()) + //.manage(ExtensionState::default()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_http::init()) diff --git a/src/components/haex/extension/dialog/install.vue b/src/components/haex/extension/dialog/install.vue index d4ed902..ed5c2a8 100644 --- a/src/components/haex/extension/dialog/install.vue +++ b/src/components/haex/extension/dialog/install.vue @@ -5,166 +5,249 @@ @confirm="onConfirm" > -
- + - -{ - "de": { - "title": "Erweiterung hinzufügen", - "question": "Erweiterung {extension} hinzufügen?", - "confirm": "Bestätigen", - "deny": "Ablehnen", - "database": "Datenbank", - "http": "Internet", - "filesystem": "Dateisystem" - }, - "en": { - "title": "Confirm Permission", - "question": "Add Extension {extension}?", - "confirm": "Confirm", - "deny": "Deny", - "database": "Database", - "http": "Internet", - "filesystem": "Filesystem" - } -} + +de: + title: Erweiterung installieren + version: Version + author: Autor + signature: + valid: Signatur verifiziert + invalid: Signatur ungültig + permissions: + title: Berechtigungen + database: Datenbank + filesystem: Dateisystem + http: Internet + shell: Terminal + +en: + title: Install Extension + version: Version + author: Author + signature: + valid: Signature verified + invalid: Invalid signature + permissions: + title: Permissions + database: Database + filesystem: Filesystem + http: Internet + shell: Terminal diff --git a/src/components/haex/extension/permission-item.vue b/src/components/haex/extension/permission-item.vue new file mode 100644 index 0000000..20d1db9 --- /dev/null +++ b/src/components/haex/extension/permission-item.vue @@ -0,0 +1,128 @@ + + + + + +de: + status: + granted: Erlaubt + ask: Nachfragen + denied: Verweigert + operation: + read: Lesen + write: Schreiben + readWrite: Lesen & Schreiben + request: Anfrage + execute: Ausführen +en: + status: + granted: Granted + ask: Ask + denied: Denied + operation: + read: Read + write: Write + readWrite: Read & Write + request: Request + execute: Execute + diff --git a/src/components/haex/extension/permission-list.vue b/src/components/haex/extension/permission-list.vue new file mode 100644 index 0000000..58f7f37 --- /dev/null +++ b/src/components/haex/extension/permission-list.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/ui/input/index.vue b/src/components/ui/input/index.vue index 34a1ac8..978a016 100644 --- a/src/components/ui/input/index.vue +++ b/src/components/ui/input/index.vue @@ -84,8 +84,6 @@ const filteredSlots = computed(() => { ) }) -watchImmediate(props, () => console.log('props', props)) - const { isSmallScreen } = storeToRefs(useUiStore()) diff --git a/src/pages/vault/[vaultId]/extensions/index.vue b/src/pages/vault/[vaultId]/extensions/index.vue index 8f6792b..c80145a 100644 --- a/src/pages/vault/[vaultId]/extensions/index.vue +++ b/src/pages/vault/[vaultId]/extensions/index.vue @@ -1,12 +1,12 @@