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}