From 42fc9ee2dc727e856ad102879a17b6a8b159ec1c Mon Sep 17 00:00:00 2001 From: Louis Frei Date: Thu, 13 Aug 2026 13:25:45 +0200 Subject: [PATCH] initla code --- .gitignore | 19 ++++++++++++++ Cargo.toml | 38 ++++++++++++++++++++++++++++ README.md | 23 +++++++++++++++++ rust-toolchain.toml | 3 +++ src/adapters/azure_cli.rs | 2 ++ src/adapters/mod.rs | 1 + src/adapters/postgres.rs | 5 ++++ src/adapters/process.rs | 3 +++ src/app/controller.rs | 3 +++ src/app/events.rs | 2 ++ src/app/mod.rs | 1 + src/app/state.rs | 1 + src/domain/catalog.rs | 10 ++++++++ src/domain/ids.rs | 9 +++++++ src/domain/inventory.rs | 7 ++++++ src/domain/mod.rs | 6 +++++ src/domain/permissions.rs | 8 ++++++ src/domain/profile.rs | 6 +++++ src/domain/sql.rs | 9 +++++++ src/error.rs | 48 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 +++++++ src/main.rs | 7 ++++++ src/platform/clipboard.rs | 1 + src/platform/clock.rs | 1 + src/platform/keychain.rs | 1 + src/platform/mod.rs | 1 + src/platform/paths.rs | 2 ++ src/platform/profile_file.rs | 1 + src/platform/secret_input.rs | 1 + src/services/catalog.rs | 1 + src/services/discovery.rs | 1 + src/services/mod.rs | 1 + src/services/profiles.rs | 1 + src/services/roles.rs | 1 + src/services/sql_executor.rs | 1 + src/services/tables.rs | 1 + src/tui/mod.rs | 1 + 37 files changed, 236 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 rust-toolchain.toml create mode 100644 src/adapters/azure_cli.rs create mode 100644 src/adapters/mod.rs create mode 100644 src/adapters/postgres.rs create mode 100644 src/adapters/process.rs create mode 100644 src/app/controller.rs create mode 100644 src/app/events.rs create mode 100644 src/app/mod.rs create mode 100644 src/app/state.rs create mode 100644 src/domain/catalog.rs create mode 100644 src/domain/ids.rs create mode 100644 src/domain/inventory.rs create mode 100644 src/domain/mod.rs create mode 100644 src/domain/permissions.rs create mode 100644 src/domain/profile.rs create mode 100644 src/domain/sql.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/platform/clipboard.rs create mode 100644 src/platform/clock.rs create mode 100644 src/platform/keychain.rs create mode 100644 src/platform/mod.rs create mode 100644 src/platform/paths.rs create mode 100644 src/platform/profile_file.rs create mode 100644 src/platform/secret_input.rs create mode 100644 src/services/catalog.rs create mode 100644 src/services/discovery.rs create mode 100644 src/services/mod.rs create mode 100644 src/services/profiles.rs create mode 100644 src/services/roles.rs create mode 100644 src/services/sql_executor.rs create mode 100644 src/services/tables.rs create mode 100644 src/tui/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f5f115 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +target/ +.DS_Store +.env* +*.pem +*.key +*.crt +*.cer +*.p12 +*.diagnostic +*.log +*.dump +.idea/ +.vscode/ +postgres-test-data/ +local-test-data/ +diagnostics/ +release-archives/ +*.credential-test +*.credentials-test diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..39d14be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "azure-database-tui" +version = "0.1.0" +edition = "2024" +rust-version = "1.85" + +[features] +postgres-integration = [] + +[dependencies] +tokio = { version = "1.43", features = ["fs", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "sync", "time"] } +ratatui = "0.29" +crossterm = "0.28" +async-trait = "0.1" +futures-util = "0.3" +tokio-util = "0.7" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["serde", "v4"] } +thiserror = "2" +zeroize = "1" +secrecy = "0.10" +getrandom = "0.3" +sha2 = "0.10" +bytes = "1" +directories = "6" +rustls = "0.23" +rustls-native-certs = "0.8" +rustls-pemfile = "2" +tokio-postgres = { version = "0.7", features = ["with-serde_json-1"] } +tokio-postgres-rustls = "0.13" + +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +keyring = "3" +arboard = "3" + +[dev-dependencies] +tempfile = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..4154144 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Azure Database TUI + +This repository contains a Rust TUI for administering Azure Database for +PostgreSQL flexible servers. It discovers servers through the current Azure CLI +tenant and connects to PostgreSQL using mandatory TLS. + +## Prerequisites + +Install Azure CLI and authenticate to the intended tenant. Network access to +the server endpoint, including private endpoint, firewall, and DNS access, is +required. Azure CLI visibility does not imply PostgreSQL authorization. + +## Build and test + +Use the pinned Rust toolchain with `cargo build`, `cargo test`, and the local +integration helper documented in `docs/operations.md`. + +Supported release targets are macOS ARM64 and Windows x86_64. Raw SQL is sent +to PostgreSQL as entered; use it only when you understand its authority and +effects. + +See [security](docs/security.md), [operations](docs/operations.md), and +[release](docs/release.md). diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..b475f2f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.85.0" +components = ["rustfmt", "clippy"] diff --git a/src/adapters/azure_cli.rs b/src/adapters/azure_cli.rs new file mode 100644 index 0000000..f0ce391 --- /dev/null +++ b/src/adapters/azure_cli.rs @@ -0,0 +1,2 @@ +pub struct AzureDiscovery; +pub struct AzureCliAdapter; diff --git a/src/adapters/mod.rs b/src/adapters/mod.rs new file mode 100644 index 0000000..41c39fe --- /dev/null +++ b/src/adapters/mod.rs @@ -0,0 +1 @@ +pub mod process; pub mod azure_cli; pub mod postgres; diff --git a/src/adapters/postgres.rs b/src/adapters/postgres.rs new file mode 100644 index 0000000..b8add12 --- /dev/null +++ b/src/adapters/postgres.rs @@ -0,0 +1,5 @@ +pub struct PostgresConnector; +pub struct SessionManager; +pub struct SqlSession; +pub struct GuidedSession; +pub struct ConnectionRequest; diff --git a/src/adapters/process.rs b/src/adapters/process.rs new file mode 100644 index 0000000..54ee0f5 --- /dev/null +++ b/src/adapters/process.rs @@ -0,0 +1,3 @@ +#[derive(Debug, Clone)] pub struct CommandSpec; +pub trait CommandRunner {} +pub struct TokioCommandRunner; diff --git a/src/app/controller.rs b/src/app/controller.rs new file mode 100644 index 0000000..5bf73b5 --- /dev/null +++ b/src/app/controller.rs @@ -0,0 +1,3 @@ +use crate::error::AppError; +pub struct AppController; +impl AppController { pub async fn new() -> Result { Err(AppError(crate::error::SafeError { kind: crate::error::ErrorKind::Internal, message: "Application unavailable", sqlstate: None, severity: crate::error::ErrorSeverity::Error, retryable: false, operation_id: None })) } pub async fn run(&mut self) -> Result<(), AppError> { Ok(()) } } diff --git a/src/app/events.rs b/src/app/events.rs new file mode 100644 index 0000000..7397886 --- /dev/null +++ b/src/app/events.rs @@ -0,0 +1,2 @@ +#[derive(Debug, Clone)] pub struct AppCommand; +#[derive(Debug, Clone)] pub struct AppEvent; diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..c219775 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1 @@ +pub mod controller; pub mod events; pub mod state; diff --git a/src/app/state.rs b/src/app/state.rs new file mode 100644 index 0000000..8229c49 --- /dev/null +++ b/src/app/state.rs @@ -0,0 +1 @@ +#[derive(Debug, Clone)] pub struct AppState; diff --git a/src/domain/catalog.rs b/src/domain/catalog.rs new file mode 100644 index 0000000..2d61fe4 --- /dev/null +++ b/src/domain/catalog.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone)] pub struct CatalogSnapshot; +#[derive(Debug, Clone)] pub struct SchemaMeta; +#[derive(Debug, Clone)] pub struct RelationMeta; +#[derive(Debug, Clone)] pub struct ColumnMeta; +#[derive(Debug, Clone)] pub struct KeysetDefinition; +#[derive(Debug, Clone)] pub struct FunctionMeta; +#[derive(Debug, Clone)] pub struct RoleMeta; +#[derive(Debug, Clone)] pub struct PrivilegeObservation; +#[derive(Debug, Clone)] pub struct DefaultPrivilegeObservation; +#[derive(Debug, Clone)] pub struct ServerCapabilities; diff --git a/src/domain/ids.rs b/src/domain/ids.rs new file mode 100644 index 0000000..e124d13 --- /dev/null +++ b/src/domain/ids.rs @@ -0,0 +1,9 @@ +use uuid::Uuid; + +macro_rules! text_id { ($name:ident) => { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct $name(String); impl $name { pub fn new(value: impl Into) -> Self { Self(value.into()) } pub fn as_str(&self) -> &str { &self.0 } } }; } +text_id!(TenantId); text_id!(SubscriptionId); text_id!(AzureResourceId); +text_id!(CatalogGeneration); text_id!(RoleOid); text_id!(RelationOid); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ProfileId(Uuid); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct OperationId(Uuid); +impl ProfileId { pub fn new() -> Self { Self(Uuid::new_v4()) } } +impl OperationId { pub fn new() -> Self { Self(Uuid::new_v4()) } } diff --git a/src/domain/inventory.rs b/src/domain/inventory.rs new file mode 100644 index 0000000..458fa6d --- /dev/null +++ b/src/domain/inventory.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone)] pub struct Tenant; +#[derive(Debug, Clone)] pub struct Subscription; +#[derive(Debug, Clone)] pub struct FlexibleServer; +#[derive(Debug, Clone)] pub struct DatabaseRef; +#[derive(Debug, Clone)] pub struct DiscoveryFailure; +#[derive(Debug, Clone)] pub struct DiscoveryReport; +#[derive(Debug, Clone)] pub struct DiscoveryEvent; diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..721fdc0 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,6 @@ +pub mod catalog; +pub mod ids; +pub mod inventory; +pub mod permissions; +pub mod profile; +pub mod sql; diff --git a/src/domain/permissions.rs b/src/domain/permissions.rs new file mode 100644 index 0000000..66e6723 --- /dev/null +++ b/src/domain/permissions.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone)] pub struct DesiredPrivilegeProfile; +#[derive(Debug, Clone)] pub struct PrivilegeScope; +#[derive(Debug, Clone)] pub struct PrivilegeKind; +#[derive(Debug, Clone)] pub struct RoleMembershipIntent; +#[derive(Debug, Clone)] pub struct Grantability; +#[derive(Debug, Clone)] pub struct PlanOperation; +#[derive(Debug, Clone)] pub struct PermissionPlan; +#[derive(Debug, Clone)] pub struct PlanResult; diff --git a/src/domain/profile.rs b/src/domain/profile.rs new file mode 100644 index 0000000..296b572 --- /dev/null +++ b/src/domain/profile.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone)] pub struct ProfileFile; +#[derive(Debug, Clone)] pub struct ConnectionProfile; +#[derive(Debug, Clone)] pub struct SecretReference; +#[derive(Debug, Clone)] pub struct TlsPolicy; +#[derive(Debug, Clone)] pub struct ConnectionDraft; +#[derive(Debug, Clone)] pub struct ProfileValidationError; diff --git a/src/domain/sql.rs b/src/domain/sql.rs new file mode 100644 index 0000000..4866e2b --- /dev/null +++ b/src/domain/sql.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Clone)] pub struct StatementRange; +pub struct SqlSubmission; +#[derive(Debug, Clone)] pub struct SqlRunRequest; +#[derive(Debug, Clone)] pub struct SqlExecutionEvent; +#[derive(Debug, Clone)] pub struct ResultSetMeta; +pub struct DisplayCell; +#[derive(Debug, Clone)] pub struct CopyDirection; +#[derive(Debug, Clone)] pub struct TransactionState; +#[derive(Debug, Clone)] pub struct SqlLexer; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1bf1ecf --- /dev/null +++ b/src/error.rs @@ -0,0 +1,48 @@ +use std::fmt; +use crate::domain::ids::OperationId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { Internal, Input, Network, Database, Process } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorSeverity { Info, Warning, Error } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SqlStateCode([u8; 5]); + +impl SqlStateCode { + pub fn parse(value: &str) -> Option { + let bytes = value.as_bytes(); + if bytes.len() == 5 && bytes.iter().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) { + Some(Self(bytes.try_into().ok()?)) + } else { None } + } +} + +#[derive(Debug, Clone)] +pub struct SafeError { + pub kind: ErrorKind, + pub message: &'static str, + pub sqlstate: Option, + pub severity: ErrorSeverity, + pub retryable: bool, + pub operation_id: Option, +} + +#[derive(Debug, Clone)] +pub struct AppError(pub SafeError); + +#[derive(Debug, Clone)] +pub struct SafeDiagnostic { + pub error: SafeError, +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.message.fmt(f) } +} + +impl std::error::Error for AppError {} + +pub fn redact_sensitive_text(input: &str) -> String { + if input.is_empty() { String::new() } else { "[diagnostic details omitted]".to_owned() } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..df29320 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +#![forbid(unsafe_code)] + +pub mod error; +pub mod adapters; +pub mod app; +pub mod domain; +pub mod platform; +pub mod services; +pub mod tui; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ae69c53 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +use azure_database_tui::app::controller::AppController; + +#[tokio::main] +async fn main() -> Result<(), azure_database_tui::error::AppError> { + let mut controller = AppController::new().await?; + controller.run().await +} diff --git a/src/platform/clipboard.rs b/src/platform/clipboard.rs new file mode 100644 index 0000000..454e97f --- /dev/null +++ b/src/platform/clipboard.rs @@ -0,0 +1 @@ +pub trait ClipboardService {} pub struct SystemClipboardService; diff --git a/src/platform/clock.rs b/src/platform/clock.rs new file mode 100644 index 0000000..f96a5ce --- /dev/null +++ b/src/platform/clock.rs @@ -0,0 +1 @@ +pub trait Clock {} pub struct SystemClock; diff --git a/src/platform/keychain.rs b/src/platform/keychain.rs new file mode 100644 index 0000000..a73cbda --- /dev/null +++ b/src/platform/keychain.rs @@ -0,0 +1 @@ +pub trait SecretStore {} pub struct KeyringSecretStore; pub struct UnavailableSecretStore; diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..c08c01f --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1 @@ +pub mod paths; pub mod clock; pub mod profile_file; pub mod keychain; pub mod clipboard; pub mod secret_input; diff --git a/src/platform/paths.rs b/src/platform/paths.rs new file mode 100644 index 0000000..00c6ab2 --- /dev/null +++ b/src/platform/paths.rs @@ -0,0 +1,2 @@ +pub struct AppPaths; +impl AppPaths { pub fn discover() -> Self { Self } } diff --git a/src/platform/profile_file.rs b/src/platform/profile_file.rs new file mode 100644 index 0000000..1e53295 --- /dev/null +++ b/src/platform/profile_file.rs @@ -0,0 +1 @@ +pub struct ProfileFileStore; diff --git a/src/platform/secret_input.rs b/src/platform/secret_input.rs new file mode 100644 index 0000000..079f083 --- /dev/null +++ b/src/platform/secret_input.rs @@ -0,0 +1 @@ +pub struct SecretInputBuffer; pub struct SecretHandle; pub struct OneTimeSecret; pub struct SecretVault; diff --git a/src/services/catalog.rs b/src/services/catalog.rs new file mode 100644 index 0000000..e811ba2 --- /dev/null +++ b/src/services/catalog.rs @@ -0,0 +1 @@ +pub struct CatalogService; diff --git a/src/services/discovery.rs b/src/services/discovery.rs new file mode 100644 index 0000000..f7fb40f --- /dev/null +++ b/src/services/discovery.rs @@ -0,0 +1 @@ +pub struct DiscoveryService; diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..6bf950d --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1 @@ +pub mod discovery; pub mod profiles; pub mod catalog; pub mod tables; pub mod roles; pub mod sql_executor; diff --git a/src/services/profiles.rs b/src/services/profiles.rs new file mode 100644 index 0000000..8ef9812 --- /dev/null +++ b/src/services/profiles.rs @@ -0,0 +1 @@ +pub struct ProfileService; diff --git a/src/services/roles.rs b/src/services/roles.rs new file mode 100644 index 0000000..1dcd25d --- /dev/null +++ b/src/services/roles.rs @@ -0,0 +1 @@ +pub struct RoleService; diff --git a/src/services/sql_executor.rs b/src/services/sql_executor.rs new file mode 100644 index 0000000..6bb9ca9 --- /dev/null +++ b/src/services/sql_executor.rs @@ -0,0 +1 @@ +pub struct SqlExecutor; diff --git a/src/services/tables.rs b/src/services/tables.rs new file mode 100644 index 0000000..6ebe16c --- /dev/null +++ b/src/services/tables.rs @@ -0,0 +1 @@ +pub struct TableService; diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..8c5eea1 --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1 @@ +pub struct TerminalGuard;