Skip to main content

qail_pg/driver/
auth_types.rs

1//! Authentication and security types: ScramChannelBindingMode, EnterpriseAuthMechanism,
2//! GssTokenProvider, GssTokenRequest, AuthSettings, TlsMode, GssEncMode, ConnectOptions.
3
4use super::connection::TlsConfig;
5use std::sync::Arc;
6
7/// SCRAM channel-binding policy during SASL negotiation.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ScramChannelBindingMode {
10    /// Do not use `SCRAM-SHA-256-PLUS` even when available.
11    Disable,
12    /// Prefer `SCRAM-SHA-256-PLUS`, fallback to plain SCRAM if needed.
13    #[default]
14    Prefer,
15    /// Require `SCRAM-SHA-256-PLUS` and fail otherwise.
16    Require,
17}
18
19impl ScramChannelBindingMode {
20    /// Parse common config string values.
21    pub fn parse(value: &str) -> Option<Self> {
22        match value.trim().to_ascii_lowercase().as_str() {
23            "disable" | "off" | "false" | "no" => Some(Self::Disable),
24            "prefer" | "on" | "true" | "yes" => Some(Self::Prefer),
25            "require" | "required" => Some(Self::Require),
26            _ => None,
27        }
28    }
29}
30
31/// Enterprise authentication mechanisms initiated by PostgreSQL.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum EnterpriseAuthMechanism {
34    /// Kerberos V5 (`AuthenticationKerberosV5`, auth code `2`).
35    KerberosV5,
36    /// GSSAPI (`AuthenticationGSS`, auth code `7`).
37    GssApi,
38    /// SSPI (`AuthenticationSSPI`, auth code `9`, primarily Windows servers).
39    Sspi,
40}
41
42/// Structured token request for stateful Kerberos/GSS/SSPI providers.
43#[derive(Debug, Clone, Copy)]
44pub struct GssTokenRequest<'a> {
45    /// Stable per-handshake identifier so providers can keep per-connection state.
46    pub session_id: u64,
47    /// Negotiated enterprise auth mechanism.
48    pub mechanism: EnterpriseAuthMechanism,
49    /// Server challenge token (`None` for initial token).
50    pub server_token: Option<&'a [u8]>,
51}
52
53/// Stateful callback for Kerberos/GSS/SSPI response generation.
54///
55/// Use this when the underlying auth stack needs per-handshake context between
56/// `AuthenticationGSS` and `AuthenticationGSSContinue` messages.
57pub type GssTokenProvider =
58    Arc<dyn for<'a> Fn(GssTokenRequest<'a>) -> Result<Vec<u8>, String> + Send + Sync>;
59
60/// Password-auth mechanism policy.
61///
62/// Defaults allow all PostgreSQL password mechanisms for compatibility.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct AuthSettings {
65    /// Allow server-requested cleartext password auth.
66    pub allow_cleartext_password: bool,
67    /// Allow server-requested MD5 password auth.
68    pub allow_md5_password: bool,
69    /// Allow server-requested SCRAM auth.
70    pub allow_scram_sha_256: bool,
71    /// Allow server-requested Kerberos V5 auth flow.
72    pub allow_kerberos_v5: bool,
73    /// Allow server-requested GSSAPI auth flow.
74    pub allow_gssapi: bool,
75    /// Allow server-requested SSPI auth flow.
76    pub allow_sspi: bool,
77    /// SCRAM channel-binding requirement.
78    pub channel_binding: ScramChannelBindingMode,
79}
80
81impl Default for AuthSettings {
82    fn default() -> Self {
83        Self {
84            allow_cleartext_password: true,
85            allow_md5_password: true,
86            allow_scram_sha_256: true,
87            allow_kerberos_v5: false,
88            allow_gssapi: false,
89            allow_sspi: false,
90            channel_binding: ScramChannelBindingMode::Prefer,
91        }
92    }
93}
94
95impl AuthSettings {
96    /// Restrictive mode: SCRAM-only password auth.
97    pub fn scram_only() -> Self {
98        Self {
99            allow_cleartext_password: false,
100            allow_md5_password: false,
101            allow_scram_sha_256: true,
102            allow_kerberos_v5: false,
103            allow_gssapi: false,
104            allow_sspi: false,
105            channel_binding: ScramChannelBindingMode::Prefer,
106        }
107    }
108
109    /// Restrictive mode: enterprise Kerberos/GSS only (no password auth).
110    pub fn gssapi_only() -> Self {
111        Self {
112            allow_cleartext_password: false,
113            allow_md5_password: false,
114            allow_scram_sha_256: false,
115            allow_kerberos_v5: true,
116            allow_gssapi: true,
117            allow_sspi: true,
118            channel_binding: ScramChannelBindingMode::Prefer,
119        }
120    }
121
122    pub(crate) fn has_any_password_method(self) -> bool {
123        self.allow_cleartext_password || self.allow_md5_password || self.allow_scram_sha_256
124    }
125}
126
127/// TLS policy for connection establishment.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum TlsMode {
130    /// Do not attempt TLS.
131    #[default]
132    Disable,
133    /// Try TLS first; fallback to plaintext only when server has no TLS support.
134    Prefer,
135    /// Require TLS and fail if unavailable.
136    Require,
137}
138
139impl TlsMode {
140    /// Parse libpq-style `sslmode` values.
141    pub fn parse_sslmode(value: &str) -> Option<Self> {
142        match value.trim().to_ascii_lowercase().as_str() {
143            "disable" => Some(Self::Disable),
144            "allow" | "prefer" => Some(Self::Prefer),
145            "require" | "verify-ca" | "verify-full" => Some(Self::Require),
146            _ => None,
147        }
148    }
149}
150
151/// GSSAPI encryption mode for transport-level encryption via Kerberos.
152///
153/// Controls whether the driver attempts GSSAPI session encryption
154/// (GSSENCRequest) before falling back to TLS or plaintext.
155///
156/// See: PostgreSQL protocol §54.2.11 — GSSAPI Session Encryption.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub enum GssEncMode {
159    /// Never attempt GSSAPI encryption.
160    #[default]
161    Disable,
162    /// Try GSSAPI encryption first; fall back to TLS or plaintext.
163    Prefer,
164    /// Require GSSAPI encryption — fail if the server rejects GSSENCRequest.
165    Require,
166}
167
168impl GssEncMode {
169    /// Parse libpq-style `gssencmode` values.
170    pub fn parse_gssencmode(value: &str) -> Option<Self> {
171        match value.trim().to_ascii_lowercase().as_str() {
172            "disable" => Some(Self::Disable),
173            "prefer" => Some(Self::Prefer),
174            "require" => Some(Self::Require),
175            _ => None,
176        }
177    }
178}
179
180/// Advanced connection options for enterprise deployments.
181///
182/// Protocol-version controls are intentionally not exposed here in this
183/// milestone. The driver requests protocol 3.2 by default and performs a
184/// one-shot fallback to protocol 3.0 only on explicit version rejection.
185#[derive(Clone, Default)]
186pub struct ConnectOptions {
187    /// TLS mode for the primary connection.
188    pub tls_mode: TlsMode,
189    /// GSSAPI session encryption mode.
190    pub gss_enc_mode: GssEncMode,
191    /// Optional custom CA bundle (PEM) for TLS server validation.
192    pub tls_ca_cert_pem: Option<Vec<u8>>,
193    /// Optional mTLS client certificate/key config.
194    pub mtls: Option<TlsConfig>,
195    /// Optional stateful Kerberos/GSS/SSPI token provider.
196    pub gss_token_provider: Option<GssTokenProvider>,
197    /// Password-auth policy.
198    pub auth: AuthSettings,
199    /// Opt into Linux io_uring for plain TCP transport.
200    ///
201    /// This is ignored for TLS, mTLS, Unix sockets, and GSSENC paths.
202    /// Defaults to false so deployments must explicitly accept the kernel
203    /// attack-surface tradeoff.
204    pub io_uring: bool,
205    /// Additional startup parameters sent in StartupMessage.
206    /// Example: `replication=database` for logical replication mode.
207    pub startup_params: Vec<(String, String)>,
208}
209
210impl std::fmt::Debug for ConnectOptions {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        f.debug_struct("ConnectOptions")
213            .field("tls_mode", &self.tls_mode)
214            .field("gss_enc_mode", &self.gss_enc_mode)
215            .field(
216                "tls_ca_cert_pem",
217                &self.tls_ca_cert_pem.as_ref().map(std::vec::Vec::len),
218            )
219            .field("mtls", &self.mtls.as_ref().map(|_| "<configured>"))
220            .field(
221                "gss_token_provider",
222                &self.gss_token_provider.as_ref().map(|_| "<configured>"),
223            )
224            .field("auth", &self.auth)
225            .field("io_uring", &self.io_uring)
226            .field("startup_params_count", &self.startup_params.len())
227            .finish()
228    }
229}
230
231impl ConnectOptions {
232    /// Add a startup parameter.
233    ///
234    /// Example: `opts.with_startup_param("application_name", "qail-repl")`.
235    pub fn with_startup_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236        let key = key.into();
237        let value = value.into();
238        self.startup_params
239            .retain(|(existing, _)| !existing.eq_ignore_ascii_case(&key));
240        self.startup_params.push((key, value));
241        self
242    }
243
244    /// Enable logical replication startup mode (`replication=database`).
245    pub fn with_logical_replication(mut self) -> Self {
246        self.startup_params
247            .retain(|(k, _)| !k.eq_ignore_ascii_case("replication"));
248        self.startup_params
249            .push(("replication".to_string(), "database".to_string()));
250        self
251    }
252
253    /// Opt into Linux io_uring for plain TCP transport.
254    pub fn with_io_uring(mut self, enabled: bool) -> Self {
255        self.io_uring = enabled;
256        self
257    }
258}