From fb577a8699e39fcdc0a14e16c73e4709b41ce5b1 Mon Sep 17 00:00:00 2001 From: haex Date: Thu, 2 Oct 2025 01:42:30 +0200 Subject: [PATCH] refactore manifest and permission --- nuxt.config.ts | 4 +- src-tauri/Cargo.lock | 479 +++++++-- src-tauri/Cargo.toml | 37 +- src-tauri/capabilities/default.json | 1 - src-tauri/database/index.ts | 2 +- .../migrations/0012_special_gwen_stacy.sql | 15 + .../migrations/meta/0012_snapshot.json | 900 ++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + src-tauri/database/schemas/haex.ts | 76 ++ src-tauri/database/schemas/vault.ts | 49 - src-tauri/database/vault.db | Bin 131072 -> 131072 bytes src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/gen/schemas/desktop-schema.json | 6 - src-tauri/gen/schemas/linux-schema.json | 6 - src-tauri/src/crdt/hlc.rs | 4 +- src-tauri/src/crdt/transformer.rs | 1 + src-tauri/src/crdt/trigger.rs | 3 +- src-tauri/src/database/core.rs | 4 +- src-tauri/src/extension/core.rs | 528 ---------- src-tauri/src/extension/core/manager.rs | 311 ++++++ src-tauri/src/extension/core/manifest.rs | 250 +++++ src-tauri/src/extension/core/mod.rs | 10 + src-tauri/src/extension/core/protocol.rs | 252 +++++ src-tauri/src/extension/core/types.rs | 94 ++ src-tauri/src/extension/core_old.rs | 973 ++++++++++++++++++ src-tauri/src/extension/crypto.rs | 74 ++ src-tauri/src/extension/database/executor.rs | 153 +++ src-tauri/src/extension/database/mod.rs | 9 +- .../src/extension/database/permissions.rs | 278 ----- src-tauri/src/extension/error.rs | 189 ++-- src-tauri/src/extension/mod.rs | 177 +++- src-tauri/src/extension/permission_manager.rs | 297 ------ .../src/extension/permissions/manager.rs | 650 ++++++++++++ src-tauri/src/extension/permissions/mod.rs | 3 + src-tauri/src/extension/permissions/types.rs | 156 +++ .../src/extension/permissions/validator.rs | 201 ++++ src-tauri/src/lib.rs | 80 +- src/components/haex/extension/card.vue | 18 +- .../manifest/permissions/database.vue | 122 ++- .../manifest/permissions/filesystem.vue | 64 +- .../extension/manifest/permissions/http.vue | 40 +- src/composables/extensionContextBroadcast.ts | 14 +- src/composables/extensionMessageHandler.ts | 373 +++---- src/pages/vault.vue | 6 +- .../[vaultId]/extensions/[extensionId].vue | 163 ++- .../vault/[vaultId]/extensions/index.vue | 27 +- src/pages/vault/[vaultId]/index.vue | 4 +- src/stores/extensions/index.ts | 452 ++++---- src/stores/extensions/tabs.ts | 143 +++ src/types/haexhub.d.ts | 11 +- 51 files changed, 5634 insertions(+), 2086 deletions(-) create mode 100644 src-tauri/database/migrations/0012_special_gwen_stacy.sql create mode 100644 src-tauri/database/migrations/meta/0012_snapshot.json create mode 100644 src-tauri/database/schemas/haex.ts delete mode 100644 src-tauri/src/extension/core.rs create mode 100644 src-tauri/src/extension/core/manager.rs create mode 100644 src-tauri/src/extension/core/manifest.rs create mode 100644 src-tauri/src/extension/core/mod.rs create mode 100644 src-tauri/src/extension/core/protocol.rs create mode 100644 src-tauri/src/extension/core/types.rs create mode 100644 src-tauri/src/extension/core_old.rs create mode 100644 src-tauri/src/extension/crypto.rs create mode 100644 src-tauri/src/extension/database/executor.rs delete mode 100644 src-tauri/src/extension/database/permissions.rs delete mode 100644 src-tauri/src/extension/permission_manager.rs create mode 100644 src-tauri/src/extension/permissions/manager.rs create mode 100644 src-tauri/src/extension/permissions/mod.rs create mode 100644 src-tauri/src/extension/permissions/types.rs create mode 100644 src-tauri/src/extension/permissions/validator.rs create mode 100644 src/stores/extensions/tabs.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 4953280..babc32a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,4 +1,4 @@ -import tailwindcss from '@tailwindcss/vite' +//import tailwindcss from '@tailwindcss/vite' // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ @@ -99,7 +99,7 @@ export default defineNuxtConfig({ }, vite: { - plugins: [tailwindcss()], + //plugins: [tailwindcss()], // Better support for Tauri CLI output clearScreen: false, // Enable environment variables diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 759a0f2..609aa93 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -62,6 +73,15 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -274,6 +294,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + [[package]] name = "bincode" version = "1.3.3" @@ -386,6 +412,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -440,7 +475,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -459,6 +494,8 @@ version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -514,6 +551,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -533,6 +580,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -627,6 +686,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -698,6 +772,33 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "darling" version = "0.20.10" @@ -739,6 +840,22 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -749,6 +866,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "derive_more" version = "0.99.19" @@ -770,6 +898,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -890,6 +1019,30 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "embed-resource" version = "3.0.2" @@ -1020,6 +1173,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -1032,11 +1191,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1081,9 +1241,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1533,6 +1693,7 @@ name = "haex-hub" version = "0.1.0" dependencies = [ "base64 0.22.1", + "ed25519-dalek", "fs_extra", "hex", "mime", @@ -1540,10 +1701,10 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "sha2", "sqlparser", "tauri", "tauri-build", - "tauri-plugin-android-fs", "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-http", @@ -1552,11 +1713,12 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-persisted-scope", "tauri-plugin-store", - "thiserror 2.0.16", - "tokio", + "thiserror 2.0.17", "ts-rs", "uhlc", + "url", "uuid", + "zip", ] [[package]] @@ -1607,6 +1769,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1887,9 +2058,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1937,6 +2108,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "io-uring" version = "0.7.8" @@ -2024,6 +2204,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -2109,6 +2299,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.175" @@ -2147,6 +2343,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2187,6 +2392,16 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + [[package]] name = "mac" version = "0.1.1" @@ -2305,7 +2520,7 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows-sys 0.59.0", ] @@ -2775,10 +2990,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -2937,6 +3162,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2990,6 +3225,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3129,7 +3370,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.5.8", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3149,7 +3390,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3338,7 +3579,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3646,9 +3887,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -3667,18 +3908,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3813,10 +4054,21 @@ dependencies = [ ] [[package]] -name = "sha2" -version = "0.10.8" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3838,6 +4090,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3946,10 +4207,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" [[package]] -name = "sqlparser" -version = "0.58.0" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4b661c54b1e4b603b37873a18c59920e4c51ea8ea2cf527d925424dbd4437c" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlparser" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" dependencies = [ "log", "recursive", @@ -4224,7 +4495,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tray-icon", "url", @@ -4277,7 +4548,7 @@ dependencies = [ "sha2", "syn 2.0.100", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "url", "uuid", @@ -4315,21 +4586,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-plugin-android-fs" -version = "12.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1387b55109ae9b8ad0521ac11f8ce827740f53c0e0ce74648d1cb2efe0fd9c09" -dependencies = [ - "percent-encoding", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.16", -] - [[package]] name = "tauri-plugin-dialog" version = "2.4.0" @@ -4344,7 +4600,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", ] @@ -4365,7 +4621,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml 0.9.5", "url", ] @@ -4388,7 +4644,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "url", "urlpattern", @@ -4408,7 +4664,7 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "url", ] @@ -4429,7 +4685,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "windows", "zbus", @@ -4450,7 +4706,7 @@ dependencies = [ "sys-locale", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4466,7 +4722,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4480,7 +4736,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -4503,7 +4759,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webview2-com", @@ -4567,7 +4823,7 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.16", + "thiserror 2.0.17", "toml 0.9.5", "url", "urlpattern", @@ -4592,7 +4848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows", "windows-version", ] @@ -4641,11 +4897,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -4661,9 +4917,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -4951,7 +5207,7 @@ dependencies = [ "once_cell", "png", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows-sys 0.59.0", ] @@ -4967,7 +5223,7 @@ version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "ts-rs-macros", ] @@ -5087,9 +5343,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -5405,7 +5661,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "windows", "windows-core 0.61.0", ] @@ -5965,7 +6221,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6128,6 +6384,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] [[package]] name = "zerovec" @@ -6151,6 +6421,79 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zip" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.2", + "hmac", + "indexmap 2.8.0", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.7.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 290fa46..ddb382d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,37 +18,38 @@ crate-type = ["staticlib", "cdylib", "rlib"] serde_json = "1.0.145" tauri-build = { version = "2.2", features = [] } -serde = { version = "1.0.226", features = ["derive"] } +serde = { version = "1.0.228", features = ["derive"] } [dependencies] rusqlite = { version = "0.37.0", features = [ "load_extension", "bundled-sqlcipher-vendored-openssl", "functions", ] } -#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] } + +#tauri-plugin-sql = { version = "2", features = ["sqlite"] }tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }#libsqlite3-sys = { version = "0.31", features = ["bundled-sqlcipher"] } #sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] } -tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } -serde = { version = "1", features = ["derive"] } -hex = "0.4" -serde_json = "1.0.143" base64 = "0.22" -mime_guess = "2.0" -mime = "0.3" +ed25519-dalek = "2.1" fs_extra = "1.3.0" -sqlparser = { version = "0.58.0", features = ["visitor"] } -uhlc = "0.8" +hex = "0.4" +mime = "0.3" +mime_guess = "2.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.143" +sha2 = "0.10.9" +sqlparser = { version = "0.59.0", features = ["visitor"] } tauri = { version = "2.8.5", features = ["protocol-asset", "devtools"] } tauri-plugin-dialog = "2.4.0" tauri-plugin-fs = "2.4.0" -tauri-plugin-opener = "2.5.0" -tauri-plugin-os = "2.3" -tauri-plugin-store = "2.4.0" tauri-plugin-http = "2.5.2" tauri-plugin-notification = "2.3.1" +tauri-plugin-opener = "2.5.0" +tauri-plugin-os = "2.3" tauri-plugin-persisted-scope = "2.3.2" -tauri-plugin-android-fs = "12.0.1" -uuid = { version = "1.18.1", features = ["v4"] } +tauri-plugin-store = "2.4.0" +thiserror = "2.0.17" ts-rs = "11.0.1" -thiserror = "2.0.16" - -#tauri-plugin-sql = { version = "2", features = ["sqlite"] } +uhlc = "0.8" +uuid = { version = "1.18.1", features = ["v4"] } +zip = "5.1.1" +url = "2.5.7" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2df7842..cb47365 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -25,7 +25,6 @@ "fs:allow-download-read-recursive", "fs:allow-download-write-recursive", "fs:default", - "android-fs:default", { "identifier": "fs:scope", "allow": [{ "path": "**" }] diff --git a/src-tauri/database/index.ts b/src-tauri/database/index.ts index 3414ad1..26550f6 100644 --- a/src-tauri/database/index.ts +++ b/src-tauri/database/index.ts @@ -1,5 +1,5 @@ import { drizzle } from 'drizzle-orm/sqlite-proxy' // Adapter für Query Building ohne direkte Verbindung -import * as schema from './schemas/vault' // Importiere alles aus deiner Schema-Datei +import * as schema from './schemas/haex' // Importiere alles aus deiner Schema-Datei // sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument. // Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden. diff --git a/src-tauri/database/migrations/0012_special_gwen_stacy.sql b/src-tauri/database/migrations/0012_special_gwen_stacy.sql new file mode 100644 index 0000000..5a78073 --- /dev/null +++ b/src-tauri/database/migrations/0012_special_gwen_stacy.sql @@ -0,0 +1,15 @@ +ALTER TABLE `haex_extension_permissions` RENAME COLUMN "resource" TO "resource_type";--> statement-breakpoint +ALTER TABLE `haex_extension_permissions` RENAME COLUMN "operation" TO "action";--> statement-breakpoint +ALTER TABLE `haex_extension_permissions` RENAME COLUMN "path" TO "target";--> statement-breakpoint +DROP INDEX `haex_extension_permissions_extension_id_resource_operation_path_unique`;--> statement-breakpoint +ALTER TABLE `haex_extension_permissions` ADD `constraints` text;--> statement-breakpoint +ALTER TABLE `haex_extension_permissions` ADD `status` text DEFAULT 'denied' NOT NULL;--> statement-breakpoint +ALTER TABLE `haex_extension_permissions` ADD `haex_timestamp` text;--> statement-breakpoint +CREATE UNIQUE INDEX `haex_extension_permissions_extension_id_resource_type_action_target_unique` ON `haex_extension_permissions` (`extension_id`,`resource_type`,`action`,`target`);--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `description` text;--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `entry` text;--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `homepage` text;--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `public_key` text;--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `signature` text;--> statement-breakpoint +ALTER TABLE `haex_extensions` ADD `haex_timestamp` text;--> statement-breakpoint +ALTER TABLE `haex_settings` ADD `haex_timestamp` text; \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/0012_snapshot.json b/src-tauri/database/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..c580a7a --- /dev/null +++ b/src-tauri/database/migrations/meta/0012_snapshot.json @@ -0,0 +1,900 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e4f45178-afcf-46e9-9a07-79b7bca88d88", + "prevId": "c8c0825d-c435-4a42-986a-a4f70e7f9e8b", + "tables": { + "haex_crdt_configs": { + "name": "haex_crdt_configs", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_crdt_logs": { + "name": "haex_crdt_logs", + "columns": { + "hlc_timestamp": { + "name": "hlc_timestamp", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "table_name": { + "name": "table_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "row_pks": { + "name": "row_pks", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "op_type": { + "name": "op_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_name": { + "name": "column_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_crdt_snapshots": { + "name": "haex_crdt_snapshots", + "columns": { + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created": { + "name": "created", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "epoch_hlc": { + "name": "epoch_hlc", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_url": { + "name": "location_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_size_bytes": { + "name": "file_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_extension_permissions": { + "name": "haex_extension_permissions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "extension_id": { + "name": "extension_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "constraints": { + "name": "constraints", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'denied'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "haex_extension_permissions_extension_id_resource_type_action_target_unique": { + "name": "haex_extension_permissions_extension_id_resource_type_action_target_unique", + "columns": [ + "extension_id", + "resource_type", + "action", + "target" + ], + "isUnique": true + } + }, + "foreignKeys": { + "haex_extension_permissions_extension_id_haex_extensions_id_fk": { + "name": "haex_extension_permissions_extension_id_haex_extensions_id_fk", + "tableFrom": "haex_extension_permissions", + "tableTo": "haex_extensions", + "columnsFrom": [ + "extension_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_extensions": { + "name": "haex_extensions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entry": { + "name": "entry", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "homepage": { + "name": "homepage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_settings": { + "name": "haex_settings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_timestamp": { + "name": "haex_timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_notifications": { + "name": "haex_notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read": { + "name": "read", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_group_items": { + "name": "haex_passwords_group_items", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk": { + "name": "haex_passwords_group_items_group_id_haex_passwords_groups_id_fk", + "tableFrom": "haex_passwords_group_items", + "tableTo": "haex_passwords_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_group_items_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_group_items", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "haex_passwords_group_items_item_id_group_id_pk": { + "columns": [ + "item_id", + "group_id" + ], + "name": "haex_passwords_group_items_item_id_group_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_groups": { + "name": "haex_passwords_groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk": { + "name": "haex_passwords_groups_parent_id_haex_passwords_groups_id_fk", + "tableFrom": "haex_passwords_groups", + "tableTo": "haex_passwords_groups", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_details": { + "name": "haex_passwords_item_details", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_history": { + "name": "haex_passwords_item_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "changed_property": { + "name": "changed_property", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "old_value": { + "name": "old_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "new_value": { + "name": "new_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_item_history_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_item_history", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "haex_passwords_item_key_values": { + "name": "haex_passwords_item_key_values", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "haex_tombstone": { + "name": "haex_tombstone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk": { + "name": "haex_passwords_item_key_values_item_id_haex_passwords_item_details_id_fk", + "tableFrom": "haex_passwords_item_key_values", + "tableTo": "haex_passwords_item_details", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"haex_extension_permissions\".\"resource\"": "\"haex_extension_permissions\".\"resource_type\"", + "\"haex_extension_permissions\".\"operation\"": "\"haex_extension_permissions\".\"action\"", + "\"haex_extension_permissions\".\"path\"": "\"haex_extension_permissions\".\"target\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src-tauri/database/migrations/meta/_journal.json b/src-tauri/database/migrations/meta/_journal.json index a77ec49..116cd06 100644 --- a/src-tauri/database/migrations/meta/_journal.json +++ b/src-tauri/database/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1757968140525, "tag": "0011_illegal_thor_girl", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1759362109283, + "tag": "0012_special_gwen_stacy", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src-tauri/database/schemas/haex.ts b/src-tauri/database/schemas/haex.ts new file mode 100644 index 0000000..2dd11aa --- /dev/null +++ b/src-tauri/database/schemas/haex.ts @@ -0,0 +1,76 @@ +import { sql } from 'drizzle-orm' +import { + integer, + sqliteTable, + text, + unique, + type AnySQLiteColumn, +} from 'drizzle-orm/sqlite-core' +import tableNames from '../tableNames.json' + +export const haexSettings = sqliteTable(tableNames.haex.settings, { + id: text().primaryKey(), + key: text(), + type: text(), + value: text(), + haex_tombstone: integer({ mode: 'boolean' }), + haex_timestamp: text(), +}) +export type InsertHaexSettings = typeof haexSettings.$inferInsert +export type SelectHaexSettings = typeof haexSettings.$inferSelect + +export const haexExtensions = sqliteTable(tableNames.haex.extensions, { + id: text().primaryKey(), + author: text(), + description: text(), + entry: text(), + homepage: text(), + enabled: integer({ mode: 'boolean' }), + icon: text(), + name: text(), + public_key: text(), + signature: text(), + url: text(), + version: text(), + haex_tombstone: integer({ mode: 'boolean' }), + haex_timestamp: text(), +}) +export type InsertHaexExtensions = typeof haexExtensions.$inferInsert +export type SelectHaexExtensions = typeof haexExtensions.$inferSelect + +export const haexExtensionPermissions = sqliteTable( + tableNames.haex.extension_permissions, + { + id: text().primaryKey(), + extensionId: text('extension_id').references( + (): AnySQLiteColumn => haexExtensions.id, + ), + resourceType: text('resource_type', { + enum: ['fs', 'http', 'db', 'shell'], + }), + action: text({ enum: ['read', 'write'] }), + target: text(), + constraints: text({ mode: 'json' }), + status: text({ enum: ['ask', 'granted', 'denied'] }) + .notNull() + .default('denied'), + createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`), + updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate( + () => new Date(), + ), + haexTombstone: integer('haex_tombstone', { mode: 'boolean' }), + haexTimestamp: text('haex_timestamp'), + }, + (table) => [ + unique().on( + table.extensionId, + table.resourceType, + table.action, + table.target, + ), + ], +) +export type InserthaexExtensionPermissions = + typeof haexExtensionPermissions.$inferInsert +export type SelecthaexExtensionPermissions = + typeof haexExtensionPermissions.$inferSelect diff --git a/src-tauri/database/schemas/vault.ts b/src-tauri/database/schemas/vault.ts index bf2f301..4971aed 100644 --- a/src-tauri/database/schemas/vault.ts +++ b/src-tauri/database/schemas/vault.ts @@ -4,59 +4,10 @@ import { primaryKey, sqliteTable, text, - unique, type AnySQLiteColumn, } from 'drizzle-orm/sqlite-core' import tableNames from '../tableNames.json' -export const haexSettings = sqliteTable(tableNames.haex.settings, { - id: text().primaryKey(), - key: text(), - type: text(), - value: text(), - haex_tombstone: integer({ mode: 'boolean' }), -}) -export type InsertHaexSettings = typeof haexSettings.$inferInsert -export type SelectHaexSettings = typeof haexSettings.$inferSelect - -export const haexExtensions = sqliteTable(tableNames.haex.extensions, { - id: text().primaryKey(), - author: text(), - enabled: integer({ mode: 'boolean' }), - icon: text(), - name: text(), - url: text(), - version: text(), - haex_tombstone: integer({ mode: 'boolean' }), -}) -export type InsertHaexExtensions = typeof haexExtensions.$inferInsert -export type SelectHaexExtensions = typeof haexExtensions.$inferSelect - -export const haexExtensionPermissions = sqliteTable( - tableNames.haex.extension_permissions, - { - id: text().primaryKey(), - extensionId: text('extension_id').references( - (): AnySQLiteColumn => haexExtensions.id, - ), - resource: text({ enum: ['fs', 'http', 'db', 'shell'] }), - operation: text({ enum: ['read', 'write', 'create'] }), - path: text(), - createdAt: text('created_at').default(sql`(CURRENT_TIMESTAMP)`), - updateAt: integer('updated_at', { mode: 'timestamp' }).$onUpdate( - () => new Date(), - ), - haex_tombstone: integer({ mode: 'boolean' }), - }, - (table) => [ - unique().on(table.extensionId, table.resource, table.operation, table.path), - ], -) -export type InserthaexExtensionPermissions = - typeof haexExtensionPermissions.$inferInsert -export type SelecthaexExtensionPermissions = - typeof haexExtensionPermissions.$inferSelect - export const haexNotifications = sqliteTable(tableNames.haex.notifications, { id: text().primaryKey(), alt: text(), diff --git a/src-tauri/database/vault.db b/src-tauri/database/vault.db index 3c09876cb9b5d0e7a3bc9684eefddb82304ae5cc..1200657c02b19e6964aef39db98929def9d7bf24 100644 GIT binary patch delta 953 zcmb_bT}V@57(TB@x4E5jJ}2bSG&@^ALliu7Zc_?vk%s>lDr8wZ$8$cnnQnG=B!8IB z&G6P(@D)LXbyozoi`_&>;7tS)bWt~T5m8-q6G%4^ow-mbFS~f*eP7<^=S*WNA)Wnw=9k|dBvtNEPGqMZl%>LwkoZP=<&P#O{!nS{uZ~--z=+Q zvnr2!yzX(?qx#^^x@$nmsx|lNS{7|m=qLJtzN2sG3;KxOqj%^H+D0$Y7TU}@gnOu@ zn5KYDZ#Cw&8sW5PHzz2T9;ML*gy>O@Hh<2QOVCYQQ-idbudj!d%b8y81?dXIVbU$b zAuLEZs%vwSpyOG+L6E|+DLgHQ@ZMs2W^6K|1h3-#n|LG?mGv2I|0pcu*`OYo!f{=m znl51LQjrQX-_->xeVKN;k{rcg%Y9JcUXiu*7B#WU#%8dQtjlLCX5Qt3RC~tG%#+Fq za8>209Q0@%c@&1PR_g-wco$;NLvjYhWHK>ku7rDQ#BmBp@EXLpw99Gc0|B@(qRV5G zq%8r}B)terjZO{6V>6n9gZkVwb{522OP#W!M`F?aS(mjC)}7?}b?{{xOci8JgH=<9 z!^DqxGOvq|oE$KQJWp5BK)|V|u2s zlqb(LxMUhUX5vGEyyna{TS^iMU`g8|)D#;(WszK2g!!`6iM&>ap*6mz)m@%9I9TvC rSg@0{Bp`gy{z(GPR_!JtHAn@B(G);Z-MT0|P1?i3TdgB#4JUTG+--LIt4{7nxfal2u9L46jPf^ z>TcyHJV6luPC})<;npr4Dup7rxR^l&J2)tai-?z196mgF-{Jdt&v7KjkyZ~F3deDl zkK_~Z8Gmpj2g>@~ON^_k&7q5BjOjaiOFyQ4(H?1M)j#UGYP(tMlEJ1{o^fxqO!=N%MH@VYh zDh2J!%usW3Q4q{Z0k~?7USEejxjib0y|`o0_X<4o@;9Ix67)-YNZoU@;S)fmI^3dG z9gs!fCVgIltE4YNm=X<`6-vW`*_dJP_yMZbAqm;uutaMzDVP^?{yyP7Zorf%`|p$f zqn?Mrxd>a+lI%XO(?l60x?P1K8Y;tNxDz`pneA8sI@hW-dw!9u1sJ4m6Gm89?Wc(r z6v=5ql+9}UXtM<=Mp{%T`bw2TubS;@eROmIR64l~34UexkVsdS;SO^`QFa`r{yFA( R#@cX{%nN1Ja+S Result { @@ -376,6 +375,7 @@ fn extract_tables_from_set_expr_recursive(set_expr: &SetExpr, tables: &mut Vec {} } } diff --git a/src-tauri/src/extension/core.rs b/src-tauri/src/extension/core.rs deleted file mode 100644 index 55c94ea..0000000 --- a/src-tauri/src/extension/core.rs +++ /dev/null @@ -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, - pub entry: String, - pub icon: Option, - pub permissions: ExtensionPermissions, - pub homepage: Option, - pub description: Option, -} - -#[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, - pub namespace: Option, - 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::()) -} -/// 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, - pub cached_at: SystemTime, - pub ttl: Duration, -} - -/// Enhanced extension manager -#[derive(Default)] -pub struct ExtensionManager { - pub production_extensions: Mutex>, - pub dev_extensions: Mutex>, - pub permission_cache: Mutex>, -} - -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 { - // 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>, -} - -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 { - 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 for DataProcessingError { - fn from(err: hex::FromHexError) -> Self { - DataProcessingError::HexDecoding(err) - } -} - -impl From for DataProcessingError { - fn from(err: std::string::FromUtf8Error) -> Self { - DataProcessingError::Utf8Conversion(err) - } -} - -impl From 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( - app_handle: &AppHandle, - extension_id: &str, - extension_version: &str, - requested_asset_path: &str, -) -> Result { - // 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::(); - - 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( - context: &UriSchemeContext<'_, R>, - request: &Request>, -) -> Result>, Box> { - 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 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 { - // 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) -} diff --git a/src-tauri/src/extension/core/manager.rs b/src-tauri/src/extension/core/manager.rs new file mode 100644 index 0000000..923f068 --- /dev/null +++ b/src-tauri/src/extension/core/manager.rs @@ -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, + pub cached_at: SystemTime, + pub ttl: Duration, +} + +#[derive(Default)] +pub struct ExtensionManager { + pub production_extensions: Mutex>, + pub dev_extensions: Mutex>, + pub permission_cache: Mutex>, +} + +impl ExtensionManager { + pub fn new() -> Self { + Self::default() + } + + pub fn get_base_extension_dir( + &self, + app_handle: &AppHandle, + ) -> Result { + 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 { + 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 { + 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 { + 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 { + 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>, +} + +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 { + let extensions = self.extensions.lock().unwrap(); + extensions.values().find(|p| p.name == addon_id).cloned() + } +} diff --git a/src-tauri/src/extension/core/manifest.rs b/src-tauri/src/extension/core/manifest.rs new file mode 100644 index 0000000..2ae35ea --- /dev/null +++ b/src-tauri/src/extension/core/manifest.rs @@ -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, + pub entry: String, + pub icon: Option, + pub public_key: String, + pub signature: String, + pub permissions: ExtensionManifestPermissions, + pub homepage: Option, + pub description: Option, +} + +impl ExtensionManifest { + pub fn calculate_key_hash(&self) -> Result { + ExtensionCrypto::calculate_key_hash(&self.public_key) + .map_err(|e| ExtensionError::InvalidPublicKey { reason: e }) + } + + pub fn full_extension_id(&self) -> Result { + 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, + #[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)] +pub struct ExtensionInfoResponse { + pub key_hash: String, + pub name: String, + pub full_id: String, + pub version: String, + pub display_name: Option, + pub namespace: Option, + pub allowed_origin: String, +} + +impl ExtensionInfoResponse { + pub fn from_extension( + extension: &crate::extension::core::types::Extension, + ) -> Result { + 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, + }) + } +} diff --git a/src-tauri/src/extension/core/mod.rs b/src-tauri/src/extension/core/mod.rs new file mode 100644 index 0000000..ded9a99 --- /dev/null +++ b/src-tauri/src/extension/core/mod.rs @@ -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::*; diff --git a/src-tauri/src/extension/core/protocol.rs b/src-tauri/src/extension/core/protocol.rs new file mode 100644 index 0000000..65e91ec --- /dev/null +++ b/src-tauri/src/extension/core/protocol.rs @@ -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 for DataProcessingError { + fn from(err: hex::FromHexError) -> Self { + DataProcessingError::HexDecoding(err) + } +} + +impl From for DataProcessingError { + fn from(err: std::string::FromUtf8Error) -> Self { + DataProcessingError::Utf8Conversion(err) + } +} + +impl From for DataProcessingError { + fn from(err: serde_json::Error) -> Self { + DataProcessingError::JsonParsing(err) + } +} + +pub fn resolve_secure_extension_asset_path( + app_handle: &AppHandle, + state: State, + extension_id: &str, + extension_version: &str, + requested_asset_path: &str, +) -> Result { + 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::(); + + 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, + app_handle: &AppHandle, + request: &Request>, +) -> Result>, Box> { + 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 { + 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) +} diff --git a/src-tauri/src/extension/core/types.rs b/src-tauri/src/extension/core/types.rs new file mode 100644 index 0000000..ac77169 --- /dev/null +++ b/src-tauri/src/extension/core/types.rs @@ -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(()) +} diff --git a/src-tauri/src/extension/core_old.rs b/src-tauri/src/extension/core_old.rs new file mode 100644 index 0000000..9414e09 --- /dev/null +++ b/src-tauri/src/extension/core_old.rs @@ -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, +} + +#[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 { + /// Konvertiert EditablePermissions zu internen ExtensionPermissions + 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, // 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 { + 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, + } + } + + /// Filtert nur granted Permissions + pub fn filter_granted(&self) -> Vec { + 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, + pub entry: String, + pub icon: Option, + pub public_key: String, + pub signature: String, + pub permissions: ExtensionManifestPermissions, + pub homepage: Option, + pub description: Option, +} + +impl ExtensionManifest { + /// Berechnet den Key Hash für diese Extension + pub fn calculate_key_hash(&self) -> Result { + 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 { + 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, + pub namespace: Option, + pub allowed_origin: String, +} + +impl ExtensionInfoResponse { + pub fn from_extension(extension: &Extension) -> Result { + // 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, + pub cached_at: SystemTime, + pub ttl: Duration, +} + +/// Enhanced extension manager +#[derive(Default)] +pub struct ExtensionManager { + pub production_extensions: Mutex>, + pub dev_extensions: Mutex>, + pub permission_cache: Mutex>, +} + +impl ExtensionManager { + pub fn new() -> Self { + Self::default() + } + + pub fn get_base_extension_dir(&self, app_handle: AppHandle) -> Result { + 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 { + 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 { + // 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 { + 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 { + 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>, +} + +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 { + 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 for DataProcessingError { + fn from(err: hex::FromHexError) -> Self { + DataProcessingError::HexDecoding(err) + } +} + +impl From for DataProcessingError { + fn from(err: std::string::FromUtf8Error) -> Self { + DataProcessingError::Utf8Conversion(err) + } +} + +impl From 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, + extension_id: &str, + extension_version: &str, + requested_asset_path: &str, +) -> Result { + // 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::(); + + 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( + state: State, + app_handle: AppHandle, + request: &Request>, +) -> Result>, Box> { + 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 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 { + // 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) +} diff --git a/src-tauri/src/extension/crypto.rs b/src-tauri/src/extension/crypto.rs new file mode 100644 index 0000000..9941d32 --- /dev/null +++ b/src-tauri/src/extension/crypto.rs @@ -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 { + 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 { + 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())) + } +} diff --git a/src-tauri/src/extension/database/executor.rs b/src-tauri/src/extension/database/executor.rs new file mode 100644 index 0000000..207887c --- /dev/null +++ b/src-tauri/src/extension/database/executor.rs @@ -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, 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, 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 = 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) + } +} diff --git a/src-tauri/src/extension/database/mod.rs b/src-tauri/src/extension/database/mod.rs index c2b9211..6ca6fa8 100644 --- a/src-tauri/src/extension/database/mod.rs +++ b/src-tauri/src/extension/database/mod.rs @@ -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, 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, 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)?; diff --git a/src-tauri/src/extension/database/permissions.rs b/src-tauri/src/extension/database/permissions.rs deleted file mode 100644 index d4bfcad..0000000 --- a/src-tauri/src/extension/database/permissions.rs +++ /dev/null @@ -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 = 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 { - 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 { - 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 { - 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, 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"), - } - } */ -} diff --git a/src-tauri/src/extension/error.rs b/src-tauri/src/extension/error.rs index 9bffeae..ac284d5 100644 --- a/src-tauri/src/extension/error.rs +++ b/src-tauri/src/extension/error.rs @@ -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(&self, serializer: S) -> Result + 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>, - }, + Http { reason: String }, #[error("Shell command failed: {reason}")] Shell { @@ -45,29 +67,51 @@ pub enum ExtensionError { exit_code: Option, }, - /* #[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, - ) -> Self { - Self::Http { - reason: reason.to_string(), - source: Some(source), - } - } - - /// Convenience constructor for shell errors - pub fn shell_error(reason: &str, exit_code: Option) -> 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 for String { + fn from(error: ExtensionError) -> Self { + serde_json::to_string(&error).unwrap_or_else(|_| error.to_string()) + } +} + impl From 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")); - } -} diff --git a/src-tauri/src/extension/mod.rs b/src-tauri/src/extension/mod.rs index a4485e4..c4e484c 100644 --- a/src-tauri/src/extension/mod.rs +++ b/src-tauri/src/extension/mod.rs @@ -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, + state: State, ) -> Result { - 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) -> Result, 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 { + 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 { + 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 { + 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 { + if let Some(ext) = state.extension_manager.get_extension(&extension_id) { + Ok(ext.manifest.version == extension_version) + } else { + Ok(false) + } } diff --git a/src-tauri/src/extension/permission_manager.rs b/src-tauri/src/extension/permission_manager.rs deleted file mode 100644 index 0298c13..0000000 --- a/src-tauri/src/extension/permission_manager.rs +++ /dev/null @@ -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, - pub filesystem: Vec, - pub http: Vec, - pub shell: Vec, -} - -#[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, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ShellPermission { - pub extension_id: String, - pub command: String, - pub arguments: Vec, -} - -/// 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, 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, 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, ExtensionError> { - // Implementierung für HTTP-Permissions - todo!("Implementiere HTTP-Permission-Loading") - } - - async fn get_shell_permissions( - connection: &DbConnection, - extension_id: &str, - ) -> Result, 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")); - } -} \ No newline at end of file diff --git a/src-tauri/src/extension/permissions/manager.rs b/src-tauri/src/extension/permissions/manager.rs new file mode 100644 index 0000000..031b063 --- /dev/null +++ b/src-tauri/src/extension/permissions/manager.rs @@ -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, 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::, _>>()?; + + Ok(permissions) + }).map_err(ExtensionError::from) + } + + // Helper für JSON -> ExtensionPermission Konvertierung + fn parse_permission_from_json(json: serde_json::Value) -> Result { + + 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 { + 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 { + 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 { + 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, ExtensionError> { + let all_permissions = Self::get_permissions(app_state, extension_id).await?; + Ok(all_permissions + .into_iter() + .filter(|perm| perm.status == PermissionStatus::Ask) + .collect()) + } +} diff --git a/src-tauri/src/extension/permissions/mod.rs b/src-tauri/src/extension/permissions/mod.rs new file mode 100644 index 0000000..740af61 --- /dev/null +++ b/src-tauri/src/extension/permissions/mod.rs @@ -0,0 +1,3 @@ +pub mod manager; +pub mod types; +pub mod validator; diff --git a/src-tauri/src/extension/permissions/types.rs b/src-tauri/src/extension/permissions/types.rs new file mode 100644 index 0000000..c4104c4 --- /dev/null +++ b/src-tauri/src/extension/permissions/types.rs @@ -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, + 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, +} + +#[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 { + 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, + #[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() + } +} diff --git a/src-tauri/src/extension/permissions/validator.rs b/src-tauri/src/extension/permissions/validator.rs new file mode 100644 index 0000000..bba9dc6 --- /dev/null +++ b/src-tauri/src/extension/permissions/validator.rs @@ -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, 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 { + 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 { + 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 { + 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) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dffe792..fcfde1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, // Kein Arc hier nötig, da der ganze AppState von Tauri in einem Arc verwaltet wird. + pub hlc: Mutex, + 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::(); + + // 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>, -) -> Result>, Box> { - 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())?) -} */ diff --git a/src/components/haex/extension/card.vue b/src/components/haex/extension/card.vue index 35ac6b9..256f3cf 100644 --- a/src/components/haex/extension/card.vue +++ b/src/components/haex/extension/card.vue @@ -4,20 +4,9 @@ v-bind="$attrs" >
- - - - - + + +
@@ -44,6 +33,7 @@
--> + hier klicken
- +
    -
  • -
    - - -
    -
  • -
