From c92b638e1fe5b8f1090f0ebf360373686d96ed15 Mon Sep 17 00:00:00 2001 From: Serge Lapin Date: Sat, 25 Jul 2026 17:57:17 +0300 Subject: [PATCH 1/4] feat(drivers): add Windows integrated (Kerberos) auth for SQL Server (#1961) --- .github/workflows/build-linux.yml | 9 +- linux/Cargo.lock | 350 ++++++++++++++++-- linux/Cargo.toml | 2 +- linux/README.md | 6 +- .../app/src/services/connection_service.rs | 1 + linux/crates/app/src/ui/connect_dialog.rs | 59 ++- linux/crates/core/src/connection.rs | 19 + linux/crates/core/src/driver.rs | 8 + linux/crates/core/src/lib.rs | 2 +- linux/crates/drivers/mssql/src/lib.rs | 17 +- .../crates/drivers/mssql/tests/integration.rs | 1 + .../crates/drivers/mysql/tests/integration.rs | 1 + .../drivers/postgres/tests/integration.rs | 1 + linux/crates/storage/src/connections.rs | 28 ++ 14 files changed, 467 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 7443ffd19..866728594 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -38,7 +38,10 @@ jobs: # ubuntu:25.10 is minimal — git for Swatinem/rust-cache key # generation, curl + ca-certificates for rust-toolchain's # rustup install, plus the GTK / libadwaita / sourceview / - # OpenSSL / libsecret -dev packages the workspace links. + # OpenSSL / libsecret / MIT Kerberos -dev packages the workspace + # links. libkrb5-dev + clang cover the MSSQL driver's integrated + # (Kerberos/GSSAPI) auth: libgssapi-sys links gssapi_krb5 and runs + # bindgen (needs libclang). # No sudo (container runs as root by default). run: | apt-get update @@ -52,7 +55,9 @@ jobs: libadwaita-1-dev \ libgtksourceview-5-dev \ libssl-dev \ - libsecret-1-dev + libsecret-1-dev \ + libkrb5-dev \ + clang - uses: dtolnay/rust-toolchain@1.93 with: components: rustfmt, clippy diff --git a/linux/Cargo.lock b/linux/Cargo.lock index a1270c316..414948cd0 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -79,6 +79,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -127,7 +136,7 @@ dependencies = [ "futures-core", "libc", "portable-atomic", - "rustc-hash", + "rustc-hash 2.1.2", "tokio", "tokio-stream", "xattr", @@ -229,6 +238,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -336,6 +356,35 @@ dependencies = [ "sha2", ] +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -408,7 +457,7 @@ checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags", + "bitflags 2.11.1", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -544,7 +593,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cairo-sys-rs", "glib", "libc", @@ -582,6 +631,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-expr" version = "0.20.7" @@ -651,6 +709,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "cmake" version = "0.1.58" @@ -852,7 +936,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.117", ] @@ -1074,6 +1158,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1320,7 +1417,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot", + "parking_lot 0.12.5", ] [[package]] @@ -1568,7 +1665,7 @@ version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b7df55594e0e787d1560e23f7e12d7360d0b22e7b7c228ec2488b9e59b1b6b" dependencies = [ - "bitflags", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -1605,6 +1702,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "gobject-sys" version = "0.22.0" @@ -1830,6 +1933,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -1914,6 +2026,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + [[package]] name = "hyper" version = "1.9.0" @@ -2186,6 +2304,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "internal-russh-forked-ssh-key" version = "0.6.11+upstream-0.6.7" @@ -2260,6 +2387,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2369,6 +2502,38 @@ dependencies = [ "rand 0.9.4", ] +[[package]] +name = "libgssapi" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724dbcd1f871da9c67983537a47ac510c278656f6392418ad67c7a52720e54b2" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "lazy_static", + "libgssapi-sys", + "parking_lot 0.11.2", +] + +[[package]] +name = "libgssapi-sys" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd7d65e409c889f6c9d81ff079371d0d8fd88d7dca702ff187ef96fb0450fb7" +dependencies = [ + "bindgen", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -2381,7 +2546,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags", + "bitflags 2.11.1", "libc", "plain", "redox_syscall 0.7.4", @@ -2398,6 +2563,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2499,6 +2670,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2526,12 +2703,22 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2836,6 +3023,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2843,7 +3041,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.12", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -2911,6 +3123,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3275,13 +3493,22 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -3290,7 +3517,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] @@ -3461,7 +3688,7 @@ checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548" dependencies = [ "aes", "aws-lc-rs", - "bitflags", + "bitflags 2.11.1", "block-padding", "byteorder", "bytes", @@ -3557,6 +3784,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3572,16 +3805,29 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3792,7 +4038,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3805,7 +4051,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4200,7 +4446,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -4245,7 +4491,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -4349,6 +4595,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.11.1" @@ -4619,10 +4871,19 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "testcontainers" version = "0.27.3" @@ -4663,6 +4924,15 @@ dependencies = [ "testcontainers", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4727,6 +4997,7 @@ dependencies = [ "encoding_rs", "enumflags2", "futures-util", + "libgssapi", "num-traits", "once_cell", "pin-project-lite", @@ -5125,6 +5396,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -5235,6 +5512,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.1" @@ -5370,7 +5653,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -5404,6 +5687,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "whoami" version = "1.6.1" @@ -5430,6 +5725,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -5776,7 +6080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", @@ -5828,7 +6132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -5870,7 +6174,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix", + "rustix 1.1.4", "serde", "serde_repr", "tokio", diff --git a/linux/Cargo.toml b/linux/Cargo.toml index 27aa71c5e..529d26ea3 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -30,7 +30,7 @@ secrecy = { version = "0.10", features = ["serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "chrono", "rust_decimal", "uuid", "json"] } -tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal"] } +tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal", "integrated-auth-gssapi"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } rust_decimal = { version = "1", default-features = false, features = ["serde", "std"] } futures = "0.3" diff --git a/linux/README.md b/linux/README.md index bd8467afd..55f9aaee2 100644 --- a/linux/README.md +++ b/linux/README.md @@ -33,13 +33,13 @@ System dependencies: ```bash # Ubuntu / Debian -sudo apt install -y build-essential pkg-config libgtk-4-dev libadwaita-1-dev libssl-dev libsecret-1-dev +sudo apt install -y build-essential pkg-config libgtk-4-dev libadwaita-1-dev libssl-dev libsecret-1-dev libkrb5-dev clang # Fedora -sudo dnf install -y gcc pkg-config gtk4-devel libadwaita-devel openssl-devel libsecret-devel +sudo dnf install -y gcc pkg-config gtk4-devel libadwaita-devel openssl-devel libsecret-devel krb5-devel clang # Arch -sudo pacman -S --needed base-devel pkg-config gtk4 libadwaita openssl libsecret +sudo pacman -S --needed base-devel pkg-config gtk4 libadwaita openssl libsecret krb5 clang ``` Verify the right versions are present: diff --git a/linux/crates/app/src/services/connection_service.rs b/linux/crates/app/src/services/connection_service.rs index 27645a594..2ca4f0249 100644 --- a/linux/crates/app/src/services/connection_service.rs +++ b/linux/crates/app/src/services/connection_service.rs @@ -30,6 +30,7 @@ pub async fn open_saved(registry: Arc, saved: SavedConnection) - username: saved.username, password, use_tls: saved.use_tls, + auth_mode: saved.auth_mode, }; let (conn, tunnel) = establish(&*driver, opts.clone(), ssh_cfg.clone(), saved.read_only).await?; diff --git a/linux/crates/app/src/ui/connect_dialog.rs b/linux/crates/app/src/ui/connect_dialog.rs index c48f29f0f..a2e7aaef6 100644 --- a/linux/crates/app/src/ui/connect_dialog.rs +++ b/linux/crates/app/src/ui/connect_dialog.rs @@ -6,7 +6,7 @@ use relm4::{adw, gtk}; use secrecy::{ExposeSecret, SecretString}; use uuid::Uuid; -use tablepro_core::{ConnectOptions, DriverRegistry, TableInfo}; +use tablepro_core::{AuthMode, ConnectOptions, DriverRegistry, TableInfo}; use tablepro_storage::{ SavedConnection, SavedSshConfig, save_connections, store_password, store_ssh_passphrase, store_ssh_password, }; @@ -24,6 +24,7 @@ pub struct ConnectDialog { database: adw::EntryRow, username: adw::EntryRow, password: adw::PasswordEntryRow, + auth_combo: adw::ComboRow, use_tls: adw::SwitchRow, read_only: adw::SwitchRow, auth_group: adw::PreferencesGroup, @@ -48,6 +49,7 @@ pub enum ConnectDialogInput { DriverChanged(u32), SshToggled, SshAuthChanged, + AuthModeChanged, Submit, TestConnection, InputChanged, @@ -187,9 +189,25 @@ impl Component for ConnectDialog { connection_group.add(&port); connection_group.add(&database); + // MSSQL supports Windows integrated (Kerberos) auth alongside SQL + // logins; the selector is revealed only for integrated-capable + // drivers (see apply_driver_form_visibility). + let auth_password_label = crate::tr!("Password"); + let auth_kerberos_label = crate::tr!("Windows (Kerberos)"); + let auth_mode_model = gtk::StringList::new(&[auth_password_label.as_str(), auth_kerberos_label.as_str()]); + let auth_combo = adw::ComboRow::builder() + .title(crate::tr!("Method")) + .model(&auth_mode_model) + .build(); + let sender_for_authmode = sender.clone(); + auth_combo.connect_selected_notify(move |_| { + sender_for_authmode.input(ConnectDialogInput::AuthModeChanged); + }); + let auth_group = adw::PreferencesGroup::builder() .title(crate::tr!("Authentication")) .build(); + auth_group.add(&auth_combo); auth_group.add(&username); auth_group.add(&password); @@ -227,6 +245,7 @@ impl Component for ConnectDialog { database, username, password, + auth_combo, use_tls, read_only, auth_group, @@ -277,6 +296,11 @@ impl Component for ConnectDialog { self.refresh_validity(); } + ConnectDialogInput::AuthModeChanged => { + self.refresh_auth_visibility(); + self.refresh_validity(); + } + ConnectDialogInput::InputChanged => { self.refresh_validity(); } @@ -307,10 +331,13 @@ impl Component for ConnectDialog { username: self.username.text().to_string(), password: SecretString::new(self.password.text().to_string().into()), use_tls: self.use_tls.is_active(), + auth_mode: self.selected_auth_mode(), }; let label = if entry.id == "sqlite" { opts.database.clone() + } else if opts.username.is_empty() { + opts.host.clone() } else { format!("{}@{}", opts.username, opts.host) }; @@ -362,6 +389,7 @@ impl Component for ConnectDialog { username: self.username.text().to_string(), password: SecretString::new(self.password.text().to_string().into()), use_tls: self.use_tls.is_active(), + auth_mode: self.selected_auth_mode(), }; let ssh_inputs = if self.ssh.is_enabled() { match self.ssh.collect() { @@ -452,7 +480,7 @@ impl ConnectDialog { if self.host.text().trim().is_empty() { return false; } - if self.username.text().trim().is_empty() { + if self.username.is_visible() && self.username.text().trim().is_empty() { return false; } } @@ -466,13 +494,16 @@ impl ConnectDialog { let file_based = driver.is_file_based(); self.host.set_visible(!file_based); self.port.set_visible(!file_based); - self.username.set_visible(!file_based); - self.password.set_visible(!file_based); self.use_tls.set_visible(!file_based); // For file-based drivers (SQLite), only Connection + Options // groups make sense; hide Authentication and SSH entirely. self.auth_group.set_visible(!file_based); self.ssh.set_visible(!file_based); + // The auth-mode selector is only meaningful for drivers that + // support Windows integrated (Kerberos) auth — currently MSSQL. + self.auth_combo + .set_visible(!file_based && driver.supports_integrated_auth()); + self.refresh_auth_visibility(); self.database.set_title(&if file_based { crate::tr!("File path") } else { @@ -480,6 +511,24 @@ impl ConnectDialog { }); } + /// Kerberos is active only when the auth-mode combo is visible (the + /// driver supports integrated auth) and set to the Windows entry. + fn selected_auth_mode(&self) -> AuthMode { + if self.auth_combo.is_visible() && self.auth_combo.selected() == 1 { + AuthMode::Kerberos + } else { + AuthMode::Password + } + } + + /// Kerberos uses the ambient ticket cache, so hide the username / + /// password rows when it is the active mode. + fn refresh_auth_visibility(&self) { + let creds = matches!(self.selected_auth_mode(), AuthMode::Password); + self.username.set_visible(creds); + self.password.set_visible(creds); + } + fn show_toast(&self, message: &str) { self.toast_overlay.add_toast(adw::Toast::new(message)); } @@ -551,6 +600,7 @@ async fn run_connect( username: opts_clone.username.clone(), use_tls: opts_clone.use_tls, read_only, + auth_mode: opts_clone.auth_mode, ssh: ssh.as_ref().map(|s| s.saved.clone()), // Stays None until `App::on_connected` stamps it. Save then // connect arrives in that order, so a freshly-saved entry is @@ -604,6 +654,7 @@ async fn find_existing_id(driver_id: &str, opts: &ConnectOptions, ssh: Option<&S && c.port == opts.port && c.database == opts.database && c.username == opts.username + && c.auth_mode == opts.auth_mode && saved_ssh_matches(&c.ssh, ssh) }) .map(|c| c.id) diff --git a/linux/crates/core/src/connection.rs b/linux/crates/core/src/connection.rs index 6cf450e9e..f0a0b451a 100644 --- a/linux/crates/core/src/connection.rs +++ b/linux/crates/core/src/connection.rs @@ -1,9 +1,26 @@ use async_trait::async_trait; use secrecy::SecretString; +use serde::{Deserialize, Serialize}; use crate::error::DriverError; use crate::query::{ColumnInfo, ExecResult, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, Value}; +/// How a driver authenticates to the database. Most drivers only +/// support [`AuthMode::Password`]; the SQL Server driver also supports +/// [`AuthMode::Kerberos`] (Windows integrated auth) using the current +/// user's Kerberos ticket cache obtained via `kinit`. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AuthMode { + /// Username + password authentication. + #[default] + Password, + /// Windows integrated authentication over Kerberos (GSSAPI), using + /// the ambient ticket cache. `username`/`password` are ignored. + /// Drivers that don't support it fall back to password auth. + Kerberos, +} + #[derive(Debug, Clone)] pub struct ConnectOptions { pub host: String, @@ -12,6 +29,7 @@ pub struct ConnectOptions { pub username: String, pub password: SecretString, pub use_tls: bool, + pub auth_mode: AuthMode, } impl Default for ConnectOptions { @@ -23,6 +41,7 @@ impl Default for ConnectOptions { username: String::new(), password: SecretString::new(String::new().into()), use_tls: false, + auth_mode: AuthMode::Password, } } } diff --git a/linux/crates/core/src/driver.rs b/linux/crates/core/src/driver.rs index c98e26383..ee8c3b8e4 100644 --- a/linux/crates/core/src/driver.rs +++ b/linux/crates/core/src/driver.rs @@ -24,5 +24,13 @@ pub trait DatabaseDriver: Send + Sync { false } + /// Whether this driver supports Windows integrated / Kerberos + /// authentication (`ConnectOptions::auth_mode == AuthMode::Kerberos`). + /// The connect dialog only shows the auth-mode selector for drivers + /// returning `true`. Default `false`. + fn supports_integrated_auth(&self) -> bool { + false + } + async fn connect(&self, opts: ConnectOptions) -> Result, DriverError>; } diff --git a/linux/crates/core/src/lib.rs b/linux/crates/core/src/lib.rs index e728b4577..c36bdd842 100644 --- a/linux/crates/core/src/lib.rs +++ b/linux/crates/core/src/lib.rs @@ -8,7 +8,7 @@ mod registry; pub mod sql_ddl; pub mod sql_dialect; -pub use connection::{ConnectOptions, Connection}; +pub use connection::{AuthMode, ConnectOptions, Connection}; pub use driver::DatabaseDriver; pub use error::DriverError; pub use filter::{BuildFilterError, Combinator, FilterOp, FilterRule, FilterSet, FilterValue, build_filter_where}; diff --git a/linux/crates/drivers/mssql/src/lib.rs b/linux/crates/drivers/mssql/src/lib.rs index a518d5a9a..352abfc4c 100644 --- a/linux/crates/drivers/mssql/src/lib.rs +++ b/linux/crates/drivers/mssql/src/lib.rs @@ -12,8 +12,8 @@ use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt}; use tablepro_core::sql_dialect::build_order_and_pagination; use tablepro_core::{ - ColumnInfo, ConnectOptions, Connection, DatabaseDriver, DriverError, ExecResult, ForeignKeyInfo, IndexInfo, - MAX_QUERY_ROWS, QueryResult, TableInfo, Value, + AuthMode, ColumnInfo, ConnectOptions, Connection, DatabaseDriver, DriverError, ExecResult, ForeignKeyInfo, + IndexInfo, MAX_QUERY_ROWS, QueryResult, TableInfo, Value, }; type MssqlClient = Client>; @@ -42,12 +42,22 @@ impl DatabaseDriver for MssqlDriver { true } + fn supports_integrated_auth(&self) -> bool { + true + } + async fn connect(&self, opts: ConnectOptions) -> Result, DriverError> { let mut config = Config::new(); config.host(&opts.host); config.port(opts.port); config.database(&opts.database); - config.authentication(AuthMethod::sql_server(&opts.username, opts.password.expose_secret())); + config.authentication(match opts.auth_mode { + // Windows integrated auth over Kerberos (GSSAPI). tiberius + // uses the ambient ticket cache (from `kinit`) and the SPN + // MSSQLSvc/:; username/password are ignored. + AuthMode::Kerberos => AuthMethod::Integrated, + AuthMode::Password => AuthMethod::sql_server(&opts.username, opts.password.expose_secret()), + }); // SQL Server always encrypts the login exchange; `Off` keeps the // post-login stream in the clear, `Required` encrypts everything. // No cert-path UI exists, so the server certificate is trusted @@ -681,6 +691,7 @@ mod tests { assert_eq!(d.display_name(), "SQL Server"); assert_eq!(d.default_port(), 1433); assert!(!d.is_file_based()); + assert!(d.supports_integrated_auth()); } #[test] diff --git a/linux/crates/drivers/mssql/tests/integration.rs b/linux/crates/drivers/mssql/tests/integration.rs index a919410aa..f505b25f0 100644 --- a/linux/crates/drivers/mssql/tests/integration.rs +++ b/linux/crates/drivers/mssql/tests/integration.rs @@ -25,6 +25,7 @@ async fn start_mssql() -> (ContainerAsync, ConnectOptions) { username: "sa".into(), password: SecretString::new(MssqlServer::DEFAULT_SA_PASSWORD.to_string().into()), use_tls: false, + ..Default::default() }; (container, opts) } diff --git a/linux/crates/drivers/mysql/tests/integration.rs b/linux/crates/drivers/mysql/tests/integration.rs index ac6cb3e1f..e19486b46 100644 --- a/linux/crates/drivers/mysql/tests/integration.rs +++ b/linux/crates/drivers/mysql/tests/integration.rs @@ -27,6 +27,7 @@ async fn start_mysql() -> (ContainerAsync, ConnectOptions) { username: "root".into(), password: secrecy::SecretString::new("tablepro_test".to_string().into()), use_tls: false, + ..Default::default() }; (container, opts) } diff --git a/linux/crates/drivers/postgres/tests/integration.rs b/linux/crates/drivers/postgres/tests/integration.rs index a47d08bc5..077bda999 100644 --- a/linux/crates/drivers/postgres/tests/integration.rs +++ b/linux/crates/drivers/postgres/tests/integration.rs @@ -32,6 +32,7 @@ async fn start_pg() -> (ContainerAsync, ConnectOptions) { username: "postgres".into(), password: secrecy::SecretString::new("postgres".to_string().into()), use_tls: false, + ..Default::default() }; (container, opts) } diff --git a/linux/crates/storage/src/connections.rs b/linux/crates/storage/src/connections.rs index e2473483b..8c58f4677 100644 --- a/linux/crates/storage/src/connections.rs +++ b/linux/crates/storage/src/connections.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use tablepro_core::AuthMode; use uuid::Uuid; use crate::error::StorageError; @@ -20,6 +21,8 @@ pub struct SavedConnection { pub use_tls: bool, #[serde(default)] pub read_only: bool, + #[serde(default)] + pub auth_mode: AuthMode, #[serde(default, skip_serializing_if = "Option::is_none")] pub ssh: Option, /// Last successful open of this connection. Drives the welcome @@ -151,6 +154,7 @@ mod tests { username: "postgres".into(), use_tls: false, read_only: false, + auth_mode: AuthMode::Password, ssh: None, last_opened_at: None, } @@ -228,4 +232,28 @@ mod tests { let loaded = load_from(&path).await.unwrap(); assert_eq!(loaded, vec![conn]); } + + #[tokio::test] + async fn auth_mode_round_trips_and_defaults_to_password() { + let dir = TempDir::new().unwrap(); + let path = dir.path().join("connections.json"); + + // Explicit Kerberos survives a save/load cycle. + let mut conn = sample_connection(); + conn.auth_mode = AuthMode::Kerberos; + save_to(&path, &[conn.clone()]).await.unwrap(); + assert_eq!(load_from(&path).await.unwrap(), vec![conn]); + + // A legacy file without the field loads as Password. + let id = Uuid::new_v4(); + let legacy = format!( + r#"{{"version":1,"connections":[{{ + "id":"{id}","name":"Old","driver_id":"mssql", + "host":"localhost","port":1433,"database":"db", + "username":"sa","use_tls":false}}]}}"# + ); + tokio::fs::write(&path, legacy).await.unwrap(); + let loaded = load_from(&path).await.unwrap(); + assert_eq!(loaded[0].auth_mode, AuthMode::Password); + } } From c0554756afdd93f694cbfcc04ff7d94a61831dda Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 26 Jul 2026 00:27:23 +0700 Subject: [PATCH 2/4] fix(linux): keep the Kerberos SPN correct behind an SSH tunnel and stop storing unused passwords --- .github/workflows/build-linux.yml | 9 +- linux/Cargo.toml | 14 +- linux/README.md | 29 +++ .../app/src/services/connection_service.rs | 30 ++- linux/crates/app/src/ui/connect_dialog.rs | 219 +++++++++++++----- linux/crates/app/src/ui/error_text.rs | 3 + linux/crates/core/src/connection.rs | 63 ++++- linux/crates/core/src/error.rs | 6 + linux/crates/core/src/lib.rs | 2 +- linux/crates/drivers/mssql/Cargo.toml | 6 + linux/crates/drivers/mssql/src/lib.rs | 71 ++++-- linux/flatpak/com.tablepro.linux.json | 9 +- 12 files changed, 378 insertions(+), 83 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 866728594..e9642f286 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -70,8 +70,15 @@ jobs: run: cargo clippy --all-targets -- -D warnings - name: Build run: cargo build --workspace + # The MSSQL driver's Kerberos support is a default-on cargo feature + # so packagers without MIT Kerberos / libclang can drop it. Check + # that path here or it rots. + - name: Build without Kerberos + run: cargo check -p tablepro-driver-mssql --no-default-features --all-targets + # --bins matters: tablepro-app has no lib target, so --lib alone + # skips every test in the app crate. - name: Unit tests - run: cargo test --workspace --lib + run: cargo test --workspace --lib --bins integration: name: Driver integration tests (docker) diff --git a/linux/Cargo.toml b/linux/Cargo.toml index 529d26ea3..a3b864ab6 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -30,7 +30,10 @@ secrecy = { version = "0.10", features = ["serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "chrono", "rust_decimal", "uuid", "json"] } -tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal", "integrated-auth-gssapi"] } +# `integrated-auth-gssapi` is not listed here: it drags in MIT Kerberos +# and bindgen, so it hangs off the mssql crate's `kerberos` feature +# (default on) where a packager can turn it off. +tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } rust_decimal = { version = "1", default-features = false, features = ["serde", "std"] } futures = "0.3" @@ -49,3 +52,12 @@ relm4 = { version = "0.11", default-features = false, features = ["macros", "lib lto = "fat" codegen-units = 1 strip = "symbols" + +# libgssapi 0.4.6 builds the empty final GSSAPI token with +# `slice::from_raw_parts(null, 0)`, which trips Rust's UB precondition +# check and aborts the process. The check is emitted per crate, so +# turning debug assertions off for this one package keeps `cargo run` +# usable for Kerberos logins without weakening them anywhere else. +# Drop this once the null guard lands upstream. +[profile.dev.package.libgssapi] +debug-assertions = false diff --git a/linux/README.md b/linux/README.md index 55f9aaee2..62ba348fa 100644 --- a/linux/README.md +++ b/linux/README.md @@ -56,6 +56,35 @@ cd linux cargo run -p tablepro-app ``` +`libkrb5-dev` and `clang` are only there for the SQL Server driver's +Windows integrated auth, which links MIT Kerberos and runs bindgen. Drop +both packages and build without it: + +```bash +cargo build --workspace --no-default-features +``` + +## SQL Server with Windows integrated auth + +Pick **Method → Windows (Kerberos)** in the connect dialog. There is no +username or password to enter: the driver uses whatever ticket `klist` +shows, so get one first. + +```bash +kinit you@EXAMPLE.COM +``` + +The driver asks for `MSSQLSvc/:`, built from the host and +port you typed, not from an SSH tunnel's local forward. Two things are +worth knowing: + +- tiberius imports that SPN as a raw Kerberos principal, so it resolves + in your *default* realm. When the service lives in another realm, set + `default_realm` in `/etc/krb5.conf` and map the host with + `[domain_realm]` (plus `[capaths]` for a cross-realm trust). +- The host has to match the SPN registered on the server. An IP address + or a CNAME usually does not. + ## Documentation index | Topic | File | diff --git a/linux/crates/app/src/services/connection_service.rs b/linux/crates/app/src/services/connection_service.rs index 2ca4f0249..6354163e4 100644 --- a/linux/crates/app/src/services/connection_service.rs +++ b/linux/crates/app/src/services/connection_service.rs @@ -1,7 +1,9 @@ use std::sync::Arc; use secrecy::SecretString; -use tablepro_core::{ConnectOptions, Connection, DriverRegistry, ReadOnlyConnection, TableInfo}; +use tablepro_core::{ + AuthMode, ConnectOptions, Connection, DriverRegistry, ReadOnlyConnection, ServiceEndpoint, TableInfo, +}; use tablepro_ssh::{SshConfig, SshTunnel}; use tablepro_storage::{SavedConnection, SavedSshAuth, load_password, load_ssh_passphrase, load_ssh_password}; @@ -11,11 +13,16 @@ pub async fn open_saved(registry: Arc, saved: SavedConnection) - let driver = registry .get(&saved.driver_id) .ok_or_else(|| format!("driver {} not registered", saved.driver_id))?; - let password = load_password(saved.id) - .await - .ok() - .flatten() - .unwrap_or_else(|| SecretString::new(String::new().into())); + // Kerberos authenticates from the ambient ticket cache, so there is + // no stored secret to read back from the keyring. + let password = match saved.auth_mode { + AuthMode::Kerberos => SecretString::new(String::new().into()), + AuthMode::Password => load_password(saved.id) + .await + .ok() + .flatten() + .unwrap_or_else(|| SecretString::new(String::new().into())), + }; let id = saved.id; let ssh_cfg = match &saved.ssh { @@ -31,6 +38,7 @@ pub async fn open_saved(registry: Arc, saved: SavedConnection) - password, use_tls: saved.use_tls, auth_mode: saved.auth_mode, + service_endpoint: None, }; let (conn, tunnel) = establish(&*driver, opts.clone(), ssh_cfg.clone(), saved.read_only).await?; @@ -59,9 +67,17 @@ pub async fn establish( let tunnel = if let Some(cfg) = ssh { let remote_host = std::mem::take(&mut opts.host); let remote_port = opts.port; - let tun = SshTunnel::open(cfg, remote_host, remote_port) + let tun = SshTunnel::open(cfg, remote_host.clone(), remote_port) .await .map_err(|e| format!("ssh: {e}"))?; + // The socket now points at the local forward, so remember what + // the service is actually called. Kerberos builds its SPN from + // it; without this a tunnelled connection would ask the KDC for + // MSSQLSvc/127.0.0.1:. + opts.service_endpoint = Some(ServiceEndpoint { + host: remote_host, + port: remote_port, + }); opts.host = tun.local_host().to_string(); opts.port = tun.local_port(); Some(tun) diff --git a/linux/crates/app/src/ui/connect_dialog.rs b/linux/crates/app/src/ui/connect_dialog.rs index a2e7aaef6..a6a7a9d83 100644 --- a/linux/crates/app/src/ui/connect_dialog.rs +++ b/linux/crates/app/src/ui/connect_dialog.rs @@ -32,6 +32,7 @@ pub struct ConnectDialog { test_button: gtk::Button, submit: gtk::Button, toast_overlay: adw::ToastOverlay, + form: AuthFormState, } #[derive(Debug, Clone)] @@ -40,6 +41,43 @@ struct DriverEntry { display_name: String, } +/// Row index of the Kerberos entry in the auth-method model. +const KERBEROS_ROW: u32 = 1; + +/// What the selected driver allows, kept beside the widgets so the form +/// never has to read its own visibility flags back to work out what the +/// user picked. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct AuthFormState { + file_based: bool, + supports_integrated: bool, + kerberos_selected: bool, +} + +impl AuthFormState { + /// Kerberos only applies to a network driver that supports it. A + /// stale selection left over from another driver resolves back to + /// password auth instead of leaking across the switch. + fn mode(self) -> AuthMode { + if !self.file_based && self.supports_integrated && self.kerberos_selected { + AuthMode::Kerberos + } else { + AuthMode::Password + } + } + + /// The method selector is noise for a driver with only one method. + fn shows_method(self) -> bool { + !self.file_based && self.supports_integrated + } + + /// Kerberos reads the ambient ticket cache, so the credential rows + /// have nothing to collect. + fn shows_credentials(self) -> bool { + !self.file_based && self.mode() == AuthMode::Password + } +} + pub struct ConnectDialogInit { pub registry: Arc, } @@ -191,7 +229,7 @@ impl Component for ConnectDialog { // MSSQL supports Windows integrated (Kerberos) auth alongside SQL // logins; the selector is revealed only for integrated-capable - // drivers (see apply_driver_form_visibility). + // drivers (see AuthFormState::shows_method). let auth_password_label = crate::tr!("Password"); let auth_kerberos_label = crate::tr!("Windows (Kerberos)"); let auth_mode_model = gtk::StringList::new(&[auth_password_label.as_str(), auth_kerberos_label.as_str()]); @@ -236,7 +274,7 @@ impl Component for ConnectDialog { let toast_overlay = adw::ToastOverlay::new(); toast_overlay.set_child(Some(&page)); - let model = ConnectDialog { + let mut model = ConnectDialog { registry: init.registry, drivers: drivers.clone(), driver_combo, @@ -253,6 +291,7 @@ impl Component for ConnectDialog { test_button, submit, toast_overlay, + form: AuthFormState::default(), }; let widgets = view_output!(); @@ -277,7 +316,7 @@ impl Component for ConnectDialog { fn update(&mut self, msg: Self::Input, sender: ComponentSender, root: &Self::Root) { match msg { ConnectDialogInput::DriverChanged(idx) => { - let Some(entry) = self.drivers.get(idx as usize) else { + let Some(entry) = self.drivers.get(idx as usize).cloned() else { return; }; if let Some(driver) = self.registry.get(&entry.id) { @@ -285,6 +324,9 @@ impl Component for ConnectDialog { self.port.set_value(driver.default_port() as f64); } root.set_title(&crate::tr!("Connect to {name}").replace("{name}", &entry.display_name)); + // Which fields are required just changed, so the submit + // state has to be recomputed against the new driver. + self.refresh_validity(); } ConnectDialogInput::SshToggled => { @@ -297,7 +339,8 @@ impl Component for ConnectDialog { } ConnectDialogInput::AuthModeChanged => { - self.refresh_auth_visibility(); + self.form.kerberos_selected = self.auth_combo.selected() == KERBEROS_ROW; + self.apply_form_state(); self.refresh_validity(); } @@ -324,19 +367,12 @@ impl Component for ConnectDialog { } }; - let opts = ConnectOptions { - host: self.host.text().to_string(), - port: self.port.value() as u16, - database: self.database.text().to_string(), - username: self.username.text().to_string(), - password: SecretString::new(self.password.text().to_string().into()), - use_tls: self.use_tls.is_active(), - auth_mode: self.selected_auth_mode(), - }; + let opts = self.collect_options(); + // A Kerberos entry has no username to name it after. let label = if entry.id == "sqlite" { opts.database.clone() - } else if opts.username.is_empty() { + } else if opts.auth_mode == AuthMode::Kerberos { opts.host.clone() } else { format!("{}@{}", opts.username, opts.host) @@ -382,15 +418,7 @@ impl Component for ConnectDialog { self.show_toast(&crate::tr!("Driver {id} not registered").replace("{id}", &entry.id)); return; }; - let opts = ConnectOptions { - host: self.host.text().to_string(), - port: self.port.value() as u16, - database: self.database.text().to_string(), - username: self.username.text().to_string(), - password: SecretString::new(self.password.text().to_string().into()), - use_tls: self.use_tls.is_active(), - auth_mode: self.selected_auth_mode(), - }; + let opts = self.collect_options(); let ssh_inputs = if self.ssh.is_enabled() { match self.ssh.collect() { Ok(inputs) => Some(inputs.cfg), @@ -459,11 +487,11 @@ impl ConnectDialog { let database_empty = self.database.text().trim().is_empty(); toggle_error(&self.database, database_empty); - let host_required = self.host.is_visible(); + let host_required = !self.form.file_based; let host_empty = host_required && self.host.text().trim().is_empty(); toggle_error(&self.host, host_empty); - let username_required = self.username.is_visible(); + let username_required = self.form.shows_credentials(); let username_empty = username_required && self.username.text().trim().is_empty(); toggle_error(&self.username, username_empty); @@ -476,11 +504,11 @@ impl ConnectDialog { if self.database.text().trim().is_empty() { return false; } - if self.host.is_visible() { + if !self.form.file_based { if self.host.text().trim().is_empty() { return false; } - if self.username.is_visible() && self.username.text().trim().is_empty() { + if self.form.shows_credentials() && self.username.text().trim().is_empty() { return false; } } @@ -490,43 +518,59 @@ impl ConnectDialog { true } - fn apply_driver_form_visibility(&self, driver: &dyn tablepro_core::DatabaseDriver) { - let file_based = driver.is_file_based(); - self.host.set_visible(!file_based); - self.port.set_visible(!file_based); - self.use_tls.set_visible(!file_based); - // For file-based drivers (SQLite), only Connection + Options - // groups make sense; hide Authentication and SSH entirely. - self.auth_group.set_visible(!file_based); - self.ssh.set_visible(!file_based); - // The auth-mode selector is only meaningful for drivers that - // support Windows integrated (Kerberos) auth — currently MSSQL. - self.auth_combo - .set_visible(!file_based && driver.supports_integrated_auth()); - self.refresh_auth_visibility(); - self.database.set_title(&if file_based { + fn apply_driver_form_visibility(&mut self, driver: &dyn tablepro_core::DatabaseDriver) { + self.form.file_based = driver.is_file_based(); + self.form.supports_integrated = driver.supports_integrated_auth(); + self.apply_form_state(); + self.database.set_title(&if self.form.file_based { crate::tr!("File path") } else { crate::tr!("Database") }); } - /// Kerberos is active only when the auth-mode combo is visible (the - /// driver supports integrated auth) and set to the Windows entry. + fn apply_form_state(&self) { + let network = !self.form.file_based; + self.host.set_visible(network); + self.port.set_visible(network); + self.use_tls.set_visible(network); + // For file-based drivers (SQLite), only Connection + Options + // groups make sense; hide Authentication and SSH entirely. + self.auth_group.set_visible(network); + self.ssh.set_visible(network); + self.auth_combo.set_visible(self.form.shows_method()); + let credentials = self.form.shows_credentials(); + self.username.set_visible(credentials); + self.password.set_visible(credentials); + } + fn selected_auth_mode(&self) -> AuthMode { - if self.auth_combo.is_visible() && self.auth_combo.selected() == 1 { - AuthMode::Kerberos - } else { - AuthMode::Password - } + self.form.mode() } - /// Kerberos uses the ambient ticket cache, so hide the username / - /// password rows when it is the active mode. - fn refresh_auth_visibility(&self) { - let creds = matches!(self.selected_auth_mode(), AuthMode::Password); - self.username.set_visible(creds); - self.password.set_visible(creds); + fn collect_options(&self) -> ConnectOptions { + // Kerberos ignores the credential rows, and whatever the user + // typed before switching modes must not reach the driver or the + // keyring, so it never leaves the widget. + let kerberos = self.selected_auth_mode() == AuthMode::Kerberos; + ConnectOptions { + host: self.host.text().to_string(), + port: self.port.value() as u16, + database: self.database.text().to_string(), + username: if kerberos { + String::new() + } else { + self.username.text().to_string() + }, + password: if kerberos { + SecretString::new(String::new().into()) + } else { + SecretString::new(self.password.text().to_string().into()) + }, + use_tls: self.use_tls.is_active(), + auth_mode: self.selected_auth_mode(), + service_endpoint: None, + } } fn show_toast(&self, message: &str) { @@ -609,7 +653,11 @@ async fn run_connect( }; save_one(&saved).await.map_err(|e| format!("save: {e}"))?; - let _ = store_password(saved.id, stored_password.expose_secret(), &label).await; + // Kerberos has no secret of ours to keep; writing one would leave an + // unreachable credential in the keyring. + if saved.auth_mode == AuthMode::Password { + let _ = store_password(saved.id, stored_password.expose_secret(), &label).await; + } if let Some(s) = &ssh { match &s.secret_to_store { SshSecretToStore::Password(p) => { @@ -667,3 +715,62 @@ fn saved_ssh_matches(saved: &Option, current: Option<&SshInputs> _ => false, } } + +#[cfg(test)] +mod tests { + use super::*; + + fn mssql(kerberos_selected: bool) -> AuthFormState { + AuthFormState { + file_based: false, + supports_integrated: true, + kerberos_selected, + } + } + + #[test] + fn password_is_the_default_everywhere() { + let state = AuthFormState::default(); + assert_eq!(state.mode(), AuthMode::Password); + assert!(!state.shows_method()); + assert!(state.shows_credentials()); + } + + #[test] + fn integrated_driver_offers_the_method_selector() { + assert!(mssql(false).shows_method()); + assert_eq!(mssql(false).mode(), AuthMode::Password); + assert!(mssql(false).shows_credentials()); + } + + #[test] + fn kerberos_hides_the_credential_rows() { + assert_eq!(mssql(true).mode(), AuthMode::Kerberos); + assert!(!mssql(true).shows_credentials()); + assert!(mssql(true).shows_method()); + } + + #[test] + fn a_stale_kerberos_selection_does_not_survive_a_driver_switch() { + let postgres = AuthFormState { + file_based: false, + supports_integrated: false, + kerberos_selected: true, + }; + assert_eq!(postgres.mode(), AuthMode::Password); + assert!(!postgres.shows_method()); + assert!(postgres.shows_credentials()); + } + + #[test] + fn file_based_drivers_ask_for_no_credentials() { + let sqlite = AuthFormState { + file_based: true, + supports_integrated: true, + kerberos_selected: true, + }; + assert_eq!(sqlite.mode(), AuthMode::Password); + assert!(!sqlite.shows_method()); + assert!(!sqlite.shows_credentials()); + } +} diff --git a/linux/crates/app/src/ui/error_text.rs b/linux/crates/app/src/ui/error_text.rs index 7d43e9d73..054599d3d 100644 --- a/linux/crates/app/src/ui/error_text.rs +++ b/linux/crates/app/src/ui/error_text.rs @@ -32,6 +32,9 @@ pub fn driver_message(error: &DriverError) -> String { crate::tr!("This connection is read-only. Reopen it without read-only mode to make changes.") } DriverError::Internal(detail) => crate::tr!("Internal driver error: {detail}").replace("{detail}", detail), + DriverError::Unsupported(detail) => { + crate::tr!("Not supported by this build: {detail}").replace("{detail}", detail) + } DriverError::Transaction { statement_index, source, diff --git a/linux/crates/core/src/connection.rs b/linux/crates/core/src/connection.rs index f0a0b451a..4881530e2 100644 --- a/linux/crates/core/src/connection.rs +++ b/linux/crates/core/src/connection.rs @@ -17,10 +17,21 @@ pub enum AuthMode { Password, /// Windows integrated authentication over Kerberos (GSSAPI), using /// the ambient ticket cache. `username`/`password` are ignored. - /// Drivers that don't support it fall back to password auth. + /// Drivers that report `supports_integrated_auth() == false` reject + /// this mode rather than silently authenticating some other way. Kerberos, } +/// Host and port the database service is known by, kept separate from +/// the socket address when an SSH tunnel rewrites `host`/`port` to a +/// local forward. Anything that names the *service* rather than the +/// socket (a Kerberos SPN, a TLS server name) has to read this. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServiceEndpoint { + pub host: String, + pub port: u16, +} + #[derive(Debug, Clone)] pub struct ConnectOptions { pub host: String, @@ -30,6 +41,20 @@ pub struct ConnectOptions { pub password: SecretString, pub use_tls: bool, pub auth_mode: AuthMode, + /// Set only when `host`/`port` were replaced by a tunnel's local + /// forward. `None` means the socket already points at the service. + pub service_endpoint: Option, +} + +impl ConnectOptions { + /// Host and port the service answers to, which is `host`/`port` + /// unless a tunnel replaced them. + pub fn service_address(&self) -> (&str, u16) { + match &self.service_endpoint { + Some(endpoint) => (endpoint.host.as_str(), endpoint.port), + None => (self.host.as_str(), self.port), + } + } } impl Default for ConnectOptions { @@ -42,6 +67,7 @@ impl Default for ConnectOptions { password: SecretString::new(String::new().into()), use_tls: false, auth_mode: AuthMode::Password, + service_endpoint: None, } } } @@ -101,3 +127,38 @@ pub trait Connection: Send + Sync { async fn ping(&self) -> Result<(), DriverError>; async fn close(self: Box) -> Result<(), DriverError>; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn service_address_falls_back_to_the_socket_address() { + let opts = ConnectOptions { + host: "sql.corp.example".into(), + port: 1433, + ..Default::default() + }; + assert_eq!(opts.service_address(), ("sql.corp.example", 1433)); + } + + #[test] + fn service_address_survives_a_tunnel_rewrite() { + let opts = ConnectOptions { + host: "127.0.0.1".into(), + port: 54321, + service_endpoint: Some(ServiceEndpoint { + host: "sql.corp.example".into(), + port: 1433, + }), + ..Default::default() + }; + assert_eq!(opts.service_address(), ("sql.corp.example", 1433)); + } + + #[test] + fn auth_mode_defaults_to_password() { + assert_eq!(AuthMode::default(), AuthMode::Password); + assert_eq!(ConnectOptions::default().auth_mode, AuthMode::Password); + } +} diff --git a/linux/crates/core/src/error.rs b/linux/crates/core/src/error.rs index 771adf4cb..b31dd3422 100644 --- a/linux/crates/core/src/error.rs +++ b/linux/crates/core/src/error.rs @@ -23,6 +23,12 @@ pub enum DriverError { #[error("driver internal error: {0}")] Internal(String), + /// The driver understood the request but this binary cannot serve + /// it, typically because an optional cargo feature was compiled + /// out. Distinct from `Internal`: nothing went wrong at runtime. + #[error("not supported by this build: {0}")] + Unsupported(String), + /// Returned by `Connection::execute_in_transaction` when one of the /// statements failed; the index identifies which statement (so the /// UI can highlight the offending row) and `source` carries the diff --git a/linux/crates/core/src/lib.rs b/linux/crates/core/src/lib.rs index c36bdd842..214590b52 100644 --- a/linux/crates/core/src/lib.rs +++ b/linux/crates/core/src/lib.rs @@ -8,7 +8,7 @@ mod registry; pub mod sql_ddl; pub mod sql_dialect; -pub use connection::{AuthMode, ConnectOptions, Connection}; +pub use connection::{AuthMode, ConnectOptions, Connection, ServiceEndpoint}; pub use driver::DatabaseDriver; pub use error::DriverError; pub use filter::{BuildFilterError, Combinator, FilterOp, FilterRule, FilterSet, FilterValue, build_filter_where}; diff --git a/linux/crates/drivers/mssql/Cargo.toml b/linux/crates/drivers/mssql/Cargo.toml index 7ee26aa61..4f2c61577 100644 --- a/linux/crates/drivers/mssql/Cargo.toml +++ b/linux/crates/drivers/mssql/Cargo.toml @@ -9,6 +9,12 @@ publish = false name = "drivers_mssql" path = "src/lib.rs" +[features] +default = ["kerberos"] +# Windows integrated auth. Links MIT Kerberos (libkrb5) and runs bindgen +# (libclang) at build time; build with --no-default-features to drop both. +kerberos = ["tiberius/integrated-auth-gssapi"] + [dependencies] tablepro-core = { path = "../../core" } async-trait.workspace = true diff --git a/linux/crates/drivers/mssql/src/lib.rs b/linux/crates/drivers/mssql/src/lib.rs index 352abfc4c..4b49d0480 100644 --- a/linux/crates/drivers/mssql/src/lib.rs +++ b/linux/crates/drivers/mssql/src/lib.rs @@ -43,21 +43,21 @@ impl DatabaseDriver for MssqlDriver { } fn supports_integrated_auth(&self) -> bool { - true + cfg!(feature = "kerberos") } async fn connect(&self, opts: ConnectOptions) -> Result, DriverError> { + // tiberius derives the Kerberos SPN and the TLS server name from + // the configured host/port, while the socket is opened here. An + // SSH tunnel replaces `opts.host`/`opts.port` with a local + // forward, so the two have to come from different places: + // `service_address()` names the server, `opts.host` reaches it. + let (service_host, service_port) = opts.service_address(); let mut config = Config::new(); - config.host(&opts.host); - config.port(opts.port); + config.host(service_host); + config.port(service_port); config.database(&opts.database); - config.authentication(match opts.auth_mode { - // Windows integrated auth over Kerberos (GSSAPI). tiberius - // uses the ambient ticket cache (from `kinit`) and the SPN - // MSSQLSvc/:; username/password are ignored. - AuthMode::Kerberos => AuthMethod::Integrated, - AuthMode::Password => AuthMethod::sql_server(&opts.username, opts.password.expose_secret()), - }); + config.authentication(authentication_for(&opts)?); // SQL Server always encrypts the login exchange; `Off` keeps the // post-login stream in the clear, `Required` encrypts everything. // No cert-path UI exists, so the server certificate is trusted @@ -74,9 +74,13 @@ impl DatabaseDriver for MssqlDriver { // and an unreachable host would otherwise hang the connect // dialog for the OS SYN timeout. The budget covers both so the // failure arrives on the same scale as the sqlx drivers' - // acquire_timeout. + // acquire_timeout. The socket targets opts.host/opts.port (an + // SSH tunnel's local forward when present); `config` already + // names the real service for the SPN and TLS server name. tokio::time::timeout(CONNECT_TIMEOUT, async { - let tcp = TcpStream::connect(config.get_addr()).await.map_err(map_io_error)?; + let tcp = TcpStream::connect((opts.host.as_str(), opts.port)) + .await + .map_err(map_io_error)?; tcp.set_nodelay(true).map_err(map_io_error)?; Client::connect(config, tcp.compat_write()) .await @@ -92,6 +96,26 @@ impl DatabaseDriver for MssqlDriver { } } +/// Windows integrated auth over Kerberos (GSSAPI) needs tiberius's +/// `integrated-auth-gssapi` feature, which links MIT Kerberos and runs +/// bindgen at build time. The `kerberos` feature keeps that opt-out-able +/// for packagers, so the mode has to fail loudly when it is compiled out +/// rather than quietly falling back to a SQL login. +fn authentication_for(opts: &ConnectOptions) -> Result { + match opts.auth_mode { + AuthMode::Password => Ok(AuthMethod::sql_server(&opts.username, opts.password.expose_secret())), + #[cfg(feature = "kerberos")] + // tiberius reads the ambient ticket cache (from `kinit`) and + // targets MSSQLSvc/:; username and password + // are ignored. + AuthMode::Kerberos => Ok(AuthMethod::Integrated), + #[cfg(not(feature = "kerberos"))] + AuthMode::Kerberos => Err(DriverError::Unsupported( + "Kerberos authentication (cargo feature `kerberos`) was not compiled in".into(), + )), + } +} + struct MssqlConnection { client: Mutex, } @@ -691,7 +715,28 @@ mod tests { assert_eq!(d.display_name(), "SQL Server"); assert_eq!(d.default_port(), 1433); assert!(!d.is_file_based()); - assert!(d.supports_integrated_auth()); + assert_eq!(d.supports_integrated_auth(), cfg!(feature = "kerberos")); + } + + #[test] + fn kerberos_matches_the_advertised_capability() { + let opts = ConnectOptions { + auth_mode: AuthMode::Kerberos, + ..Default::default() + }; + assert_eq!( + authentication_for(&opts).is_ok(), + MssqlDriver.supports_integrated_auth() + ); + } + + #[test] + fn password_auth_is_always_available() { + let opts = ConnectOptions { + username: "sa".into(), + ..Default::default() + }; + assert!(authentication_for(&opts).is_ok()); } #[test] diff --git a/linux/flatpak/com.tablepro.linux.json b/linux/flatpak/com.tablepro.linux.json index 9dde351f2..bfa6b3ec8 100644 --- a/linux/flatpak/com.tablepro.linux.json +++ b/linux/flatpak/com.tablepro.linux.json @@ -4,7 +4,8 @@ "runtime-version": "47", "sdk": "org.gnome.Sdk", "sdk-extensions": [ - "org.freedesktop.Sdk.Extension.rust-stable" + "org.freedesktop.Sdk.Extension.rust-stable", + "org.freedesktop.Sdk.Extension.llvm18" ], "command": "tablepro-app", "finish-args": [ @@ -17,9 +18,11 @@ "--talk-name=org.freedesktop.secrets" ], "build-options": { - "append-path": "/usr/lib/sdk/rust-stable/bin", + "append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm18/bin", + "prepend-ld-library-path": "/usr/lib/sdk/llvm18/lib", "env": { - "CARGO_HOME": "/run/build/tablepro-app/cargo" + "CARGO_HOME": "/run/build/tablepro-app/cargo", + "LIBCLANG_PATH": "/usr/lib/sdk/llvm18/lib" } }, "modules": [ From 8fcca0c14a1a89b43be4b189c53ebac3f17b08a9 Mon Sep 17 00:00:00 2001 From: Serge Lapin Date: Mon, 27 Jul 2026 13:37:13 +0300 Subject: [PATCH 3/4] fix(linux): pin tiberius to its fixed main so Kerberos logins don't abort debug builds tiberius 0.12.3 (its latest release) pins libgssapi ^0.4.5. On a successful GSSAPI handshake the KDC returns an empty final token (value=NULL, length=0), and libgssapi 0.4.6's `Buf::deref` calls `slice::from_raw_parts(NULL, 0)` -- undefined behavior that trips the standard library's precondition check and aborts (SIGABRT) at util.rs:237. Every Kerberos login therefore crashes under a debug build (`cargo run`); the previously committed per-package `debug-assertions = false` override did not reliably suppress it and left the UB in place regardless. tiberius already fixed this on `main` (merged PR prisma/tiberius#372: bump libgssapi 0.4.5 -> 0.8.x, whose deref guards the null/empty case), but has not cut a release; the bug is tracked open as prisma/tiberius#343. Pin the fixed commit (a6b4fcdae0de5702427290b89f8d05bc51f3bcfa) via [patch.crates-io] so libgssapi resolves to the guarded 0.8.3. Drop this patch and bump the `tiberius` version once a release ships. No TablePro code changes -- the mssql driver builds unchanged. --- linux/Cargo.lock | 297 ++++++++--------------------------------------- linux/Cargo.toml | 18 +-- 2 files changed, 58 insertions(+), 257 deletions(-) diff --git a/linux/Cargo.lock b/linux/Cargo.lock index 414948cd0..486e0b8b8 100644 --- a/linux/Cargo.lock +++ b/linux/Cargo.lock @@ -79,15 +79,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -136,7 +127,7 @@ dependencies = [ "futures-core", "libc", "portable-atomic", - "rustc-hash 2.1.2", + "rustc-hash", "tokio", "tokio-stream", "xattr", @@ -238,17 +229,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -358,33 +338,24 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.59.2" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cexpr", "clang-sys", - "clap", - "env_logger", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", - "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", - "which", + "syn 2.0.117", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.1" @@ -457,7 +428,7 @@ checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -593,7 +564,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cairo-sys-rs", "glib", "libc", @@ -720,21 +691,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - [[package]] name = "cmake" version = "0.1.58" @@ -936,7 +892,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", + "strsim", "syn 2.0.117", ] @@ -1158,19 +1114,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_logger" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1417,7 +1360,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.5", + "parking_lot", ] [[package]] @@ -1665,7 +1608,7 @@ version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b7df55594e0e787d1560e23f7e12d7360d0b22e7b7c228ec2488b9e59b1b6b" dependencies = [ - "bitflags 2.11.1", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -1933,15 +1876,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hex" version = "0.4.3" @@ -2026,12 +1960,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" - [[package]] name = "hyper" version = "1.9.0" @@ -2304,15 +2232,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "internal-russh-forked-ssh-key" version = "0.6.11+upstream-0.6.7" @@ -2341,6 +2260,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2387,12 +2315,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -2504,24 +2426,24 @@ dependencies = [ [[package]] name = "libgssapi" -version = "0.4.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "724dbcd1f871da9c67983537a47ac510c278656f6392418ad67c7a52720e54b2" +checksum = "b9e668df13f2e97f3eed52d9301f6b1c4c1ccfccc30eab9e6628e4a8c1fc3546" dependencies = [ - "bitflags 1.3.2", + "bitflags", "bytes", "lazy_static", "libgssapi-sys", - "parking_lot 0.11.2", ] [[package]] name = "libgssapi-sys" -version = "0.2.4" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd7d65e409c889f6c9d81ff079371d0d8fd88d7dca702ff187ef96fb0450fb7" +checksum = "5103ac4557eacd36ff678b654b943f8966d3db9688fbd180a0b4c5464759ce17" dependencies = [ "bindgen", + "pkg-config", ] [[package]] @@ -2546,7 +2468,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "plain", "redox_syscall 0.7.4", @@ -2563,12 +2485,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2703,7 +2619,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -3023,17 +2939,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.5" @@ -3041,21 +2946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.12", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -3123,12 +3014,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3355,7 +3240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3493,22 +3378,13 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3517,7 +3393,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3688,7 +3564,7 @@ checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548" dependencies = [ "aes", "aws-lc-rs", - "bitflags 2.11.1", + "bitflags", "block-padding", "byteorder", "bytes", @@ -3784,12 +3660,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.2" @@ -3805,29 +3675,16 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -4038,7 +3895,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4051,7 +3908,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4446,7 +4303,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags", "byteorder", "bytes", "chrono", @@ -4491,7 +4348,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags", "byteorder", "chrono", "crc", @@ -4595,12 +4452,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -4871,19 +4722,10 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "testcontainers" version = "0.27.3" @@ -4900,7 +4742,7 @@ dependencies = [ "ferroid", "futures", "http", - "itertools", + "itertools 0.14.0", "log", "memchr", "parse-display", @@ -4924,15 +4766,6 @@ dependencies = [ "testcontainers", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4985,8 +4818,7 @@ dependencies = [ [[package]] name = "tiberius" version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d" +source = "git+https://github.com/prisma/tiberius?rev=a6b4fcdae0de5702427290b89f8d05bc51f3bcfa#a6b4fcdae0de5702427290b89f8d05bc51f3bcfa" dependencies = [ "async-trait", "asynchronous-codec", @@ -5396,12 +5228,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -5512,12 +5338,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version-compare" version = "0.2.1" @@ -5653,7 +5473,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -5687,18 +5507,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "whoami" version = "1.6.1" @@ -5725,15 +5533,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -6080,7 +5879,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags", "indexmap 2.14.0", "log", "serde", @@ -6132,7 +5931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", ] [[package]] @@ -6174,7 +5973,7 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.4", + "rustix", "serde", "serde_repr", "tokio", diff --git a/linux/Cargo.toml b/linux/Cargo.toml index a3b864ab6..b1229961c 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -53,11 +53,13 @@ lto = "fat" codegen-units = 1 strip = "symbols" -# libgssapi 0.4.6 builds the empty final GSSAPI token with -# `slice::from_raw_parts(null, 0)`, which trips Rust's UB precondition -# check and aborts the process. The check is emitted per crate, so -# turning debug assertions off for this one package keeps `cargo run` -# usable for Kerberos logins without weakening them anywhere else. -# Drop this once the null guard lands upstream. -[profile.dev.package.libgssapi] -debug-assertions = false +# tiberius 0.12.3 (its latest release) pins libgssapi ^0.4.5. libgssapi 0.4.6's +# `Buf::deref` calls `slice::from_raw_parts(NULL, 0)` on the empty token a +# successful Kerberos handshake returns -- undefined behavior that aborts every +# debug build (`cargo run`). tiberius already fixed this on `main` (merged PR +# prisma/tiberius#372: bump libgssapi 0.4.5 -> 0.8.x, whose deref is guarded) +# but has not cut a release; the bug is tracked open as prisma/tiberius#343. +# Pin the fixed commit until a release lands, then drop this and bump the +# `tiberius` version above to the release. +[patch.crates-io] +tiberius = { git = "https://github.com/prisma/tiberius", rev = "a6b4fcdae0de5702427290b89f8d05bc51f3bcfa" } From d97f50025119e99d2b256537fe7983d08d20fca5 Mon Sep 17 00:00:00 2001 From: Serge Lapin Date: Tue, 28 Jul 2026 12:21:19 +0300 Subject: [PATCH 4/4] refactor(linux): always compile Kerberos in and trim the auth plumbing Kerberos login is unchanged for users: the connect dialog still offers Method -> Password / Windows (Kerberos), and AuthMode::Kerberos still maps to tiberius AuthMethod::Integrated. What goes away is the opt-out around it. The mssql `kerberos` cargo feature existed so a packager without MIT Kerberos could drop it, but ADR 0001 statically links every driver into one binary, so there is no unit to swap and no packager to serve. The tiberius `integrated-auth-gssapi` feature moves to the workspace dep and the opt-out's whole tail goes with the feature: DriverError::Unsupported and its UI string, the authentication_for indirection, the extra CI job, and the build-without-Kerberos README block. Alongside it, the rest of the branch's over-engineering: - ServiceEndpoint (one construction site) becomes Option<(String, u16)>, service_address() a single map_or. - collect_options() derives blanked credentials from shows_credentials() instead of re-deriving the mode per field, so the two cannot drift. - selected_auth_mode() dropped; callers use form.mode(). - Eight assertion-per-test cases collapse into one table test each for AuthFormState and service_address; the tests asserting #[derive(Default)] and re-testing serde round-trip are gone, the legacy-file default (real data-loss risk) stays. - Comments that restated the line under them, or duplicated another file's comment, removed. Verified: cargo fmt --check, clippy -D warnings, 279 unit tests, full workspace link (gssapi_krb5 now links unconditionally), app launches. A scratch test confirmed AuthMode::Kerberos reaches the socket and fails with ConnectionRefused rather than being compiled out. --- .github/workflows/build-linux.yml | 13 +- linux/Cargo.toml | 7 +- linux/README.md | 10 +- .../app/src/services/connection_service.rs | 14 +- linux/crates/app/src/ui/connect_dialog.rs | 148 ++++++++---------- linux/crates/app/src/ui/error_text.rs | 3 - linux/crates/core/src/connection.rs | 45 ++---- linux/crates/core/src/error.rs | 6 - linux/crates/core/src/lib.rs | 2 +- linux/crates/drivers/mssql/Cargo.toml | 6 - linux/crates/drivers/mssql/src/lib.rs | 57 ++----- linux/crates/storage/src/connections.rs | 10 +- 12 files changed, 104 insertions(+), 217 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index e9642f286..762f57e65 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -38,10 +38,10 @@ jobs: # ubuntu:25.10 is minimal — git for Swatinem/rust-cache key # generation, curl + ca-certificates for rust-toolchain's # rustup install, plus the GTK / libadwaita / sourceview / - # OpenSSL / libsecret / MIT Kerberos -dev packages the workspace - # links. libkrb5-dev + clang cover the MSSQL driver's integrated - # (Kerberos/GSSAPI) auth: libgssapi-sys links gssapi_krb5 and runs - # bindgen (needs libclang). + # OpenSSL / libsecret -dev packages the workspace links, plus + # libkrb5-dev + clang for the MSSQL driver's integrated + # (Kerberos/GSSAPI) auth (libgssapi-sys links gssapi_krb5 and + # runs bindgen). # No sudo (container runs as root by default). run: | apt-get update @@ -70,11 +70,6 @@ jobs: run: cargo clippy --all-targets -- -D warnings - name: Build run: cargo build --workspace - # The MSSQL driver's Kerberos support is a default-on cargo feature - # so packagers without MIT Kerberos / libclang can drop it. Check - # that path here or it rots. - - name: Build without Kerberos - run: cargo check -p tablepro-driver-mssql --no-default-features --all-targets # --bins matters: tablepro-app has no lib target, so --lib alone # skips every test in the app crate. - name: Unit tests diff --git a/linux/Cargo.toml b/linux/Cargo.toml index b1229961c..b444a8ec1 100644 --- a/linux/Cargo.toml +++ b/linux/Cargo.toml @@ -30,10 +30,9 @@ secrecy = { version = "0.10", features = ["serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "chrono", "rust_decimal", "uuid", "json"] } -# `integrated-auth-gssapi` is not listed here: it drags in MIT Kerberos -# and bindgen, so it hangs off the mssql crate's `kerberos` feature -# (default on) where a packager can turn it off. -tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal"] } +# `integrated-auth-gssapi` (Windows integrated auth) links MIT Kerberos +# (libkrb5) and runs bindgen (libclang) at build time. +tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono", "rust_decimal", "integrated-auth-gssapi"] } chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } rust_decimal = { version = "1", default-features = false, features = ["serde", "std"] } futures = "0.3" diff --git a/linux/README.md b/linux/README.md index 62ba348fa..79628c00f 100644 --- a/linux/README.md +++ b/linux/README.md @@ -56,13 +56,9 @@ cd linux cargo run -p tablepro-app ``` -`libkrb5-dev` and `clang` are only there for the SQL Server driver's -Windows integrated auth, which links MIT Kerberos and runs bindgen. Drop -both packages and build without it: - -```bash -cargo build --workspace --no-default-features -``` +`libkrb5-dev` and `clang` are there for the SQL Server driver's Windows +integrated auth, which links MIT Kerberos and runs bindgen at build +time. ## SQL Server with Windows integrated auth diff --git a/linux/crates/app/src/services/connection_service.rs b/linux/crates/app/src/services/connection_service.rs index 6354163e4..c7578ffa4 100644 --- a/linux/crates/app/src/services/connection_service.rs +++ b/linux/crates/app/src/services/connection_service.rs @@ -1,9 +1,7 @@ use std::sync::Arc; use secrecy::SecretString; -use tablepro_core::{ - AuthMode, ConnectOptions, Connection, DriverRegistry, ReadOnlyConnection, ServiceEndpoint, TableInfo, -}; +use tablepro_core::{AuthMode, ConnectOptions, Connection, DriverRegistry, ReadOnlyConnection, TableInfo}; use tablepro_ssh::{SshConfig, SshTunnel}; use tablepro_storage::{SavedConnection, SavedSshAuth, load_password, load_ssh_passphrase, load_ssh_password}; @@ -71,13 +69,9 @@ pub async fn establish( .await .map_err(|e| format!("ssh: {e}"))?; // The socket now points at the local forward, so remember what - // the service is actually called. Kerberos builds its SPN from - // it; without this a tunnelled connection would ask the KDC for - // MSSQLSvc/127.0.0.1:. - opts.service_endpoint = Some(ServiceEndpoint { - host: remote_host, - port: remote_port, - }); + // the service is actually called; without this Kerberos would + // ask the KDC for MSSQLSvc/127.0.0.1:. + opts.service_endpoint = Some((remote_host, remote_port)); opts.host = tun.local_host().to_string(); opts.port = tun.local_port(); Some(tun) diff --git a/linux/crates/app/src/ui/connect_dialog.rs b/linux/crates/app/src/ui/connect_dialog.rs index a6a7a9d83..8f75027a9 100644 --- a/linux/crates/app/src/ui/connect_dialog.rs +++ b/linux/crates/app/src/ui/connect_dialog.rs @@ -45,8 +45,7 @@ struct DriverEntry { const KERBEROS_ROW: u32 = 1; /// What the selected driver allows, kept beside the widgets so the form -/// never has to read its own visibility flags back to work out what the -/// user picked. +/// never reads its own visibility flags back to work out the mode. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] struct AuthFormState { file_based: bool, @@ -55,9 +54,9 @@ struct AuthFormState { } impl AuthFormState { - /// Kerberos only applies to a network driver that supports it. A - /// stale selection left over from another driver resolves back to - /// password auth instead of leaking across the switch. + /// A stale Kerberos selection left over from another driver + /// resolves back to password auth instead of leaking across the + /// switch. fn mode(self) -> AuthMode { if !self.file_based && self.supports_integrated && self.kerberos_selected { AuthMode::Kerberos @@ -227,9 +226,6 @@ impl Component for ConnectDialog { connection_group.add(&port); connection_group.add(&database); - // MSSQL supports Windows integrated (Kerberos) auth alongside SQL - // logins; the selector is revealed only for integrated-capable - // drivers (see AuthFormState::shows_method). let auth_password_label = crate::tr!("Password"); let auth_kerberos_label = crate::tr!("Windows (Kerberos)"); let auth_mode_model = gtk::StringList::new(&[auth_password_label.as_str(), auth_kerberos_label.as_str()]); @@ -324,8 +320,6 @@ impl Component for ConnectDialog { self.port.set_value(driver.default_port() as f64); } root.set_title(&crate::tr!("Connect to {name}").replace("{name}", &entry.display_name)); - // Which fields are required just changed, so the submit - // state has to be recomputed against the new driver. self.refresh_validity(); } @@ -544,31 +538,23 @@ impl ConnectDialog { self.password.set_visible(credentials); } - fn selected_auth_mode(&self) -> AuthMode { - self.form.mode() - } - fn collect_options(&self) -> ConnectOptions { - // Kerberos ignores the credential rows, and whatever the user - // typed before switching modes must not reach the driver or the - // keyring, so it never leaves the widget. - let kerberos = self.selected_auth_mode() == AuthMode::Kerberos; + // Kerberos ignores the credential rows, so whatever the user + // typed before switching modes never reaches the driver or the + // keyring. + let (username, password) = if self.form.shows_credentials() { + (self.username.text().to_string(), self.password.text().to_string()) + } else { + (String::new(), String::new()) + }; ConnectOptions { host: self.host.text().to_string(), port: self.port.value() as u16, database: self.database.text().to_string(), - username: if kerberos { - String::new() - } else { - self.username.text().to_string() - }, - password: if kerberos { - SecretString::new(String::new().into()) - } else { - SecretString::new(self.password.text().to_string().into()) - }, + username, + password: SecretString::new(password.into()), use_tls: self.use_tls.is_active(), - auth_mode: self.selected_auth_mode(), + auth_mode: self.form.mode(), service_endpoint: None, } } @@ -720,57 +706,59 @@ fn saved_ssh_matches(saved: &Option, current: Option<&SshInputs> mod tests { use super::*; - fn mssql(kerberos_selected: bool) -> AuthFormState { - AuthFormState { - file_based: false, - supports_integrated: true, - kerberos_selected, - } - } - + /// (state, mode, shows_method, shows_credentials) #[test] - fn password_is_the_default_everywhere() { - let state = AuthFormState::default(); - assert_eq!(state.mode(), AuthMode::Password); - assert!(!state.shows_method()); - assert!(state.shows_credentials()); - } - - #[test] - fn integrated_driver_offers_the_method_selector() { - assert!(mssql(false).shows_method()); - assert_eq!(mssql(false).mode(), AuthMode::Password); - assert!(mssql(false).shows_credentials()); - } - - #[test] - fn kerberos_hides_the_credential_rows() { - assert_eq!(mssql(true).mode(), AuthMode::Kerberos); - assert!(!mssql(true).shows_credentials()); - assert!(mssql(true).shows_method()); - } - - #[test] - fn a_stale_kerberos_selection_does_not_survive_a_driver_switch() { - let postgres = AuthFormState { - file_based: false, - supports_integrated: false, - kerberos_selected: true, - }; - assert_eq!(postgres.mode(), AuthMode::Password); - assert!(!postgres.shows_method()); - assert!(postgres.shows_credentials()); - } - - #[test] - fn file_based_drivers_ask_for_no_credentials() { - let sqlite = AuthFormState { - file_based: true, - supports_integrated: true, - kerberos_selected: true, - }; - assert_eq!(sqlite.mode(), AuthMode::Password); - assert!(!sqlite.shows_method()); - assert!(!sqlite.shows_credentials()); + fn auth_form_state_drives_mode_and_visibility() { + let cases = [ + (AuthFormState::default(), AuthMode::Password, false, true), + // MSSQL: offers the selector, password until Kerberos is picked. + ( + AuthFormState { + file_based: false, + supports_integrated: true, + kerberos_selected: false, + }, + AuthMode::Password, + true, + true, + ), + ( + AuthFormState { + file_based: false, + supports_integrated: true, + kerberos_selected: true, + }, + AuthMode::Kerberos, + true, + false, + ), + // Postgres: a stale Kerberos selection does not survive the switch. + ( + AuthFormState { + file_based: false, + supports_integrated: false, + kerberos_selected: true, + }, + AuthMode::Password, + false, + true, + ), + // SQLite: no credentials at all. + ( + AuthFormState { + file_based: true, + supports_integrated: true, + kerberos_selected: true, + }, + AuthMode::Password, + false, + false, + ), + ]; + for (state, mode, method, credentials) in cases { + assert_eq!(state.mode(), mode, "{state:?}"); + assert_eq!(state.shows_method(), method, "{state:?}"); + assert_eq!(state.shows_credentials(), credentials, "{state:?}"); + } } } diff --git a/linux/crates/app/src/ui/error_text.rs b/linux/crates/app/src/ui/error_text.rs index 054599d3d..7d43e9d73 100644 --- a/linux/crates/app/src/ui/error_text.rs +++ b/linux/crates/app/src/ui/error_text.rs @@ -32,9 +32,6 @@ pub fn driver_message(error: &DriverError) -> String { crate::tr!("This connection is read-only. Reopen it without read-only mode to make changes.") } DriverError::Internal(detail) => crate::tr!("Internal driver error: {detail}").replace("{detail}", detail), - DriverError::Unsupported(detail) => { - crate::tr!("Not supported by this build: {detail}").replace("{detail}", detail) - } DriverError::Transaction { statement_index, source, diff --git a/linux/crates/core/src/connection.rs b/linux/crates/core/src/connection.rs index 4881530e2..432da9680 100644 --- a/linux/crates/core/src/connection.rs +++ b/linux/crates/core/src/connection.rs @@ -17,21 +17,9 @@ pub enum AuthMode { Password, /// Windows integrated authentication over Kerberos (GSSAPI), using /// the ambient ticket cache. `username`/`password` are ignored. - /// Drivers that report `supports_integrated_auth() == false` reject - /// this mode rather than silently authenticating some other way. Kerberos, } -/// Host and port the database service is known by, kept separate from -/// the socket address when an SSH tunnel rewrites `host`/`port` to a -/// local forward. Anything that names the *service* rather than the -/// socket (a Kerberos SPN, a TLS server name) has to read this. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ServiceEndpoint { - pub host: String, - pub port: u16, -} - #[derive(Debug, Clone)] pub struct ConnectOptions { pub host: String, @@ -43,17 +31,16 @@ pub struct ConnectOptions { pub auth_mode: AuthMode, /// Set only when `host`/`port` were replaced by a tunnel's local /// forward. `None` means the socket already points at the service. - pub service_endpoint: Option, + pub service_endpoint: Option<(String, u16)>, } impl ConnectOptions { /// Host and port the service answers to, which is `host`/`port` /// unless a tunnel replaced them. pub fn service_address(&self) -> (&str, u16) { - match &self.service_endpoint { - Some(endpoint) => (endpoint.host.as_str(), endpoint.port), - None => (self.host.as_str(), self.port), - } + self.service_endpoint + .as_ref() + .map_or((self.host.as_str(), self.port), |(host, port)| (host.as_str(), *port)) } } @@ -133,32 +120,20 @@ mod tests { use super::*; #[test] - fn service_address_falls_back_to_the_socket_address() { - let opts = ConnectOptions { + fn service_address_prefers_the_tunnelled_service_over_the_socket() { + let direct = ConnectOptions { host: "sql.corp.example".into(), port: 1433, ..Default::default() }; - assert_eq!(opts.service_address(), ("sql.corp.example", 1433)); - } + assert_eq!(direct.service_address(), ("sql.corp.example", 1433)); - #[test] - fn service_address_survives_a_tunnel_rewrite() { - let opts = ConnectOptions { + let tunnelled = ConnectOptions { host: "127.0.0.1".into(), port: 54321, - service_endpoint: Some(ServiceEndpoint { - host: "sql.corp.example".into(), - port: 1433, - }), + service_endpoint: Some(("sql.corp.example".into(), 1433)), ..Default::default() }; - assert_eq!(opts.service_address(), ("sql.corp.example", 1433)); - } - - #[test] - fn auth_mode_defaults_to_password() { - assert_eq!(AuthMode::default(), AuthMode::Password); - assert_eq!(ConnectOptions::default().auth_mode, AuthMode::Password); + assert_eq!(tunnelled.service_address(), ("sql.corp.example", 1433)); } } diff --git a/linux/crates/core/src/error.rs b/linux/crates/core/src/error.rs index b31dd3422..771adf4cb 100644 --- a/linux/crates/core/src/error.rs +++ b/linux/crates/core/src/error.rs @@ -23,12 +23,6 @@ pub enum DriverError { #[error("driver internal error: {0}")] Internal(String), - /// The driver understood the request but this binary cannot serve - /// it, typically because an optional cargo feature was compiled - /// out. Distinct from `Internal`: nothing went wrong at runtime. - #[error("not supported by this build: {0}")] - Unsupported(String), - /// Returned by `Connection::execute_in_transaction` when one of the /// statements failed; the index identifies which statement (so the /// UI can highlight the offending row) and `source` carries the diff --git a/linux/crates/core/src/lib.rs b/linux/crates/core/src/lib.rs index 214590b52..c36bdd842 100644 --- a/linux/crates/core/src/lib.rs +++ b/linux/crates/core/src/lib.rs @@ -8,7 +8,7 @@ mod registry; pub mod sql_ddl; pub mod sql_dialect; -pub use connection::{AuthMode, ConnectOptions, Connection, ServiceEndpoint}; +pub use connection::{AuthMode, ConnectOptions, Connection}; pub use driver::DatabaseDriver; pub use error::DriverError; pub use filter::{BuildFilterError, Combinator, FilterOp, FilterRule, FilterSet, FilterValue, build_filter_where}; diff --git a/linux/crates/drivers/mssql/Cargo.toml b/linux/crates/drivers/mssql/Cargo.toml index 4f2c61577..7ee26aa61 100644 --- a/linux/crates/drivers/mssql/Cargo.toml +++ b/linux/crates/drivers/mssql/Cargo.toml @@ -9,12 +9,6 @@ publish = false name = "drivers_mssql" path = "src/lib.rs" -[features] -default = ["kerberos"] -# Windows integrated auth. Links MIT Kerberos (libkrb5) and runs bindgen -# (libclang) at build time; build with --no-default-features to drop both. -kerberos = ["tiberius/integrated-auth-gssapi"] - [dependencies] tablepro-core = { path = "../../core" } async-trait.workspace = true diff --git a/linux/crates/drivers/mssql/src/lib.rs b/linux/crates/drivers/mssql/src/lib.rs index 4b49d0480..6cd00c059 100644 --- a/linux/crates/drivers/mssql/src/lib.rs +++ b/linux/crates/drivers/mssql/src/lib.rs @@ -43,7 +43,7 @@ impl DatabaseDriver for MssqlDriver { } fn supports_integrated_auth(&self) -> bool { - cfg!(feature = "kerberos") + true } async fn connect(&self, opts: ConnectOptions) -> Result, DriverError> { @@ -57,7 +57,13 @@ impl DatabaseDriver for MssqlDriver { config.host(service_host); config.port(service_port); config.database(&opts.database); - config.authentication(authentication_for(&opts)?); + // `Integrated` reads the ambient Kerberos ticket cache (from + // `kinit`) and targets MSSQLSvc/:; username + // and password are ignored. + config.authentication(match opts.auth_mode { + AuthMode::Password => AuthMethod::sql_server(&opts.username, opts.password.expose_secret()), + AuthMode::Kerberos => AuthMethod::Integrated, + }); // SQL Server always encrypts the login exchange; `Off` keeps the // post-login stream in the clear, `Required` encrypts everything. // No cert-path UI exists, so the server certificate is trusted @@ -74,9 +80,7 @@ impl DatabaseDriver for MssqlDriver { // and an unreachable host would otherwise hang the connect // dialog for the OS SYN timeout. The budget covers both so the // failure arrives on the same scale as the sqlx drivers' - // acquire_timeout. The socket targets opts.host/opts.port (an - // SSH tunnel's local forward when present); `config` already - // names the real service for the SPN and TLS server name. + // acquire_timeout. tokio::time::timeout(CONNECT_TIMEOUT, async { let tcp = TcpStream::connect((opts.host.as_str(), opts.port)) .await @@ -96,26 +100,6 @@ impl DatabaseDriver for MssqlDriver { } } -/// Windows integrated auth over Kerberos (GSSAPI) needs tiberius's -/// `integrated-auth-gssapi` feature, which links MIT Kerberos and runs -/// bindgen at build time. The `kerberos` feature keeps that opt-out-able -/// for packagers, so the mode has to fail loudly when it is compiled out -/// rather than quietly falling back to a SQL login. -fn authentication_for(opts: &ConnectOptions) -> Result { - match opts.auth_mode { - AuthMode::Password => Ok(AuthMethod::sql_server(&opts.username, opts.password.expose_secret())), - #[cfg(feature = "kerberos")] - // tiberius reads the ambient ticket cache (from `kinit`) and - // targets MSSQLSvc/:; username and password - // are ignored. - AuthMode::Kerberos => Ok(AuthMethod::Integrated), - #[cfg(not(feature = "kerberos"))] - AuthMode::Kerberos => Err(DriverError::Unsupported( - "Kerberos authentication (cargo feature `kerberos`) was not compiled in".into(), - )), - } -} - struct MssqlConnection { client: Mutex, } @@ -715,28 +699,7 @@ mod tests { assert_eq!(d.display_name(), "SQL Server"); assert_eq!(d.default_port(), 1433); assert!(!d.is_file_based()); - assert_eq!(d.supports_integrated_auth(), cfg!(feature = "kerberos")); - } - - #[test] - fn kerberos_matches_the_advertised_capability() { - let opts = ConnectOptions { - auth_mode: AuthMode::Kerberos, - ..Default::default() - }; - assert_eq!( - authentication_for(&opts).is_ok(), - MssqlDriver.supports_integrated_auth() - ); - } - - #[test] - fn password_auth_is_always_available() { - let opts = ConnectOptions { - username: "sa".into(), - ..Default::default() - }; - assert!(authentication_for(&opts).is_ok()); + assert!(d.supports_integrated_auth()); } #[test] diff --git a/linux/crates/storage/src/connections.rs b/linux/crates/storage/src/connections.rs index 8c58f4677..0da3a0e42 100644 --- a/linux/crates/storage/src/connections.rs +++ b/linux/crates/storage/src/connections.rs @@ -234,17 +234,9 @@ mod tests { } #[tokio::test] - async fn auth_mode_round_trips_and_defaults_to_password() { + async fn auth_mode_defaults_to_password_on_a_legacy_file() { let dir = TempDir::new().unwrap(); let path = dir.path().join("connections.json"); - - // Explicit Kerberos survives a save/load cycle. - let mut conn = sample_connection(); - conn.auth_mode = AuthMode::Kerberos; - save_to(&path, &[conn.clone()]).await.unwrap(); - assert_eq!(load_from(&path).await.unwrap(), vec![conn]); - - // A legacy file without the field loads as Password. let id = Uuid::new_v4(); let legacy = format!( r#"{{"version":1,"connections":[{{