Skip to main content

persistant_sqlx/
options.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use crate::{PgBackend, classify};
3use persistant_relational::{ConnectionSecret, RelError};
4use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
5use std::path::PathBuf;
6use std::str::FromStr;
7use std::time::Duration;
8
9/// How a connection is secured. A typed setting, not a `sslmode=` string someone has to remember.
10///
11/// The policy given to [`PgBackend::connect_with`] (or `MysqlBackend::connect_with`) **overrides** whatever `sslmode` the connection
12/// string carries, so a URL cannot weaken it. The default is [`TlsPolicy::Require`]: encryption is
13/// on unless a program says otherwise in code.
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum TlsPolicy {
17    /// No TLS. For a loopback or socket connection in a test; never over a network.
18    Disable,
19    /// TLS if the server offers it, plain otherwise (sqlx's default). **A downgrade is silent**:
20    /// an attacker who strips the offer gets a plain connection.
21    Prefer,
22    /// TLS, or fail. The certificate is **not** checked, so this stops eavesdropping but not an
23    /// impostor server.
24    #[default]
25    Require,
26    /// TLS, and the server's certificate must chain to a trusted root (`root_cert`, or the
27    /// built-in web roots if `None`). The host name is not checked.
28    VerifyCa {
29        /// A PEM file of the root(s) to trust, instead of the built-in roots.
30        root_cert: Option<PathBuf>,
31    },
32    /// As `VerifyCa`, and the certificate must also name the host connected to.
33    VerifyFull {
34        /// A PEM file of the root(s) to trust, instead of the built-in roots.
35        root_cert: Option<PathBuf>,
36    },
37}
38
39/// Settings for [`PgBackend::connect_with`] (and `MysqlBackend::connect_with`, feature `mysql`).
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ConnectOptions {
42    /// How the connection is secured. Overrides the URL's `sslmode`.
43    pub tls: TlsPolicy,
44    /// Most connections the pool opens.
45    pub max_connections: u32,
46    /// How long a caller waits for a connection before [`RelError::Unavailable`]. Bounded by
47    /// default so a saturated pool is an error, not a hang.
48    pub acquire_timeout: Duration,
49}
50
51impl Default for ConnectOptions {
52    fn default() -> Self {
53        ConnectOptions {
54            tls: TlsPolicy::default(),
55            max_connections: 16,
56            acquire_timeout: Duration::from_secs(10),
57        }
58    }
59}
60
61impl PgBackend {
62    /// Connect with an explicit TLS policy and pool bounds, to a connection string held as a
63    /// [`ConnectionSecret`] (resolved here, never printed).
64    pub async fn connect_with(
65        secret: &ConnectionSecret,
66        options: &ConnectOptions,
67    ) -> Result<Self, RelError> {
68        let url = secret.resolve()?;
69        let mut o = PgConnectOptions::from_str(url.expose())
70            .map_err(|_| RelError::Backend("connection string is not valid".to_string()))?;
71        o = match &options.tls {
72            TlsPolicy::Disable => o.ssl_mode(PgSslMode::Disable),
73            TlsPolicy::Prefer => o.ssl_mode(PgSslMode::Prefer),
74            TlsPolicy::Require => o.ssl_mode(PgSslMode::Require),
75            TlsPolicy::VerifyCa { root_cert } => {
76                let o = o.ssl_mode(PgSslMode::VerifyCa);
77                match root_cert {
78                    Some(p) => o.ssl_root_cert(p),
79                    None => o,
80                }
81            }
82            TlsPolicy::VerifyFull { root_cert } => {
83                let o = o.ssl_mode(PgSslMode::VerifyFull);
84                match root_cert {
85                    Some(p) => o.ssl_root_cert(p),
86                    None => o,
87                }
88            }
89        };
90        let pool = PgPoolOptions::new()
91            .max_connections(options.max_connections)
92            .acquire_timeout(options.acquire_timeout)
93            .connect_with(o)
94            .await
95            .map_err(classify)?;
96        Self::from_pool(pool).await
97    }
98
99    /// Whether the connection this backend uses is encrypted, as the **server** reports it
100    /// (`pg_stat_ssl`), not as the client was asked.
101    pub async fn connection_is_encrypted(&self) -> Result<bool, RelError> {
102        sqlx::query_scalar("SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid()")
103            .fetch_one(&self.pool)
104            .await
105            .map_err(classify)
106    }
107}