From 54f09fa7f1699e7fb539fe922b91b336eb1be52f Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 26 Jul 2024 22:37:26 +0200 Subject: [PATCH 01/13] boilerplate --- .bumpversion.cfg | 4 + Cargo.lock | 21 ++- Cargo.toml | 1 + README.md | 4 +- justfile | 2 +- tests/conftest.py | 10 +- warpgate-common/Cargo.toml | 2 + warpgate-common/src/config/defaults.rs | 6 + warpgate-common/src/config/mod.rs | 42 ++++++ warpgate-common/src/config/target.rs | 20 +++ .../src/tls/maybe_tls_stream.rs | 0 warpgate-common/src/tls/mod.rs | 7 + .../src/tls/rustls_helpers.rs | 3 +- .../src/tls/rustls_root_certs.rs | 0 warpgate-core/src/config_providers/db.rs | 9 ++ warpgate-core/src/config_providers/file.rs | 9 ++ warpgate-db-entities/src/Target.rs | 3 + .../src/api/targets_list.rs | 7 +- warpgate-protocol-mysql/Cargo.toml | 1 - warpgate-protocol-mysql/src/client.rs | 3 +- warpgate-protocol-mysql/src/error.rs | 3 +- warpgate-protocol-mysql/src/lib.rs | 4 +- warpgate-protocol-mysql/src/stream.rs | 3 +- warpgate-protocol-mysql/src/tls/mod.rs | 7 - warpgate-protocol-postgres/Cargo.toml | 27 ++++ warpgate-protocol-postgres/src/lib.rs | 133 ++++++++++++++++++ .../src/admin/AuthPolicyEditor.svelte | 2 +- warpgate-web/src/admin/Config.svelte | 3 + warpgate-web/src/admin/CreateTarget.svelte | 14 +- warpgate-web/src/admin/Session.svelte | 6 +- warpgate-web/src/admin/Target.svelte | 11 +- warpgate-web/src/admin/User.svelte | 4 +- .../src/admin/lib/openapi-schema.json | 58 +++++++- .../src/common/ConnectionInstructions.svelte | 4 + warpgate-web/src/gateway/TargetList.svelte | 3 + .../src/gateway/lib/openapi-schema.json | 3 +- warpgate/Cargo.toml | 1 + warpgate/src/commands/check.rs | 12 ++ warpgate/src/commands/run.rs | 15 ++ warpgate/src/commands/setup.rs | 28 +++- warpgate/src/commands/test_target.rs | 3 + warpgate/src/main.rs | 4 + 42 files changed, 462 insertions(+), 40 deletions(-) rename {warpgate-protocol-mysql => warpgate-common}/src/tls/maybe_tls_stream.rs (100%) rename {warpgate-protocol-mysql => warpgate-common}/src/tls/rustls_helpers.rs (97%) rename {warpgate-protocol-mysql => warpgate-common}/src/tls/rustls_root_certs.rs (100%) delete mode 100644 warpgate-protocol-mysql/src/tls/mod.rs create mode 100644 warpgate-protocol-postgres/Cargo.toml create mode 100644 warpgate-protocol-postgres/src/lib.rs diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0cbc71e45..796abdaa1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -39,6 +39,10 @@ replace = version = "{new_version}" search = version = "{current_version}" replace = version = "{new_version}" +[bumpversion:file:warpgate-protocol-postgres/Cargo.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + [bumpversion:file:warpgate-protocol-ssh/Cargo.toml] search = version = "{current_version}" replace = version = "{new_version}" diff --git a/Cargo.lock b/Cargo.lock index cc15110d7..ba31d9481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5567,6 +5567,7 @@ dependencies = [ "warpgate-db-entities", "warpgate-protocol-http", "warpgate-protocol-mysql", + "warpgate-protocol-postgres", "warpgate-protocol-ssh", ] @@ -5620,12 +5621,14 @@ dependencies = [ "rand_chacha", "rand_core", "rustls 0.20.9", + "rustls-native-certs", "rustls-pemfile", "sea-orm", "serde", "serde_json", "thiserror", "tokio", + "tokio-rustls 0.23.4", "totp-rs", "tracing", "tracing-core", @@ -5758,7 +5761,6 @@ dependencies = [ "password-hash 0.2.1", "rand", "rustls 0.20.9", - "rustls-native-certs", "rustls-pemfile", "sha1", "thiserror", @@ -5773,6 +5775,23 @@ dependencies = [ "webpki", ] +[[package]] +name = "warpgate-protocol-postgres" +version = "0.10.1" +dependencies = [ + "anyhow", + "async-trait", + "rustls 0.20.9", + "rustls-native-certs", + "rustls-pemfile", + "tokio", + "tokio-rustls 0.23.4", + "tracing", + "warpgate-common", + "warpgate-core", + "warpgate-database-protocols", +] + [[package]] name = "warpgate-protocol-ssh" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 0fb4772a8..56c6eaffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "warpgate-database-protocols", "warpgate-protocol-http", "warpgate-protocol-mysql", + "warpgate-protocol-postgres", "warpgate-protocol-ssh", "warpgate-sso", "warpgate-web", diff --git a/README.md b/README.md index 8ab3831e6..d003a584d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ --- -Warpgate is a smart SSH, HTTPS and MySQL bastion host for Linux that doesn't need special client apps. +Warpgate is a smart SSH, HTTPS, MySQL and PostgreSQL bastion host for Linux that doesn't need special client apps. * Set it up in your DMZ, add user accounts and easily assign them to specific hosts and URLs within the network. * Warpgate will record every session for you to view (live) and replay later through a built-in admin web UI. @@ -63,7 +63,7 @@ In particular, we're working on: ## How it works -Warpgate is a service that you deploy on the bastion/DMZ host, which will accept SSH, HTTPS and MySQL connections and provide an (optional) web admin UI. +Warpgate is a service that you deploy on the bastion/DMZ host, which will accept SSH, HTTPS, MySQL and PostgreSQL connections and provide an (optional) web admin UI. Run `warpgate setup` to interactively generate a config file, including port bindings. See [Getting started](https://github.com/warp-tech/warpgate/wiki/Getting-started) for details. diff --git a/justfile b/justfile index 33fe745da..ca9ab5e3e 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-database-protocols warpgate-protocol-ssh warpgate-protocol-mysql warpgate-protocol-http warpgate-core warpgate-sso" +projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-database-protocols warpgate-protocol-ssh warpgate-protocol-mysql warpgate-protocol-postgres warpgate-protocol-http warpgate-core warpgate-sso" run *ARGS: RUST_BACKTRACE=1 cargo run --all-features -- --config config.yaml {{ARGS}} diff --git a/tests/conftest.py b/tests/conftest.py index 1d608b820..7047c53da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from pathlib import Path from textwrap import dedent -from typing import List +from typing import List, Optional from .util import alloc_port, wait_port from .test_http_common import echo_server_port # noqa @@ -40,6 +40,7 @@ class WarpgateProcess: http_port: int ssh_port: int mysql_port: int + postgres_port: int class ProcessManager: @@ -132,7 +133,7 @@ def start_wg( self, config="", args=None, - share_with: WarpgateProcess = None, + share_with: Optional[WarpgateProcess] = None, stderr=None, stdout=None, ) -> WarpgateProcess: @@ -142,11 +143,13 @@ def start_wg( config_path = share_with.config_path ssh_port = share_with.ssh_port mysql_port = share_with.mysql_port + postgres_port = share_with.postgres_port http_port = share_with.http_port else: ssh_port = alloc_port() http_port = alloc_port() mysql_port = alloc_port() + postgres_port = alloc_port() data_dir = self.ctx.tmpdir / f"wg-data-{uuid.uuid4()}" data_dir.mkdir(parents=True) @@ -198,6 +201,8 @@ def run(args, env={}): str(http_port), "--mysql-port", str(mysql_port), + "--postgres-port", + str(postgres_port), "--data-path", data_dir, ], @@ -221,6 +226,7 @@ def run(args, env={}): ssh_port=ssh_port, http_port=http_port, mysql_port=mysql_port, + postgres_port=postgres_port, ) def start_ssh_client(self, *args, password=None, **kwargs): diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 6e25214ea..604009559 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -26,6 +26,7 @@ poem-openapi = { version = "2.0", features = [ rand = "0.8" rand_chacha = "0.3" rand_core = { version = "0.6", features = ["std"] } +rustls-native-certs = "0.6" sea-orm = { version = "0.12.2", features = [ "runtime-tokio-rustls", "macros", @@ -34,6 +35,7 @@ serde = "1.0" serde_json = "1.0" thiserror = "1.0" tokio = { version = "1.20", features = ["tracing"] } +tokio-rustls = "0.23" totp-rs = { version = "5.0", features = ["otpauth"] } tracing = "0.1" tracing-core = "0.1" diff --git a/warpgate-common/src/config/defaults.rs b/warpgate-common/src/config/defaults.rs index 4b554583b..8b6ef87fc 100644 --- a/warpgate-common/src/config/defaults.rs +++ b/warpgate-common/src/config/defaults.rs @@ -51,6 +51,12 @@ pub(crate) fn _default_mysql_listen() -> ListenEndpoint { ListenEndpoint("0.0.0.0:33306".to_socket_addrs().unwrap().next().unwrap()) } +#[inline] +pub(crate) fn _default_postgres_listen() -> ListenEndpoint { + #[allow(clippy::unwrap_used)] + ListenEndpoint("0.0.0.0:55432".to_socket_addrs().unwrap().next().unwrap()) +} + #[inline] pub(crate) fn _default_retention() -> Duration { Duration::SECOND * 60 * 60 * 24 * 7 diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index d47f1ce51..ae46938ac 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -71,6 +71,8 @@ pub struct UserRequireCredentialsPolicy { pub ssh: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub mysql: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub postgres: Option>, } #[derive(Debug, Deserialize, Serialize, Clone, Object)] @@ -226,6 +228,42 @@ impl MySqlConfig { } } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PostgresConfig { + #[serde(default = "_default_false")] + pub enable: bool, + + #[serde(default = "_default_postgres_listen")] + pub listen: ListenEndpoint, + + #[serde(default)] + pub external_port: Option, + + #[serde(default)] + pub certificate: String, + + #[serde(default)] + pub key: String, +} + +impl Default for PostgresConfig { + fn default() -> Self { + PostgresConfig { + enable: false, + listen: _default_postgres_listen(), + external_port: None, + certificate: "".to_owned(), + key: "".to_owned(), + } + } +} + +impl PostgresConfig { + pub fn external_port(&self) -> u16 { + self.external_port.unwrap_or(self.listen.port()) + } +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct RecordingsConfig { #[serde(default = "_default_false")] @@ -306,6 +344,9 @@ pub struct WarpgateConfigStore { #[serde(default)] pub mysql: MySqlConfig, + #[serde(default)] + pub postgres: PostgresConfig, + #[serde(default)] pub log: LogConfig, @@ -326,6 +367,7 @@ impl Default for WarpgateConfigStore { ssh: <_>::default(), http: <_>::default(), mysql: <_>::default(), + postgres: <_>::default(), log: <_>::default(), config_provider: <_>::default(), } diff --git a/warpgate-common/src/config/target.rs b/warpgate-common/src/config/target.rs index 6bc4535e3..2abc2bc32 100644 --- a/warpgate-common/src/config/target.rs +++ b/warpgate-common/src/config/target.rs @@ -107,6 +107,24 @@ pub struct TargetMySqlOptions { pub tls: Tls, } +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct TargetPostgresOptions { + #[serde(default = "_default_empty_string")] + pub host: String, + + #[serde(default = "_default_mysql_port")] + pub port: u16, + + #[serde(default = "_default_username")] + pub username: String, + + #[serde(default)] + pub password: Option, + + #[serde(default)] + pub tls: Tls, +} + #[derive(Debug, Deserialize, Serialize, Clone, Object, Default)] pub struct TargetWebAdminOptions {} @@ -130,6 +148,8 @@ pub enum TargetOptions { Http(TargetHTTPOptions), #[serde(rename = "mysql")] MySql(TargetMySqlOptions), + #[serde(rename = "postgres")] + Postgres(TargetPostgresOptions), #[serde(rename = "web_admin")] WebAdmin(TargetWebAdminOptions), } diff --git a/warpgate-protocol-mysql/src/tls/maybe_tls_stream.rs b/warpgate-common/src/tls/maybe_tls_stream.rs similarity index 100% rename from warpgate-protocol-mysql/src/tls/maybe_tls_stream.rs rename to warpgate-common/src/tls/maybe_tls_stream.rs diff --git a/warpgate-common/src/tls/mod.rs b/warpgate-common/src/tls/mod.rs index c38fd62ad..2cd2243e6 100644 --- a/warpgate-common/src/tls/mod.rs +++ b/warpgate-common/src/tls/mod.rs @@ -1,5 +1,12 @@ mod cert; mod error; +mod maybe_tls_stream; +mod rustls_helpers; +mod rustls_root_certs; pub use cert::*; pub use error::*; + +pub use maybe_tls_stream::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; +pub use rustls_helpers::{configure_tls_connector, ResolveServerCert}; +pub use rustls_root_certs::ROOT_CERT_STORE; diff --git a/warpgate-protocol-mysql/src/tls/rustls_helpers.rs b/warpgate-common/src/tls/rustls_helpers.rs similarity index 97% rename from warpgate-protocol-mysql/src/tls/rustls_helpers.rs rename to warpgate-common/src/tls/rustls_helpers.rs index 6fa5d4572..b5ff16b93 100644 --- a/warpgate-protocol-mysql/src/tls/rustls_helpers.rs +++ b/warpgate-common/src/tls/rustls_helpers.rs @@ -6,9 +6,8 @@ use rustls::client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}; use rustls::server::{ClientHello, ResolvesServerCert}; use rustls::sign::CertifiedKey; use rustls::{ClientConfig, Error as TlsError, ServerName}; -use warpgate_common::RustlsSetupError; -use super::ROOT_CERT_STORE; +use super::{RustlsSetupError, ROOT_CERT_STORE}; pub struct ResolveServerCert(pub Arc); diff --git a/warpgate-protocol-mysql/src/tls/rustls_root_certs.rs b/warpgate-common/src/tls/rustls_root_certs.rs similarity index 100% rename from warpgate-protocol-mysql/src/tls/rustls_root_certs.rs rename to warpgate-common/src/tls/rustls_root_certs.rs diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 7b98d1493..45da68acb 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -116,6 +116,15 @@ impl ConfigProvider for DatabaseConfigProvider { }), ); } + if let Some(p) = req.postgres { + policy.protocols.insert( + "PostgreSQL", + Box::new(AllCredentialsPolicy { + supported_credential_types: supported_credential_types.clone(), + required_credential_types: p.into_iter().collect(), + }), + ); + } if let Some(p) = req.ssh { policy.protocols.insert( "SSH", diff --git a/warpgate-core/src/config_providers/file.rs b/warpgate-core/src/config_providers/file.rs index 51b900ed7..763c5e552 100644 --- a/warpgate-core/src/config_providers/file.rs +++ b/warpgate-core/src/config_providers/file.rs @@ -110,6 +110,15 @@ impl ConfigProvider for FileConfigProvider { }), ); } + if let Some(p) = req.postgres { + policy.protocols.insert( + "PostgreSQL", + Box::new(AllCredentialsPolicy { + supported_credential_types: supported_credential_types.clone(), + required_credential_types: p.into_iter().collect(), + }), + ); + } if let Some(p) = req.ssh { policy.protocols.insert( "SSH", diff --git a/warpgate-db-entities/src/Target.rs b/warpgate-db-entities/src/Target.rs index 353c90766..951812e7c 100644 --- a/warpgate-db-entities/src/Target.rs +++ b/warpgate-db-entities/src/Target.rs @@ -13,6 +13,8 @@ pub enum TargetKind { MySql, #[sea_orm(string_value = "ssh")] Ssh, + #[sea_orm(string_value = "postgres")] + Postgres, #[sea_orm(string_value = "web_admin")] WebAdmin, } @@ -22,6 +24,7 @@ impl From<&TargetOptions> for TargetKind { match options { TargetOptions::Http(_) => Self::Http, TargetOptions::MySql(_) => Self::MySql, + TargetOptions::Postgres(_) => Self::Postgres, TargetOptions::Ssh(_) => Self::Ssh, TargetOptions::WebAdmin(_) => Self::WebAdmin, } diff --git a/warpgate-protocol-http/src/api/targets_list.rs b/warpgate-protocol-http/src/api/targets_list.rs index b0ff477c9..405b7e693 100644 --- a/warpgate-protocol-http/src/api/targets_list.rs +++ b/warpgate-protocol-http/src/api/targets_list.rs @@ -78,12 +78,7 @@ impl Api { .into_iter() .map(|t| TargetSnapshot { name: t.name.clone(), - kind: match t.options { - TargetOptions::Ssh(_) => Target::TargetKind::Ssh, - TargetOptions::Http(_) => Target::TargetKind::Http, - TargetOptions::MySql(_) => Target::TargetKind::MySql, - TargetOptions::WebAdmin(_) => Target::TargetKind::WebAdmin, - }, + kind: (&t.options).into(), external_host: match t.options { TargetOptions::Http(ref opt) => opt.external_host.clone(), _ => None, diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index bee12869f..806e50a8f 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -25,4 +25,3 @@ tokio-rustls = "0.23" thiserror = "1.0" webpki = "0.22" once_cell = "1.17" -rustls-native-certs = "0.6" diff --git a/warpgate-protocol-mysql/src/client.rs b/warpgate-protocol-mysql/src/client.rs index 1ac6682f9..681df929e 100644 --- a/warpgate-protocol-mysql/src/client.rs +++ b/warpgate-protocol-mysql/src/client.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bytes::BytesMut; use tokio::net::TcpStream; use tracing::*; -use warpgate_common::{TargetMySqlOptions, TlsMode}; +use warpgate_common::{configure_tls_connector, TargetMySqlOptions, TlsMode}; use warpgate_database_protocols::io::Decode; use warpgate_database_protocols::mysql::protocol::auth::AuthPlugin; use warpgate_database_protocols::mysql::protocol::connect::{ @@ -15,7 +15,6 @@ use warpgate_database_protocols::mysql::protocol::Capabilities; use crate::common::compute_auth_challenge_response; use crate::error::MySqlError; use crate::stream::MySqlStream; -use crate::tls::configure_tls_connector; pub struct MySqlClient { pub stream: MySqlStream>, diff --git a/warpgate-protocol-mysql/src/error.rs b/warpgate-protocol-mysql/src/error.rs index 1736366bf..92b7efabc 100644 --- a/warpgate-protocol-mysql/src/error.rs +++ b/warpgate-protocol-mysql/src/error.rs @@ -1,10 +1,9 @@ use std::error::Error; -use warpgate_common::{RustlsSetupError, WarpgateError}; +use warpgate_common::{MaybeTlsStreamError, RustlsSetupError, WarpgateError}; use warpgate_database_protocols::error::Error as SqlxError; use crate::stream::MySqlStreamError; -use crate::tls::MaybeTlsStreamError; #[derive(thiserror::Error, Debug)] pub enum MySqlError { diff --git a/warpgate-protocol-mysql/src/lib.rs b/warpgate-protocol-mysql/src/lib.rs index b0dbe9df1..747ca5181 100644 --- a/warpgate-protocol-mysql/src/lib.rs +++ b/warpgate-protocol-mysql/src/lib.rs @@ -5,7 +5,6 @@ mod error; mod session; mod session_handle; mod stream; -mod tls; use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; @@ -18,13 +17,12 @@ use rustls::ServerConfig; use tokio::net::TcpListener; use tracing::*; use warpgate_common::{ - Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey, + ResolveServerCert, Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey }; use warpgate_core::{ProtocolServer, Services, SessionStateInit, TargetTestError}; use crate::session::MySqlSession; use crate::session_handle::MySqlSessionHandle; -use crate::tls::ResolveServerCert; pub struct MySQLProtocolServer { services: Services, diff --git a/warpgate-protocol-mysql/src/stream.rs b/warpgate-protocol-mysql/src/stream.rs index f429c71d9..4d23b0714 100644 --- a/warpgate-protocol-mysql/src/stream.rs +++ b/warpgate-protocol-mysql/src/stream.rs @@ -4,10 +4,9 @@ use mysql_common::proto::codec::PacketCodec; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; use tracing::*; +use warpgate_common::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; use warpgate_database_protocols::io::Encode; -use crate::tls::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; - #[derive(thiserror::Error, Debug)] pub enum MySqlStreamError { #[error("packet codec error: {0}")] diff --git a/warpgate-protocol-mysql/src/tls/mod.rs b/warpgate-protocol-mysql/src/tls/mod.rs deleted file mode 100644 index 011c6302a..000000000 --- a/warpgate-protocol-mysql/src/tls/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod maybe_tls_stream; -mod rustls_helpers; -mod rustls_root_certs; - -pub use maybe_tls_stream::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; -pub use rustls_helpers::{configure_tls_connector, ResolveServerCert}; -pub use rustls_root_certs::ROOT_CERT_STORE; diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml new file mode 100644 index 000000000..503afa3fc --- /dev/null +++ b/warpgate-protocol-postgres/Cargo.toml @@ -0,0 +1,27 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-protocol-postgres" +version = "0.10.1" + +[dependencies] +warpgate-common = { version = "*", path = "../warpgate-common" } +warpgate-core = { version = "*", path = "../warpgate-core" } +# warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } +warpgate-database-protocols = { version = "*", path = "../warpgate-database-protocols" } +anyhow = { version = "1.0", features = ["std"] } +async-trait = "0.1" +tokio = { version = "1.20", features = ["tracing", "signal"] } +tracing = "0.1" +# uuid = { version = "1.2", features = ["v4"] } +# bytes = "1.3" +# rand = "0.8" +# sha1 = "0.10.5" +# password-hash = { version = "0.2", features = ["std"] } +rustls = { version = "0.20", features = ["dangerous_configuration"] } +rustls-pemfile = "1.0" +tokio-rustls = "0.23" +# thiserror = "1.0" +# webpki = "0.22" +# once_cell = "1.17" +rustls-native-certs = "0.6" diff --git a/warpgate-protocol-postgres/src/lib.rs b/warpgate-protocol-postgres/src/lib.rs new file mode 100644 index 000000000..cfbaaf66f --- /dev/null +++ b/warpgate-protocol-postgres/src/lib.rs @@ -0,0 +1,133 @@ +#![feature(type_alias_impl_trait, try_blocks)] +// mod client; +// mod common; +// mod error; +// mod session; +// mod session_handle; +// mod stream; +// mod tls; +use std::fmt::Debug; +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use async_trait::async_trait; +// use client::{ConnectionOptions, MySqlClient}; +use rustls::server::NoClientAuth; +use rustls::ServerConfig; +use tokio::net::TcpListener; +use tracing::*; +use warpgate_common::{ + ResolveServerCert, Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey +}; +use warpgate_core::{ProtocolServer, Services, SessionStateInit, TargetTestError}; + +// use crate::session::MySqlSession; +// use crate::session_handle::MySqlSessionHandle; +// use crate::tls::ResolveServerCert; + +pub struct PostgresProtocolServer { + services: Services, +} + +impl PostgresProtocolServer { + pub async fn new(services: &Services) -> Result { + Ok(PostgresProtocolServer { + services: services.clone(), + }) + } +} + +#[async_trait] +impl ProtocolServer for PostgresProtocolServer { + async fn run(self, address: SocketAddr) -> Result<()> { + let certificate_and_key = { + let config = self.services.config.lock().await; + let certificate_path = config + .paths_relative_to + .join(&config.store.mysql.certificate); + let key_path = config.paths_relative_to.join(&config.store.mysql.key); + + TlsCertificateAndPrivateKey { + certificate: TlsCertificateBundle::from_file(&certificate_path) + .await + .with_context(|| { + format!("reading SSL private key from '{}'", key_path.display()) + })?, + private_key: TlsPrivateKey::from_file(&key_path).await.with_context(|| { + format!( + "reading SSL certificate from '{}'", + certificate_path.display() + ) + })?, + } + }; + + let tls_config = ServerConfig::builder() + .with_safe_defaults() + .with_client_cert_verifier(NoClientAuth::new()) + .with_cert_resolver(Arc::new(ResolveServerCert(Arc::new( + certificate_and_key.into(), + )))); + + info!(?address, "Listening"); + let listener = TcpListener::bind(address).await?; + // loop { + // let (stream, remote_address) = listener.accept().await?; + // let tls_config = tls_config.clone(); + // let services = self.services.clone(); + // tokio::spawn(async move { + // let (session_handle, mut abort_rx) = MySqlSessionHandle::new(); + + // let server_handle = services + // .state + // .lock() + // .await + // .register_session( + // &crate::common::PROTOCOL_NAME, + // SessionStateInit { + // remote_address: Some(remote_address), + // handle: Box::new(session_handle), + // }, + // ) + // .await?; + + // let session = + // MySqlSession::new(server_handle, services, stream, tls_config, remote_address) + // .await; + // let span = session.make_logging_span(); + // tokio::select! { + // result = session.run().instrument(span) => match result { + // Ok(_) => info!("Session ended"), + // Err(e) => error!(error=%e, "Session failed"), + // }, + // _ = abort_rx.recv() => { + // warn!("Session aborted by admin"); + // }, + // } + + // Ok::<(), anyhow::Error>(()) + // }); + // } + unimplemented!(); + } + + async fn test_target(&self, target: Target) -> Result<(), TargetTestError> { + unimplemented!(); + // let TargetOptions::MySql(options) = target.options else { + // return Err(TargetTestError::Misconfigured( + // "Not a MySQL target".to_owned(), + // )); + // }; + // MySqlClient::connect(&options, ConnectionOptions::default()) + // .await + // .map_err(|e| TargetTestError::ConnectionError(format!("{e}")))?; + // Ok(()) + } +} + +impl Debug for PostgresProtocolServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PostgresProtocolServer") + } +} diff --git a/warpgate-web/src/admin/AuthPolicyEditor.svelte b/warpgate-web/src/admin/AuthPolicyEditor.svelte index ed3456bbd..8752f8a4f 100644 --- a/warpgate-web/src/admin/AuthPolicyEditor.svelte +++ b/warpgate-web/src/admin/AuthPolicyEditor.svelte @@ -6,7 +6,7 @@ import { CredentialKind, type User, type UserRequireCredentialsPolicy } from './ export let user: User export let value: UserRequireCredentialsPolicy export let possibleCredentials: Set -export let protocolId: 'http' | 'ssh' | 'mysql' +export let protocolId: 'http' | 'ssh' | 'mysql' | 'postgres' const labels = { Password: 'Password', diff --git a/warpgate-web/src/admin/Config.svelte b/warpgate-web/src/admin/Config.svelte index 18dbbda57..2421e10b1 100644 --- a/warpgate-web/src/admin/Config.svelte +++ b/warpgate-web/src/admin/Config.svelte @@ -63,6 +63,9 @@ {#if target.options.kind === 'MySql'} MySQL {/if} + {#if target.options.kind === 'Postgres'} + PostgreSQL + {/if} {#if target.options.kind === 'Ssh'} SSH {/if} diff --git a/warpgate-web/src/admin/CreateTarget.svelte b/warpgate-web/src/admin/CreateTarget.svelte index 69e346f9b..7c01e5887 100644 --- a/warpgate-web/src/admin/CreateTarget.svelte +++ b/warpgate-web/src/admin/CreateTarget.svelte @@ -6,7 +6,7 @@ import { Alert, FormGroup } from '@sveltestrap/sveltestrap' let error: Error|null = null let name = '' -let type: 'Http' | 'MySql' | 'Ssh' = 'Ssh' +let type: 'Http' | 'MySql' | 'Ssh' | 'Postgres' = 'Ssh' async function create () { if (!name || !type) { @@ -42,6 +42,17 @@ async function create () { username: 'root', password: '', }, + Postgres: { + kind: 'Postgres' as const, + host: '192.168.0.1', + port: 5432, + tls: { + mode: TlsMode.Preferred, + verify: true, + }, + username: 'postgres', + password: '', + }, }[type] if (!options) { return @@ -78,6 +89,7 @@ async function create () { + diff --git a/warpgate-web/src/admin/Session.svelte b/warpgate-web/src/admin/Session.svelte index a3b6fdfcf..0b3f66f6f 100644 --- a/warpgate-web/src/admin/Session.svelte +++ b/warpgate-web/src/admin/Session.svelte @@ -1,5 +1,5 @@ @@ -76,5 +79,22 @@ {/if} {#if targetKind === TargetKind.Postgres} -

TODO

+ + + + + + + + + + + + + + + + + Make sure you've set your client to require TLS and allowed cleartext password authentication. + {/if} diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index f2d315074..7fc8dd290 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -40,6 +40,17 @@ export function makeExampleMySQLURI (opt: ConnectionOptions): string { return `mysql://${makeMySQLUsername(opt)}${pwSuffix}@${opt.serverInfo?.externalHost ?? 'warpgate-host'}:${opt.serverInfo?.ports.mysql ?? 'warpgate-mysql-port'}?sslMode=required` } +export const makePostgreSQLUsername = makeMySQLUsername + +export function makeExamplePostgreSQLCommand (opt: ConnectionOptions): string { + return shellEscape(['psql', '-U', makeMySQLUsername(opt), '--host', opt.serverInfo?.externalHost ?? 'warpgate-host', '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString()]) +} + +export function makeExamplePostgreSQLURI (opt: ConnectionOptions): string { + const pwSuffix = opt.ticketSecret ? '' : ':' + return `postgresql://${makePostgreSQLUsername(opt)}${pwSuffix}@${opt.serverInfo?.externalHost ?? 'warpgate-host'}:${opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port'}?sslmode=require` +} + export function makeTargetURL (opt: ConnectionOptions): string { const host = opt.targetExternalHost ? `${opt.targetExternalHost}:${opt.serverInfo?.ports.http ?? 443}` : location.host if (opt.ticketSecret) { diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index b83404b52..66acf2e79 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -515,6 +515,10 @@ "mysql": { "type": "integer", "format": "uint16" + }, + "postgres": { + "type": "integer", + "format": "uint16" } } }, From 33fb58c1d05c324df549d68a683e10e3c1ef7cb1 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 21:10:21 +0200 Subject: [PATCH 08/13] fixes --- warpgate-common/src/auth/selector.rs | 14 +++++--- warpgate-common/src/tls/cert.rs | 2 +- warpgate-protocol-mysql/src/session.rs | 3 +- warpgate-protocol-postgres/src/session.rs | 40 +++++++++++++---------- warpgate-web/src/common/protocols.ts | 4 +-- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/warpgate-common/src/auth/selector.rs b/warpgate-common/src/auth/selector.rs index b3854a7b1..1509dfb97 100644 --- a/warpgate-common/src/auth/selector.rs +++ b/warpgate-common/src/auth/selector.rs @@ -13,16 +13,20 @@ pub enum AuthSelector { }, } -impl From<&String> for AuthSelector { - fn from(selector: &String) -> Self { - if let Some(secret) = selector.strip_prefix(TICKET_SELECTOR_PREFIX) { +impl> From for AuthSelector { + fn from(selector: T) -> Self { + if let Some(secret) = selector.as_ref().strip_prefix(TICKET_SELECTOR_PREFIX) { let secret = Secret::new(secret.into()); return AuthSelector::Ticket { secret }; } - let separator = if selector.contains('#') { '#' } else { ':' }; + let separator = if selector.as_ref().contains('#') { + '#' + } else { + ':' + }; - let mut parts = selector.splitn(2, separator); + let mut parts = selector.as_ref().splitn(2, separator); let username = parts.next().unwrap_or("").to_string(); let target_name = parts.next().unwrap_or("").to_string(); AuthSelector::User { diff --git a/warpgate-common/src/tls/cert.rs b/warpgate-common/src/tls/cert.rs index 294176499..b8fde85aa 100644 --- a/warpgate-common/src/tls/cert.rs +++ b/warpgate-common/src/tls/cert.rs @@ -61,7 +61,7 @@ impl TlsPrivateKey { pub fn from_bytes(bytes: Vec) -> Result { let bytes = { // https://github.com/rustls/rustls/issues/767 - let ac = AhoCorasick::new(&[b"EC PRIVATE KEY"]).expect("EC PK AhoCorasick"); + let ac = AhoCorasick::new([b"EC PRIVATE KEY"]).expect("EC PK AhoCorasick"); let mut new_bytes = vec![]; ac.replace_all_with_bytes(&bytes, &mut new_bytes, |_, _, dst| { dst.extend_from_slice(b"PRIVATE KEY"); diff --git a/warpgate-protocol-mysql/src/session.rs b/warpgate-protocol-mysql/src/session.rs index 43ec1159e..c8d6956e4 100644 --- a/warpgate-protocol-mysql/src/session.rs +++ b/warpgate-protocol-mysql/src/session.rs @@ -1,4 +1,5 @@ use std::net::SocketAddr; +use std::ops::Deref; use std::sync::Arc; use bytes::{Buf, Bytes, BytesMut}; @@ -165,7 +166,7 @@ impl MySqlSession { handshake: HandshakeResponse, password: Secret, ) -> Result<(), MySqlError> { - let selector: AuthSelector = (&handshake.username).into(); + let selector: AuthSelector = handshake.username.deref().into(); async fn fail(this: &mut MySqlSession) -> Result<(), MySqlError> { this.stream.push( diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index 93e600fb3..0664e111d 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -85,27 +85,33 @@ impl PostgresSession { return Err(PostgresError::ProtocolError("expected Startup".into())); }; - self.stream - .push(pgwire::messages::startup::Authentication::CleartextPassword)?; - self.stream.flush().await?; - - let Some(PgWireGenericFrontendMessage(PgWireFrontendMessage::PasswordMessageFamily( - message, - ))) = self.stream.recv::().await? - else { - return Err(PostgresError::Eof); - }; - let username = startup.parameters.get("user").cloned(); self.username = username.clone(); self.database = startup.parameters.get("database").cloned(); - let password = Secret::from( - message - .into_password() - .map_err(PostgresError::from)? - .password, - ); + let password = if let AuthSelector::Ticket { .. } = + AuthSelector::from(username.as_deref().unwrap_or("")) + { + Secret::from("".to_string()) + } else { + self.stream + .push(pgwire::messages::startup::Authentication::CleartextPassword)?; + self.stream.flush().await?; + + let Some(PgWireGenericFrontendMessage(PgWireFrontendMessage::PasswordMessageFamily( + message, + ))) = self.stream.recv::().await? + else { + return Err(PostgresError::Eof); + }; + + Secret::from( + message + .into_password() + .map_err(PostgresError::from)? + .password, + ) + }; self.run_authorization(startup, &username.unwrap_or("".into()), password) .await diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index 7fc8dd290..ab0f41360 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -43,12 +43,12 @@ export function makeExampleMySQLURI (opt: ConnectionOptions): string { export const makePostgreSQLUsername = makeMySQLUsername export function makeExamplePostgreSQLCommand (opt: ConnectionOptions): string { - return shellEscape(['psql', '-U', makeMySQLUsername(opt), '--host', opt.serverInfo?.externalHost ?? 'warpgate-host', '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString()]) + return shellEscape(['psql', '-U', makeMySQLUsername(opt), '--host', opt.serverInfo?.externalHost ?? 'warpgate-host', '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString(), 'database-name']) } export function makeExamplePostgreSQLURI (opt: ConnectionOptions): string { const pwSuffix = opt.ticketSecret ? '' : ':' - return `postgresql://${makePostgreSQLUsername(opt)}${pwSuffix}@${opt.serverInfo?.externalHost ?? 'warpgate-host'}:${opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port'}?sslmode=require` + return `postgresql://${makePostgreSQLUsername(opt)}${pwSuffix}@${opt.serverInfo?.externalHost ?? 'warpgate-host'}:${opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port'}/database-name?sslmode=require` } export function makeTargetURL (opt: ConnectionOptions): string { From 7419c12afb4d21a470cd5242dbbbb1e4f6317e33 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 23:24:10 +0200 Subject: [PATCH 09/13] scram auth, target test and e2e test --- Cargo.lock | 28 ++++ tests/Makefile | 5 +- tests/conftest.py | 7 + tests/images/postgres-server/Dockerfile | 7 + tests/images/postgres-server/init.sql | 4 + tests/test_postgres_user_auth_password.py | 101 ++++++++++++ warpgate-common/src/tls/cert.rs | 1 + warpgate-protocol-postgres/Cargo.toml | 12 +- warpgate-protocol-postgres/src/client.rs | 182 +++++++++++++++++----- warpgate-protocol-postgres/src/error.rs | 5 + warpgate-protocol-postgres/src/lib.rs | 29 ++-- warpgate-protocol-postgres/src/stream.rs | 65 +++++++- warpgate-web/src/common/protocols.ts | 2 +- 13 files changed, 387 insertions(+), 61 deletions(-) create mode 100644 tests/images/postgres-server/Dockerfile create mode 100644 tests/images/postgres-server/init.sql create mode 100644 tests/test_postgres_user_auth_password.py diff --git a/Cargo.lock b/Cargo.lock index 312bf84a8..0f33083e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,6 +1008,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.14" @@ -3920,6 +3929,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsasl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45035615cdd68c71daac89aef75b130d4b2cad29599966e1b4671f8fbb463559" +dependencies = [ + "base64 0.22.1", + "core2", + "digest", + "hmac", + "pbkdf2 0.12.2", + "rand", + "serde_json", + "sha2", + "stringprep", + "thiserror", +] + [[package]] name = "russh" version = "0.44.1" @@ -6018,6 +6045,7 @@ dependencies = [ "async-trait", "bytes", "pgwire", + "rsasl", "rustls 0.23.12", "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", diff --git a/tests/Makefile b/tests/Makefile index 4702f5e33..4bb83fcd3 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -4,4 +4,7 @@ image-ssh-server: image-mysql-server: cd images/mysql-server && docker build -t warpgate-e2e-mysql-server . -all: image-ssh-server image-mysql-server +image-postgres-server: + cd images/postgres-server && docker build -t warpgate-e2e-postgres-server . + +all: image-ssh-server image-mysql-server image-postgres-server diff --git a/tests/conftest.py b/tests/conftest.py index 7047c53da..e947ddba7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,6 +129,13 @@ def start_mysql_server(self): ) return port + def start_postgres_server(self): + port = alloc_port() + self.start( + ["docker", "run", "--rm", "-p", f"{port}:5432", "warpgate-e2e-postgres-server"] + ) + return port + def start_wg( self, config="", diff --git a/tests/images/postgres-server/Dockerfile b/tests/images/postgres-server/Dockerfile new file mode 100644 index 000000000..fbf43da90 --- /dev/null +++ b/tests/images/postgres-server/Dockerfile @@ -0,0 +1,7 @@ +FROM postgres:17.0 + +ENV POSTGRES_DB=db +ENV POSTGRES_USER=user +ENV POSTGRES_PASSWORD=123 + +ADD init.sql /docker-entrypoint-initdb.d diff --git a/tests/images/postgres-server/init.sql b/tests/images/postgres-server/init.sql new file mode 100644 index 000000000..c4b7a55ea --- /dev/null +++ b/tests/images/postgres-server/init.sql @@ -0,0 +1,4 @@ +CREATE TABLE tbl ( + id int NOT NULL, + name text NOT NULL +); diff --git a/tests/test_postgres_user_auth_password.py b/tests/test_postgres_user_auth_password.py new file mode 100644 index 000000000..dd40daa0a --- /dev/null +++ b/tests/test_postgres_user_auth_password.py @@ -0,0 +1,101 @@ +import os +import subprocess +from uuid import uuid4 + +from .api_client import ( + api_admin_session, + api_create_target, + api_create_user, + api_create_role, + api_add_role_to_user, + api_add_role_to_target, +) +from .conftest import WarpgateProcess, ProcessManager +from .util import wait_port + + +class Test: + def test( + self, + processes: ProcessManager, + timeout, + shared_wg: WarpgateProcess, + ): + db_port = processes.start_postgres_server() + url = f"https://localhost:{shared_wg.http_port}" + with api_admin_session(url) as session: + role = api_create_role(url, session, {"name": f"role-{uuid4()}"}) + user = api_create_user( + url, + session, + { + "username": f"user-{uuid4()}", + "credentials": [ + { + "kind": "Password", + "hash": "123", + }, + ], + }, + ) + api_add_role_to_user(url, session, user["id"], role["id"]) + target = api_create_target( + url, + session, + { + "name": f"posgresq-{uuid4()}", + "options": { + "kind": "Postgres", + "host": "localhost", + "port": db_port, + "username": "user", + "password": "123", + "tls": { + "mode": "Preferred", + "verify": False, + }, + }, + }, + ) + api_add_role_to_target(url, session, target["id"], role["id"]) + + wait_port(db_port, recv=False) + wait_port(shared_wg.postgres_port, recv=False) + + client = processes.start( + [ + "psql", + "--user", + f"{user['username']}#{target['name']}", + "--password", + "--host", + "127.0.0.1", + "--port", + str(shared_wg.postgres_port), + "db", + ], + env={"PGPASSWORD": "123", **os.environ}, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + assert b"tbl" in client.communicate(b"\\dt\n", timeout=timeout)[0] + assert client.returncode == 0 + + client = processes.start( + [ + "psql", + "--user", + f"{user['username']}#{target['name']}", + "--password", + "--host", + "127.0.0.1", + "--port", + str(shared_wg.postgres_port), + "db", + ], + env={"PGPASSWORD": "wrong", **os.environ}, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + client.communicate(b"\\dt\n", timeout=timeout) + assert client.returncode != 0 diff --git a/warpgate-common/src/tls/cert.rs b/warpgate-common/src/tls/cert.rs index b8fde85aa..bb1e093c7 100644 --- a/warpgate-common/src/tls/cert.rs +++ b/warpgate-common/src/tls/cert.rs @@ -61,6 +61,7 @@ impl TlsPrivateKey { pub fn from_bytes(bytes: Vec) -> Result { let bytes = { // https://github.com/rustls/rustls/issues/767 + #[allow(clippy::expect_used)] let ac = AhoCorasick::new([b"EC PRIVATE KEY"]).expect("EC PK AhoCorasick"); let mut new_bytes = vec![]; ac.replace_all_with_bytes(&bytes, &mut new_bytes, |_, _, dst| { diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index d30da3b66..562de6bd0 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -7,22 +7,18 @@ version = "0.10.1" [dependencies] warpgate-common = { version = "*", path = "../warpgate-common" } warpgate-core = { version = "*", path = "../warpgate-core" } -# warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } -# warpgate-database-protocols = { version = "*", path = "../warpgate-database-protocols" } anyhow = { version = "1.0", features = ["std"] } async-trait = "0.1" tokio = { version = "1.20", features = ["tracing", "signal"] } tracing = "0.1" uuid = { version = "1.2" } bytes = "1.3" -# rand = "0.8" -# sha1 = "0.10.5" -# password-hash = { version = "0.2", features = ["std"] } rustls = { version = "0.23", features = ["ring"], default-features = false } rustls-pemfile = "1.0" tokio-rustls = "0.26" thiserror = "1.0" -# webpki = "0.22" -# once_cell = "1.17" rustls-native-certs = "0.6" -pgwire = { version = "0.23", default-features = false, features = ["server-api"]} +pgwire = { version = "0.23", default-features = false, features = [ + "server-api", +] } +rsasl = { version = "2.1.0", default-features = false, features = ["config_builder", "scram-sha-2", "std", "plain", "provider"] } diff --git a/warpgate-protocol-postgres/src/client.rs b/warpgate-protocol-postgres/src/client.rs index 8e3e0a8c7..a2584846a 100644 --- a/warpgate-protocol-postgres/src/client.rs +++ b/warpgate-protocol-postgres/src/client.rs @@ -1,8 +1,11 @@ use std::collections::BTreeMap; use std::fmt::Debug; +use std::io::Write; use std::sync::Arc; use pgwire::messages::PgWireBackendMessage; +use rsasl::config::SASLConfig; +use rsasl::prelude::{Mechname, SASLClient}; use tokio::net::TcpStream; use tokio_rustls::client::TlsStream; use tracing::*; @@ -31,6 +34,23 @@ impl Default for ConnectionOptions { } } +struct SaslBufferWriter<'a>(&'a mut Option>); + +impl<'a> Write for SaslBufferWriter<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Some(data) = self.0.as_mut() { + data.extend_from_slice(buf); + } else { + *self.0 = Some(buf.to_vec()); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + impl PostgresClient { pub async fn connect( target: &TargetPostgresOptions, @@ -102,49 +122,57 @@ impl PostgresClient { return Err(PostgresError::Eof); }; + let get_password = || { + target + .password + .as_ref() + .ok_or(PostgresError::PasswordRequired) + }; + match payload.0 { PgWireBackendMessage::ErrorResponse(err) => { return Err(PostgresError::from(err)); } - PgWireBackendMessage::Authentication(auth) => { - match auth { - pgwire::messages::startup::Authentication::Ok => { - info!("Authenticated at target"); - break; - } - pgwire::messages::startup::Authentication::CleartextPassword => { - let password = target - .password - .as_ref() - .ok_or(PostgresError::PasswordRequired)?; - let password_message = - pgwire::messages::startup::Password::new(password.into()); - stream.push(password_message)?; - stream.flush().await?; - } - pgwire::messages::startup::Authentication::MD5Password(scramble) => { - let password = target - .password - .as_ref() - .ok_or(PostgresError::PasswordRequired)?; - let hashed = pgwire::api::auth::md5pass::hash_md5_password( - &target.username, - password, - &scramble, - ); - let password_message = pgwire::messages::startup::Password::new(hashed); - stream.push(password_message)?; - stream.flush().await?; - } - // TODO SCRAM auth - x => { - return Err(PostgresError::ProtocolError(format!( - "Unsupported authentication method: {:?}", - x - ))); - } + PgWireBackendMessage::Authentication(auth) => match auth { + pgwire::messages::startup::Authentication::Ok => { + info!("Authenticated at target"); + break; } - } + pgwire::messages::startup::Authentication::CleartextPassword => { + let password = get_password()?; + let password_message = + pgwire::messages::startup::Password::new(password.into()); + stream.push(password_message)?; + stream.flush().await?; + } + pgwire::messages::startup::Authentication::MD5Password(scramble) => { + let password = get_password()?; + let hashed = pgwire::api::auth::md5pass::hash_md5_password( + &target.username, + password, + &scramble, + ); + let password_message = pgwire::messages::startup::Password::new(hashed); + stream.push(password_message)?; + stream.flush().await?; + } + pgwire::messages::startup::Authentication::SASL(mechanisms) => { + let password = get_password()?; + PostgresClient::run_sasl_auth( + &mut stream, + mechanisms, + &target.username, + password, + ) + .await?; + } + x => { + return Err(PostgresError::ProtocolError(format!( + "Unsupported authentication method: {:?}", + x + ))); + } + }, _ => { return Err(PostgresError::ProtocolError( "Expected authentication".to_owned(), @@ -156,6 +184,84 @@ impl PostgresClient { Ok(Self { stream }) } + async fn run_sasl_auth( + stream: &mut PostgresStream>, + mechanisms: Vec, + username: &str, + password: &str, + ) -> Result<(), PostgresError> { + let cfg = SASLConfig::with_credentials(None, username.into(), password.into())?; + let sasl = SASLClient::new(cfg); + let mut session = sasl.start_suggested( + &mechanisms + .iter() + .map(|x| Mechname::parse(x.as_bytes())) + .filter_map(Result::ok) + .collect::>(), + )?; + + let mut data: Option> = None; + if !session.are_we_first() { + return Err(PostgresError::ProtocolError( + "SASL mechanism expects server to send data first".to_owned(), + )); + } + + let mut is_first_response = true; + while { + let mut data_to_send = None; + + let state = { + let mut writer = SaslBufferWriter(&mut data_to_send); + session.step(data.as_deref(), &mut writer)? + }; + + if let Some(data) = data_to_send { + if is_first_response { + let selected_mechanism = session.get_mechname(); + debug!("Selected SASL mechanism: {selected_mechanism:?}"); + stream.push(pgwire::messages::startup::SASLInitialResponse::new( + selected_mechanism.to_string(), + Some(data.into()), + ))?; + is_first_response = false; + } else { + stream.push(pgwire::messages::startup::SASLResponse::new(data.into()))?; + }; + stream.flush().await?; + } + + state.is_running() + } { + let Some(payload) = stream.recv::().await? else { + return Err(PostgresError::Eof); + }; + + dbg!(&payload); + + match payload.0 { + PgWireBackendMessage::ErrorResponse(response) => return Err(response.into()), + PgWireBackendMessage::Authentication( + pgwire::messages::startup::Authentication::SASLContinue(msg), + ) => { + data = Some(msg.to_vec()); + } + PgWireBackendMessage::Authentication( + pgwire::messages::startup::Authentication::SASLFinal(msg), + ) => { + data = Some(msg.to_vec()); + } + payload => { + return Err(PostgresError::ProtocolError(format!( + "Unexpected message: {payload:?}", + ))); + } + } + } + + Ok(()) + } + pub async fn recv(&mut self) -> Result, PostgresError> { self.stream .recv::() diff --git a/warpgate-protocol-postgres/src/error.rs b/warpgate-protocol-postgres/src/error.rs index 5af772033..4eb915673 100644 --- a/warpgate-protocol-postgres/src/error.rs +++ b/warpgate-protocol-postgres/src/error.rs @@ -3,6 +3,7 @@ use std::string::FromUtf8Error; use pgwire::error::PgWireError; use pgwire::messages::response::ErrorResponse; +use rsasl::prelude::{SASLError, SessionError}; use warpgate_common::{MaybeTlsStreamError, RustlsSetupError, WarpgateError}; use crate::stream::PostgresStreamError; @@ -33,6 +34,10 @@ pub enum PostgresError { Io(#[from] std::io::Error), #[error("UTF-8: {0}")] Utf8(#[from] FromUtf8Error), + #[error("SASL: {0}")] + Sasl(#[from] SASLError), + #[error("SASL session: {0}")] + SaslSession(#[from] SessionError), #[error("Password is required for authentication")] PasswordRequired, // #[error("packet decode error: {0}")] diff --git a/warpgate-protocol-postgres/src/lib.rs b/warpgate-protocol-postgres/src/lib.rs index 9332cf0fd..89767b786 100644 --- a/warpgate-protocol-postgres/src/lib.rs +++ b/warpgate-protocol-postgres/src/lib.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use async_trait::async_trait; +use client::{ConnectionOptions, PostgresClient}; use rustls::server::NoClientAuth; use rustls::ServerConfig; use session::PostgresSession; @@ -19,7 +20,8 @@ use session_handle::PostgresSessionHandle; use tokio::net::TcpListener; use tracing::*; use warpgate_common::{ - ResolveServerCert, Target, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey, + ResolveServerCert, Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, + TlsPrivateKey, }; use warpgate_core::{ProtocolServer, Services, SessionStateInit, TargetTestError}; @@ -115,17 +117,20 @@ impl ProtocolServer for PostgresProtocolServer { } } - async fn test_target(&self, _target: Target) -> Result<(), TargetTestError> { - unimplemented!(); - // let TargetOptions::MySql(options) = target.options else { - // return Err(TargetTestError::Misconfigured( - // "Not a MySQL target".to_owned(), - // )); - // }; - // MySqlClient::connect(&options, ConnectionOptions::default()) - // .await - // .map_err(|e| TargetTestError::ConnectionError(format!("{e}")))?; - // Ok(()) + async fn test_target(&self, target: Target) -> Result<(), TargetTestError> { + let TargetOptions::Postgres(options) = target.options else { + return Err(TargetTestError::Misconfigured( + "Not a PostgreSQL target".to_owned(), + )); + }; + let mut conn_options = ConnectionOptions::default(); + conn_options + .parameters + .insert("database".into(), "postgres".into()); + PostgresClient::connect(&options, conn_options) + .await + .map_err(|e| TargetTestError::ConnectionError(format!("{e}")))?; + Ok(()) } } diff --git a/warpgate-protocol-postgres/src/stream.rs b/warpgate-protocol-postgres/src/stream.rs index 496b71749..90e06d920 100644 --- a/warpgate-protocol-postgres/src/stream.rs +++ b/warpgate-protocol-postgres/src/stream.rs @@ -1,7 +1,9 @@ use std::fmt::Debug; +use std::io::Cursor; -use bytes::BytesMut; +use bytes::{Buf, BytesMut}; use pgwire::error::{PgWireError, PgWireResult}; +use pgwire::messages::startup::MESSAGE_TYPE_BYTE_AUTHENTICATION; use pgwire::messages::{PgWireBackendMessage, PgWireFrontendMessage}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpStream; @@ -57,6 +59,23 @@ impl PostgresDecode for PgWireGenericFrontendMessage { impl PostgresDecode for PgWireGenericBackendMessage { fn decode(buf: &mut BytesMut) -> PgWireResult> { + let first_byte = { + let mut peeker = Cursor::new(&mut buf[..]); + if peeker.remaining() > 1 { + Some(peeker.get_u8()) + } else { + None + } + }; + match first_byte { + Some(MESSAGE_TYPE_BYTE_AUTHENTICATION) => { + return Ok(AuthenticationMsgExt::decode(buf)?.map(|x| { + PgWireGenericBackendMessage(PgWireBackendMessage::Authentication(x.0)) + })); + } + _ => (), + } + PgWireBackendMessage::decode(buf).map(|x| x.map(PgWireGenericBackendMessage)) } } @@ -85,6 +104,50 @@ impl PostgresEncode for T { } } +mod authentication_ext { + use std::io::Cursor; + + use bytes::Buf; + use pgwire::messages::startup::Authentication; + use pgwire::messages::Message; + + use super::*; + + /// Workaround for https://github.com/sunng87/pgwire/issues/208 + #[derive(PartialEq, Eq, Debug)] + pub struct AuthenticationMsgExt(pub Authentication); + + impl Message for AuthenticationMsgExt { + #[inline] + fn message_type() -> Option { + Authentication::message_type() + } + + #[inline] + fn message_length(&self) -> usize { + self.0.message_length() + } + + fn encode_body(&self, buf: &mut BytesMut) -> PgWireResult<()> { + self.0.encode_body(buf) + } + + fn decode_body(buf: &mut BytesMut, len: usize) -> PgWireResult { + let mut peeker = Cursor::new(&buf[..]); + let code = peeker.get_i32(); + Ok(match code { + 12 => { + buf.advance(4); + Self(Authentication::SASLFinal(buf.split_to(len - 8).freeze())) + } + _ => Self(Authentication::decode_body(buf, len)?), + }) + } + } +} + +pub use authentication_ext::AuthenticationMsgExt; + pub(crate) struct PostgresStream where TcpStream: UpgradableStream, diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index ab0f41360..5fdee76ba 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -43,7 +43,7 @@ export function makeExampleMySQLURI (opt: ConnectionOptions): string { export const makePostgreSQLUsername = makeMySQLUsername export function makeExamplePostgreSQLCommand (opt: ConnectionOptions): string { - return shellEscape(['psql', '-U', makeMySQLUsername(opt), '--host', opt.serverInfo?.externalHost ?? 'warpgate-host', '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString(), 'database-name']) + return shellEscape(['psql', '-U', makeMySQLUsername(opt), '--host', opt.serverInfo?.externalHost ?? 'warpgate-host', '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString(), '--password', 'database-name']) } export function makeExamplePostgreSQLURI (opt: ConnectionOptions): string { From 1c63610cefbeb4ccafd5fe64d097c21be61ae274 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 23:27:37 +0200 Subject: [PATCH 10/13] Update stream.rs --- warpgate-protocol-postgres/src/stream.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/warpgate-protocol-postgres/src/stream.rs b/warpgate-protocol-postgres/src/stream.rs index 90e06d920..636cd6dac 100644 --- a/warpgate-protocol-postgres/src/stream.rs +++ b/warpgate-protocol-postgres/src/stream.rs @@ -67,6 +67,8 @@ impl PostgresDecode for PgWireGenericBackendMessage { None } }; + + #[allow(clippy::single_match)] match first_byte { Some(MESSAGE_TYPE_BYTE_AUTHENTICATION) => { return Ok(AuthenticationMsgExt::decode(buf)?.map(|x| { From 8caae6bb21c7f6981232cc2f812edb454cc7e985 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 23:28:27 +0200 Subject: [PATCH 11/13] Update README.md --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index cd00477e6..06a76e021 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,7 @@ Warpgate is a smart SSH, HTTPS, MySQL and PostgreSQL bastion host for Linux that ## Project Status -The project is currently in **alpha** stage and is gathering community feedback. See the [official roadmap](https://github.com/orgs/warp-tech/projects/1/views/2) for the upcoming features. - -In particular, we're working on: - -* Requesting admin approvals for sessions -* Support for tunneling PostgreSQL connections, -* and much more. +The project is currently in **alpha** stage and is gathering community feedback. ## How it works From 2faafcdeb88b3eaa0af571794af3975a01fa5ab4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 23:44:58 +0200 Subject: [PATCH 12/13] cargo deny --- .github/workflows/build.yml | 7 +- deny.toml | 213 +++++++++++++++++------------------- 2 files changed, 107 insertions(+), 113 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab932c894..d7a58f179 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,9 +43,14 @@ jobs: with: key: "build" - - name: Install just + - name: Install tools run: | cargo install just + cargo install cargo-deny + + - name: cargo-deny + run: | + cargo deny check bans - name: Install admin UI deps run: | diff --git a/deny.toml b/deny.toml index fab3c9ebe..52d6c8509 100644 --- a/deny.toml +++ b/deny.toml @@ -9,6 +9,11 @@ # The values provided in this template are the default values that will be used # when any section or field is not specified in your own configuration +# Root options + +# The graph table configures how the dependency graph is constructed and thus +# which crates the checks are performed against +[graph] # If 1 or more target triples (and optionally, target_features) are specified, # only the specified targets will be checked when running `cargo deny check`. # This means, if a particular package is only ever used as a target specific @@ -18,122 +23,64 @@ # dependencies not shared by any other crates, would be ignored, as the target # list here is effectively saying which targets you are building for. targets = [ - { triple = "x86_64-unknown-linux-musl" }, + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #"x86_64-unknown-linux-musl", + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, ] +# When creating the dependency graph used as the source of truth when checks are +# executed, this field can be used to prune crates from the graph, removing them +# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate +# is pruned from the graph, all of its dependencies will also be pruned unless +# they are connected to another crate in the graph that hasn't been pruned, +# so it should be used with care. The identifiers are [Package ID Specifications] +# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) +#exclude = [] +# If true, metadata will be collected with `--all-features`. Note that this can't +# be toggled off if true, if you want to conditionally enable `--all-features` it +# is recommended to pass `--all-features` on the cmd line instead +all-features = false +# If true, metadata will be collected with `--no-default-features`. The same +# caveat with `all-features` applies +no-default-features = false +# If set, these feature will be enabled when collecting metadata. If `--features` +# is specified on the cmd line they will take precedence over this option. +#features = [] + +# The output table provides options for how/if diagnostics are outputted +[output] +# When outputting inclusion graphs in diagnostics that include features, this +# option can be used to specify the depth at which feature edges will be added. +# This option is included since the graphs can be quite large and the addition +# of features from the crate(s) to all of the graph roots can be far too verbose. +# This option can be overridden via `--feature-depth` on the cmd line +feature-depth = 1 # This section is considered when running `cargo deny check advisories` # More documentation for the advisories section can be found here: # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] -# The path where the advisory database is cloned/fetched into -db-path = "~/.cargo/advisory-db" +# The path where the advisory databases are cloned/fetched into +#db-path = "$CARGO_HOME/advisory-dbs" # The url(s) of the advisory databases to use -db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" -# The lint level for crates that have been yanked from their source registry -yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" +#db-urls = ["https://github.com/rustsec/advisory-db"] # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ #"RUSTSEC-0000-0000", + #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, + #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish + #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, ] -# Threshold for security vulnerabilities, any vulnerability with a CVSS score -# lower than the range specified will be ignored. Note that ignored advisories -# will still output a note when they are encountered. -# * None - CVSS Score 0.0 -# * Low - CVSS Score 0.1 - 3.9 -# * Medium - CVSS Score 4.0 - 6.9 -# * High - CVSS Score 7.0 - 8.9 -# * Critical - CVSS Score 9.0 - 10.0 -#severity-threshold = +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true -# This section is considered when running `cargo deny check licenses` -# More documentation for the licenses section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html -[licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" -# List of explicitly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. -allow = [ - "MIT", - "Apache-2.0", - "Apache-2.0 WITH LLVM-exception", - "WTFPL", -] -# List of explicitly disallowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. -deny = [ - #"Nokia", -] -# Lint level for licenses considered copyleft -copyleft = "warn" -# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses -# * both - The license will be approved if it is both OSI-approved *AND* FSF -# * either - The license will be approved if it is either OSI-approved *OR* FSF -# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF -# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved -# * neither - This predicate is ignored and the default lint level is used -allow-osi-fsf-free = "either" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" -# The confidence threshold for detecting a license from license text. -# The higher the value, the more closely the license text must be to the -# canonical license text of a valid SPDX license file. -# [possible values: any between 0.0 and 1.0]. -confidence-threshold = 0.8 -# Allow 1 or more licenses on a per-crate basis, so that particular licenses -# aren't accepted for every possible crate as with the normal allow list -exceptions = [ - # Each entry is the crate and version constraint, and its specific allow - # list - #{ allow = ["Zlib"], name = "adler32", version = "*" }, -] - -# Some crates don't have (easily) machine readable licensing information, -# adding a clarification entry for it allows you to manually specify the -# licensing information -#[[licenses.clarify]] -# The name of the crate the clarification applies to -#name = "ring" -# The optional version constraint for the crate -#version = "*" -# The SPDX expression for the license requirements of the crate -#expression = "MIT AND ISC AND OpenSSL" -# One or more files in the crate's source used as the "source of truth" for -# the license expression. If the contents match, the clarification will be used -# when running the license check, otherwise the clarification will be ignored -# and the crate will be checked normally, which may produce warnings or errors -# depending on the rest of your configuration -#license-files = [ - # Each entry is a crate relative path, and the (opaque) hash of its contents - #{ path = "LICENSE", hash = 0xbd0eed23 } -#] - -[licenses.private] -# If true, ignores workspace crates that aren't published, or are only -# published to private registries. -# To see how to mark a crate as unpublished (to the official registry), -# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. -ignore = false -# One or more private registries that you might publish crates to, if a crate -# is only published to private registries, and ignore is true, the crate will -# not have its license(s) checked -registries = [ - #"https://sekretz.com/registry -] # This section is considered when running `cargo deny check bans`. # More documentation about the 'bans' section can be found here: @@ -149,30 +96,64 @@ wildcards = "allow" # * simplest-path - The path to the version with the fewest edges is highlighted # * all - Both lowest-version and simplest-path are used highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" # List of crates that are allowed. Use with care! allow = [ - #{ name = "ansi_term", version = "=0.11.0" }, + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, ] # List of crates to deny deny = [ - # Each entry the name of a crate and a version range. If version is - # not specified, all versions will be matched. - #{ name = "ansi_term", version = "=0.11.0" }, - # + "openssl-sys" + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, # Wrapper crates can optionally be specified to allow the crate when it # is a direct dependency of the otherwise banned crate - #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, + #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, ] + +# List of features to allow/deny +# Each entry the name of a crate and a version range. If version is +# not specified, all versions will be matched. +#[[bans.features]] +#crate = "reqwest" +# Features to not allow +#deny = ["json"] +# Features to allow +#allow = [ +# "rustls", +# "__rustls", +# "__tls", +# "hyper-rustls", +# "rustls", +# "rustls-pemfile", +# "rustls-tls-webpki-roots", +# "tokio-rustls", +# "webpki-roots", +#] +# If true, the allowed features must exactly match the enabled feature set. If +# this is set there is no point setting `deny` +#exact = true + # Certain crates/versions that will be skipped when doing duplicate detection. skip = [ - #{ name = "ansi_term", version = "=0.11.0" }, + #"ansi_term@0.11.0", + #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, ] # Similarly to `skip` allows you to skip certain crates during duplicate # detection. Unlike skip, it also includes the entire tree of transitive # dependencies starting at the specified crate, up to a certain depth, which is -# by default infinite +# by default infinite. skip-tree = [ - #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, + #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies + #{ crate = "ansi_term@0.11.0", depth = 20 }, ] # This section is considered when running `cargo deny check sources`. @@ -190,3 +171,11 @@ unknown-git = "warn" allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [] + +[sources.allow-org] +# github.com organizations to allow git sources for +github = [] +# gitlab.com organizations to allow git sources for +gitlab = [] +# bitbucket.org organizations to allow git sources for +bitbucket = [] From 7c0a7b3e7d8df076cb2621e9fd754180b9267c5c Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 8 Oct 2024 23:56:27 +0200 Subject: [PATCH 13/13] fixes --- tests/conftest.py | 35 +++++++++++++++++++---- tests/test_postgres_user_auth_password.py | 2 -- tests/util.py | 14 +++++---- warpgate-protocol-postgres/src/error.rs | 4 --- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e947ddba7..d1a1a5200 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import logging import os +import time import psutil import pytest import requests @@ -14,7 +15,7 @@ from textwrap import dedent from typing import List, Optional -from .util import alloc_port, wait_port +from .util import _wait_timeout, alloc_port, wait_port from .test_http_common import echo_server_port # noqa @@ -46,9 +47,10 @@ class WarpgateProcess: class ProcessManager: children: List[Child] - def __init__(self, ctx: Context) -> None: + def __init__(self, ctx: Context, timeout: int) -> None: self.children = [] self.ctx = ctx + self.timeout = timeout def stop(self): for child in self.children: @@ -131,9 +133,32 @@ def start_mysql_server(self): def start_postgres_server(self): port = alloc_port() + container_name = f"warpgate-e2e-postgres-server-{uuid.uuid4()}" self.start( - ["docker", "run", "--rm", "-p", f"{port}:5432", "warpgate-e2e-postgres-server"] + ["docker", "run", "--rm", '--name', container_name, "-p", f"{port}:5432", "warpgate-e2e-postgres-server"] ) + + def wait_postgres(): + while True: + try: + subprocess.check_call( + [ + "docker", + "exec", + container_name, + "pg_isready", + "-h", + "localhost", + "-U", + "user", + ] + ) + break + except subprocess.CalledProcessError: + time.sleep(1) + + _wait_timeout(wait_postgres, "Postgres is not ready", timeout=self.timeout) + logging.debug(f"Postgres {container_name} is up") return port def start_wg( @@ -280,8 +305,8 @@ def ctx(): @pytest.fixture(scope="session") -def processes(ctx, report_generation): - mgr = ProcessManager(ctx) +def processes(ctx, timeout, report_generation): + mgr = ProcessManager(ctx, timeout) try: yield mgr finally: diff --git a/tests/test_postgres_user_auth_password.py b/tests/test_postgres_user_auth_password.py index dd40daa0a..84f46c3f5 100644 --- a/tests/test_postgres_user_auth_password.py +++ b/tests/test_postgres_user_auth_password.py @@ -67,7 +67,6 @@ def test( "psql", "--user", f"{user['username']}#{target['name']}", - "--password", "--host", "127.0.0.1", "--port", @@ -86,7 +85,6 @@ def test( "psql", "--user", f"{user['username']}#{target['name']}", - "--password", "--host", "127.0.0.1", "--port", diff --git a/tests/util.py b/tests/util.py index c378ccd41..df2e3b340 100644 --- a/tests/util.py +++ b/tests/util.py @@ -23,6 +23,14 @@ def alloc_port(): return last_port +def _wait_timeout(fn, msg, timeout=60): + t = threading.Thread(target=fn, daemon=True) + t.start() + t.join(timeout=timeout) + if t.is_alive(): + raise Exception(msg) + + def wait_port(port, recv=True, timeout=60, for_process: subprocess.Popen = None): logging.debug(f"Waiting for port {port}") @@ -53,11 +61,7 @@ def wait(): else: time.sleep(0.1) - t = threading.Thread(target=wait, daemon=True) - t.start() - t.join(timeout=timeout) - if t.is_alive(): - raise Exception(f"Port {port} is not up") + _wait_timeout(wait, f"Port {port} is not up", timeout=timeout) return data diff --git a/warpgate-protocol-postgres/src/error.rs b/warpgate-protocol-postgres/src/error.rs index 4eb915673..6872cdca2 100644 --- a/warpgate-protocol-postgres/src/error.rs +++ b/warpgate-protocol-postgres/src/error.rs @@ -22,8 +22,6 @@ pub enum PostgresError { Stream(#[from] PostgresStreamError), #[error("server doesn't offer TLS")] TlsNotSupported, - // #[error("client doesn't support TLS")] - // TlsNotSupportedByClient, #[error("TLS setup failed: {0}")] TlsSetup(#[from] RustlsSetupError), #[error("TLS stream error: {0}")] @@ -40,8 +38,6 @@ pub enum PostgresError { SaslSession(#[from] SessionError), #[error("Password is required for authentication")] PasswordRequired, - // #[error("packet decode error: {0}")] - // Decode(Box), #[error(transparent)] Warpgate(#[from] WarpgateError), #[error(transparent)]