-
- - - - -
    -
  • +
  • - + :id="Object.keys(read).at(0)" + type="checkbox" + class="checkbox" + :checked="Object.values(read).at(0)" + /> +
-
+ - +
    -
  • +
  • - + :id="Object.keys(write).at(0)" + type="checkbox" + class="checkbox" + :checked="Object.values(write).at(0)" + /> +
-
+ + + + + +
    +
  • +
    + + +
    +
  • +
+
- de: - permission: - read: Lesen - write: Schreiben - create: Erstellen +de: + permission: + read: Lesen + write: Schreiben + create: Erstellen - en: - permission: - read: Read - write: Write - create: Create +en: + permission: + read: Read + write: Write + create: Create diff --git a/src/components/haex/extension/manifest/permissions/filesystem.vue b/src/components/haex/extension/manifest/permissions/filesystem.vue index 46f031d..5cebc37 100644 --- a/src/components/haex/extension/manifest/permissions/filesystem.vue +++ b/src/components/haex/extension/manifest/permissions/filesystem.vue @@ -1,38 +1,56 @@ - de: - permission: - read: Lesen - write: Schreiben +de: + permission: + read: Lesen + write: Schreiben - en: - permission: - read: Read - write: Write +en: + permission: + read: Read + write: Write diff --git a/src/components/haex/extension/manifest/permissions/http.vue b/src/components/haex/extension/manifest/permissions/http.vue index 0d6c464..47bde8f 100644 --- a/src/components/haex/extension/manifest/permissions/http.vue +++ b/src/components/haex/extension/manifest/permissions/http.vue @@ -1,33 +1,43 @@ - de: - http: - access: Internet Zugriff +de: + http: + access: Internet Zugriff - en: - http: - access: Internet Access +en: + http: + access: Internet Access diff --git a/src/composables/extensionContextBroadcast.ts b/src/composables/extensionContextBroadcast.ts index fc2a2c2..9e24880 100644 --- a/src/composables/extensionContextBroadcast.ts +++ b/src/composables/extensionContextBroadcast.ts @@ -1,15 +1,17 @@ -/** - * Broadcasts context changes to all active extensions - */ +// composables/extensionContextBroadcast.ts export const useExtensionContextBroadcast = () => { - const extensionIframes = ref([]) + // Globaler State für alle aktiven IFrames + const extensionIframes = useState>( + 'extension-iframes', + () => new Set(), + ) const registerExtensionIframe = (iframe: HTMLIFrameElement) => { - extensionIframes.value.push(iframe) + extensionIframes.value.add(iframe) } const unregisterExtensionIframe = (iframe: HTMLIFrameElement) => { - extensionIframes.value = extensionIframes.value.filter((f) => f !== iframe) + extensionIframes.value.delete(iframe) } const broadcastContextChange = (context: { diff --git a/src/composables/extensionMessageHandler.ts b/src/composables/extensionMessageHandler.ts index 561d731..1285ff0 100644 --- a/src/composables/extensionMessageHandler.ts +++ b/src/composables/extensionMessageHandler.ts @@ -1,4 +1,6 @@ -import type { IHaexHubExtensionLink } from '~/types/haexhub' +// composables/extensionMessageHandler.ts +import { invoke } from '@tauri-apps/api/core' +import type { IHaexHubExtension } from '~/types/haexhub' interface ExtensionRequest { id: string @@ -7,119 +9,124 @@ interface ExtensionRequest { timestamp: number } -interface ExtensionResponse { - id: string - result?: unknown - error?: { - code: string - message: string - details?: unknown - } -} +// Globaler Handler - nur einmal registriert +let globalHandlerRegistered = false +const iframeRegistry = new Map() -export const useExtensionMessageHandler = ( - iframeRef: Ref, - extension: ComputedRef, -) => { - const handleMessage = async (event: MessageEvent) => { - // Security: Only accept messages from our iframe - if (!iframeRef.value || event.source !== iframeRef.value.contentWindow) { - return +const registerGlobalMessageHandler = () => { + if (globalHandlerRegistered) return + + window.addEventListener('message', async (event: MessageEvent) => { + // Finde die Extension für dieses IFrame + let extension: IHaexHubExtension | undefined + let sourceIframe: HTMLIFrameElement | undefined + + for (const [iframe, ext] of iframeRegistry.entries()) { + if (event.source === iframe.contentWindow) { + extension = ext + sourceIframe = iframe + break + } + } + + if (!extension || !sourceIframe) { + return // Message ist nicht von einem registrierten IFrame } const request = event.data as ExtensionRequest - // Validate request structure if (!request.id || !request.method) { console.error('Invalid extension request:', request) return } - console.log('[HaexHub] Extension request:', request.method, request.params) + console.log( + `[HaexHub] ${extension.name} request:`, + request.method, + request.params, + ) try { let result: unknown - // Route request to appropriate handler if (request.method.startsWith('extension.')) { - result = await handleExtensionMethod(request, extension) + result = await handleExtensionMethodAsync(request, extension) } else if (request.method.startsWith('db.')) { - result = await handleDatabaseMethod(request, extension) + result = await handleDatabaseMethodAsync(request, extension) + } else if (request.method.startsWith('fs.')) { + result = await handleFilesystemMethodAsync(request, extension) + } else if (request.method.startsWith('http.')) { + result = await handleHttpMethodAsync(request, extension) } else if (request.method.startsWith('permissions.')) { - result = await handlePermissionsMethod(request, extension) + result = await handlePermissionsMethodAsync(request, extension) } else if (request.method.startsWith('context.')) { - result = await handleContextMethod(request) - } else if (request.method.startsWith('search.')) { - result = await handleSearchMethod(request, extension) + result = await handleContextMethodAsync(request) } else { throw new Error(`Unknown method: ${request.method}`) } - // Send success response - sendResponse(iframeRef.value, { - id: request.id, - result, - }) + sourceIframe.contentWindow?.postMessage( + { + id: request.id, + result, + }, + '*', + ) } catch (error) { console.error('[HaexHub] Extension request error:', error) - // Send error response - sendResponse(iframeRef.value, { - id: request.id, - error: { - code: 'INTERNAL_ERROR', - message: error instanceof Error ? error.message : 'Unknown error', - details: error, + sourceIframe.contentWindow?.postMessage( + { + id: request.id, + error: { + code: 'INTERNAL_ERROR', + message: error instanceof Error ? error.message : 'Unknown error', + details: error, + }, }, - }) + '*', + ) } - } - - const sendResponse = ( - iframe: HTMLIFrameElement, - response: ExtensionResponse, - ) => { - iframe.contentWindow?.postMessage(response, '*') - } - - // Register/unregister message listener - onMounted(() => { - window.addEventListener('message', handleMessage) }) + globalHandlerRegistered = true +} + +export const useExtensionMessageHandler = ( + iframeRef: Ref, + extension: ComputedRef, +) => { + // Registriere globalen Handler beim ersten Aufruf + registerGlobalMessageHandler() + + // Registriere dieses IFrame + watchEffect(() => { + if (iframeRef.value && extension.value) { + iframeRegistry.set(iframeRef.value, extension.value) + } + }) + + // Cleanup beim Unmount onUnmounted(() => { - window.removeEventListener('message', handleMessage) + if (iframeRef.value) { + iframeRegistry.delete(iframeRef.value) + } }) - - return { - handleMessage, - } } // ========================================== // Extension Methods // ========================================== -async function handleExtensionMethod( +async function handleExtensionMethodAsync( request: ExtensionRequest, - extension: ComputedRef, + extension: IHaexHubExtension, // Direkter Typ, kein ComputedRef mehr ) { switch (request.method) { case 'extension.getInfo': - return { - keyHash: extension.value?.id || '', // TODO: Real key hash - name: extension.value?.name || '', - fullId: `${extension.value?.id}/${extension.value?.name}@${extension.value?.version}`, - version: extension.value?.version || '', - displayName: extension.value?.name, - namespace: extension.value?.author, - allowedOrigin: window.location.origin, // "tauri://localhost" - } - - case 'extensions.getDependencies': - // TODO: Implement dependencies from manifest - return [] - + return await invoke('get_extension_info', { + extensionId: extension.id, + }) default: throw new Error(`Unknown extension method: ${request.method}`) } @@ -129,47 +136,41 @@ async function handleExtensionMethod( // Database Methods // ========================================== -async function handleDatabaseMethod( +async function handleDatabaseMethodAsync( request: ExtensionRequest, - extension: ComputedRef, + extension: IHaexHubExtension, // Direkter Typ ) { - const { currentVault } = useVaultStore() - if (!currentVault) { - throw new Error('No vault available') + const params = request.params as { + query?: string + params?: unknown[] } - if (!extension.value) { - throw new Error('Extension not found') - } - - const params = request.params as { query?: string; params?: unknown[] } - switch (request.method) { case 'db.query': { - // Validate permission - await validateDatabaseAccess(extension.value, params.query || '', 'read') - - // Execute query - const result = await currentVault.drizzle.execute(params.query || '') + const rows = await invoke('extension_sql_select', { + sql: params.query || '', + params: params.params || [], + extensionId: extension.id, + }) return { - rows: result.rows || [], + rows, rowsAffected: 0, lastInsertId: undefined, } } case 'db.execute': { - // Validate permission - await validateDatabaseAccess(extension.value, params.query || '', 'write') - - // Execute query - const result = await currentVault.drizzle.execute(params.query || '') + await invoke('extension_sql_execute', { + sql: params.query || '', + params: params.params || [], + extensionId: extension.id, + }) return { rows: [], - rowsAffected: result.rowsAffected || 0, - lastInsertId: result.lastInsertId, + rowsAffected: 1, + lastInsertId: undefined, } } @@ -177,18 +178,14 @@ async function handleDatabaseMethod( const statements = (request.params as { statements?: string[] }).statements || [] - // Validate all statements for (const stmt of statements) { - await validateDatabaseAccess(extension.value, stmt, 'write') + await invoke('extension_sql_execute', { + sql: stmt, + params: [], + extensionId: extension.id, + }) } - // Execute transaction - await currentVault.drizzle.transaction(async (tx) => { - for (const stmt of statements) { - await tx.execute(stmt) - } - }) - return { success: true } } @@ -196,125 +193,63 @@ async function handleDatabaseMethod( throw new Error(`Unknown database method: ${request.method}`) } } - // ========================================== -// Permission Validation +// Filesystem Methods (TODO) // ========================================== -async function validateDatabaseAccess( - extension: IHaexHubExtensionLink, - query: string, - operation: 'read' | 'write', -): Promise { - // Extract table name from query - const tableMatch = query.match(/(?:FROM|INTO|UPDATE|TABLE)\s+(\w+)/i) - if (!tableMatch) { - throw new Error('Could not extract table name from query') - } - - const tableName = tableMatch[1] - - // Check if it's the extension's own table - const extensionPrefix = `${extension.id}_${extension.name?.replace(/-/g, '_')}_` - const isOwnTable = tableName.startsWith(extensionPrefix) - - if (isOwnTable) { - // Own tables: always allowed - return - } - - // External table: Check permissions - const hasPermission = await checkDatabasePermission( - extension.id, - tableName, - operation, - ) - - if (!hasPermission) { - throw new Error(`Permission denied: ${operation} access to ${tableName}`) - } -} - -async function checkDatabasePermission( - extensionId: string, - tableName: string, - operation: 'read' | 'write', -): Promise { - // TODO: Query permissions from database - // SELECT * FROM db_extension_permissions - // WHERE extension_id = ? AND resource = ? AND operation = ? - - console.warn('TODO: Implement permission check', { - extensionId, - tableName, - operation, - }) - - // For now: deny by default - return false -} - -// ========================================== -// Permission Methods -// ========================================== - -async function handlePermissionsMethod( +async function handleFilesystemMethodAsync( request: ExtensionRequest, - extension: ComputedRef, + extension: IHaexHubExtension, ) { - switch (request.method) { - case 'permissions.database.request': { - const params = request.params as { - resource: string - operation: 'read' | 'write' - reason?: string - } + if (!request || !extension) return + // TODO: Implementiere Filesystem Commands im Backend + throw new Error('Filesystem methods not yet implemented') +} - // TODO: Show user dialog to grant/deny permission - console.log('[HaexHub] Permission request:', params) +// ========================================== +// HTTP Methods (TODO) +// ========================================== - // For now: return ASK - return { - status: 'ask', - permanent: false, - } - } - - case 'permissions.database.check': { - const params = request.params as { - resource: string - operation: 'read' | 'write' - } - - const hasPermission = await checkDatabasePermission( - extension.value?.id || '', - params.resource, - params.operation, - ) - - return { - status: hasPermission ? 'granted' : 'denied', - permanent: true, - } - } - - default: - throw new Error(`Unknown permission method: ${request.method}`) +async function handleHttpMethodAsync( + request: ExtensionRequest, + extension: IHaexHubExtension, +) { + if (!extension || !request) { + throw new Error('Extension not found') } + + // TODO: Implementiere HTTP Commands im Backend + throw new Error('HTTP methods not yet implemented') +} + +// ========================================== +// Permission Methods (TODO) +// ========================================== + +async function handlePermissionsMethodAsync( + request: ExtensionRequest, + extension: IHaexHubExtension, +) { + if (!extension || !request) { + throw new Error('Extension not found') + } + + // TODO: Implementiere Permission Request UI + throw new Error('Permission methods not yet implemented') } // ========================================== // Context Methods // ========================================== -async function handleContextMethod(request: ExtensionRequest) { - const { theme } = useThemeStore() +async function handleContextMethodAsync(request: ExtensionRequest) { + const { currentTheme } = storeToRefs(useUiStore()) const { locale } = useI18n() switch (request.method) { case 'context.get': return { - theme: theme.value || 'system', + theme: currentTheme.value || 'system', locale: locale.value, platform: detectPlatform(), } @@ -330,29 +265,3 @@ function detectPlatform(): 'desktop' | 'mobile' | 'tablet' { if (width < 1024) return 'tablet' return 'desktop' } - -// ========================================== -// Search Methods -// ========================================== - -async function handleSearchMethod( - request: ExtensionRequest, - extension: ComputedRef, -) { - switch (request.method) { - case 'search.respond': { - const params = request.params as { - requestId: string - results: unknown[] - } - - // TODO: Store search results for display - console.log('[HaexHub] Search results from extension:', params) - - return { success: true } - } - - default: - throw new Error(`Unknown search method: ${request.method}`) - } -} diff --git a/src/pages/vault.vue b/src/pages/vault.vue index 8e7275e..8841b78 100644 --- a/src/pages/vault.vue +++ b/src/pages/vault.vue @@ -1,7 +1,7 @@
- +
diff --git a/src/pages/vault/[vaultId]/extensions/index.vue b/src/pages/vault/[vaultId]/extensions/index.vue index f31d160..6735bfc 100644 --- a/src/pages/vault/[vaultId]/extensions/index.vue +++ b/src/pages/vault/[vaultId]/extensions/index.vue @@ -31,18 +31,17 @@ class="size-full md:size-2/3 md:translate-x-1/5 md:translate-y-1/3" />
- - - - - + + +
@@ -93,7 +92,7 @@ const extension = reactive<{ path: '', }) -const loadExtensionManifestAsync = async () => { +/* const loadExtensionManifestAsync = async () => { try { extension.path = await open({ directory: true, recursive: true }) if (!extension.path) return @@ -111,7 +110,7 @@ const loadExtensionManifestAsync = async () => { add({ color: 'error', description: JSON.stringify(error) }) await addNotificationAsync({ text: JSON.stringify(error), type: 'error' }) } -} +} */ const { add } = useToast() const { addNotificationAsync } = useNotificationStore() diff --git a/src/pages/vault/[vaultId]/index.vue b/src/pages/vault/[vaultId]/index.vue index bac1141..b6b8f6c 100644 --- a/src/pages/vault/[vaultId]/index.vue +++ b/src/pages/vault/[vaultId]/index.vue @@ -1,12 +1,12 @@