mz_pgwire/server.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::future::Future;
11use std::net::IpAddr;
12use std::pin::Pin;
13use std::str::FromStr;
14
15use anyhow::Context;
16use async_trait::async_trait;
17use mz_authenticator::GenericOidcAuthenticator;
18use mz_frontegg_auth::Authenticator as FronteggAuthenticator;
19use mz_ore::now::{SYSTEM_TIME, epoch_to_uuid_v7};
20use mz_pgwire_common::{
21 ACCEPT_SSL_ENCRYPTION, CONN_UUID_KEY, Conn, ConnectionCounter, FrontendStartupMessage,
22 MAX_FORWARDED_STARTUP_FRAME_SIZE, MZ_FORWARDED_FOR_KEY, REJECT_ENCRYPTION, decode_startup,
23};
24use mz_server_core::listeners::{AllowedRoles, AuthenticatorKind};
25use mz_server_core::{Connection, ConnectionHandler, ReloadingTlsConfig};
26use openssl::ssl::Ssl;
27use tokio::io::AsyncWriteExt;
28use tokio_metrics::TaskMetrics;
29use tokio_openssl::SslStream;
30use tracing::{debug, error, trace};
31
32use crate::codec::FramedConn;
33use crate::metrics::{Metrics, MetricsConfig};
34use crate::protocol;
35
36/// Configures a [`Server`].
37#[derive(Debug)]
38pub struct Config {
39 /// The label for the mz_connection_status metric.
40 pub label: &'static str,
41 /// A client for the adapter with which the server will communicate.
42 pub adapter_client: mz_adapter::Client,
43 /// The TLS configuration for the server.
44 ///
45 /// If not present, then TLS is not enabled, and clients requests to
46 /// negotiate TLS will be rejected.
47 pub tls: Option<ReloadingTlsConfig>,
48 /// Frontegg JWT authenticator.
49 pub frontegg: Option<FronteggAuthenticator>,
50 /// OIDC authenticator.
51 pub oidc: GenericOidcAuthenticator,
52 /// The authentication method defined by the server's listener
53 /// configuration.
54 pub authenticator_kind: AuthenticatorKind,
55 /// The registry entries that the pgwire server uses to report metrics.
56 pub metrics: MetricsConfig,
57 /// Global connection limit and count
58 pub active_connection_counter: ConnectionCounter,
59 /// Helm chart version
60 pub helm_chart_version: Option<String>,
61 /// Whether to allow reserved users (ie: mz_system).
62 pub allowed_roles: AllowedRoles,
63}
64
65/// A server that communicates with clients via the pgwire protocol.
66pub struct Server {
67 tls: Option<ReloadingTlsConfig>,
68 adapter_client: mz_adapter::Client,
69 authenticator_kind: AuthenticatorKind,
70 frontegg: Option<FronteggAuthenticator>,
71 oidc: GenericOidcAuthenticator,
72 metrics: Metrics,
73 active_connection_counter: ConnectionCounter,
74 helm_chart_version: Option<String>,
75 allowed_roles: AllowedRoles,
76}
77
78#[async_trait]
79impl mz_server_core::Server for Server {
80 const NAME: &'static str = "pgwire";
81
82 fn handle_connection(
83 &self,
84 conn: Connection,
85 tokio_metrics_intervals: impl Iterator<Item = TaskMetrics> + Send + 'static,
86 ) -> ConnectionHandler {
87 // Using fully-qualified syntax means we won't accidentally call
88 // ourselves (i.e., silently infinitely recurse) if the name or type of
89 // `crate::Server::handle_connection` changes.
90 Box::pin(crate::Server::handle_connection(
91 self,
92 conn,
93 tokio_metrics_intervals,
94 ))
95 }
96}
97
98impl Server {
99 /// Constructs a new server.
100 pub fn new(config: Config) -> Server {
101 Server {
102 tls: config.tls,
103 adapter_client: config.adapter_client,
104 authenticator_kind: config.authenticator_kind,
105 frontegg: config.frontegg,
106 oidc: config.oidc,
107 metrics: Metrics::new(config.metrics, config.label),
108 active_connection_counter: config.active_connection_counter,
109 helm_chart_version: config.helm_chart_version,
110 allowed_roles: config.allowed_roles,
111 }
112 }
113
114 #[mz_ore::instrument(level = "debug")]
115 pub fn handle_connection(
116 &self,
117 conn: Connection,
118 tokio_metrics_intervals: impl Iterator<Item = TaskMetrics> + Send + 'static,
119 ) -> impl Future<Output = Result<(), anyhow::Error>> + Send + 'static {
120 let adapter_client = self.adapter_client.clone();
121 let authenticator_kind = self.authenticator_kind;
122 let frontegg = self.frontegg.clone();
123 let oidc = self.oidc.clone();
124 let tls = self.tls.clone();
125 let metrics = self.metrics.clone();
126 let active_connection_counter = self.active_connection_counter.clone();
127 let helm_chart_version = self.helm_chart_version.clone();
128 let allowed_roles = self.allowed_roles;
129
130 // TODO(guswynn): remove this redundant_closure_call
131 #[allow(clippy::redundant_closure_call)]
132 async move {
133 let result = (|| {
134 async move {
135 let conn_id = adapter_client.new_conn_id()?;
136 let mut conn = Conn::Unencrypted(conn);
137 loop {
138 let message =
139 decode_startup(&mut conn, MAX_FORWARDED_STARTUP_FRAME_SIZE).await?;
140
141 match &message {
142 Some(message) => trace!("cid={} recv={:?}", conn_id, message),
143 None => trace!("cid={} recv=<eof>", conn_id),
144 }
145
146 conn = match message {
147 // Clients sometimes hang up during the startup sequence, e.g.
148 // because they receive an unacceptable response to an
149 // `SslRequest`. This is considered a graceful termination.
150 None => return Ok(()),
151
152 Some(FrontendStartupMessage::Startup {
153 version,
154 mut params,
155 }) => {
156 // If someone (usually the balancer) forwarded a connection UUID,
157 // then use that, otherwise generate one.
158 let conn_uuid_handle = conn.inner_mut().uuid_handle();
159 let conn_uuid = params
160 .remove(CONN_UUID_KEY)
161 .and_then(|uuid| {
162 uuid.parse()
163 .inspect_err(|e| {
164 error!(
165 "pgwire connection with invalid conn UUID: {e}",
166 )
167 })
168 .ok()
169 });
170 let conn_uuid_forwarded = conn_uuid.is_some();
171 // FIXME(ptravers): we should be able to inject
172 // the clock when instantiating the `Server`
173 // but as of writing there's no great way, I can
174 // see, to harmonize the lifetimes of the return
175 // type and &self which must house `NowFn`.
176 let conn_uuid = conn_uuid.unwrap_or_else(
177 || epoch_to_uuid_v7(&(SYSTEM_TIME.clone())()),
178 );
179 conn_uuid_handle.set(conn_uuid);
180 debug!(
181 conn_uuid = %conn_uuid_handle.display(),
182 conn_uuid_forwarded,
183 "starting new pgwire connection in adapter",
184 );
185
186 let direct_peer_addr = conn
187 .inner_mut()
188 .peer_addr()
189 .context("fetching peer addr")?
190 .ip();
191 let peer_addr= match params.remove(MZ_FORWARDED_FOR_KEY) {
192 Some(ip_str) => {
193 match IpAddr::from_str(&ip_str) {
194 Ok(ip) => Some(ip),
195 Err(e) => {
196 error!("pgwire connection with invalid mz_forwarded_for address: {e}");
197 None
198 }
199 }
200 }
201 None => Some(direct_peer_addr)
202 };
203 let mut conn = FramedConn::new(
204 conn_id.clone(),
205 peer_addr,
206 conn,
207 );
208
209 protocol::run(protocol::RunParams {
210 tls_mode: tls.as_ref().map(|tls| tls.mode),
211 adapter_client,
212 conn: &mut conn,
213 conn_uuid,
214 version,
215 params,
216 frontegg,
217 oidc,
218 authenticator_kind,
219 active_connection_counter,
220 helm_chart_version,
221 allowed_roles,
222 tokio_metrics_intervals,
223 })
224 .await?;
225 conn.flush().await?;
226 return Ok(());
227 }
228
229 Some(FrontendStartupMessage::CancelRequest {
230 conn_id,
231 secret_key,
232 }) => {
233 adapter_client.cancel_request(conn_id, secret_key);
234 // For security, the client is not told whether the cancel
235 // request succeeds or fails.
236 return Ok(());
237 }
238
239 Some(FrontendStartupMessage::SslRequest) => match (conn, &tls) {
240 (Conn::Unencrypted(mut conn), Some(tls)) => {
241 trace!("cid={} send=AcceptSsl", conn_id);
242 conn.write_all(&[ACCEPT_SSL_ENCRYPTION]).await?;
243 let mut ssl_stream =
244 SslStream::new(Ssl::new(&tls.context.get())?, conn)?;
245 if let Err(e) = Pin::new(&mut ssl_stream).accept().await {
246 let _ = ssl_stream.get_mut().shutdown().await;
247 return Err(e.into());
248 }
249 Conn::Ssl(ssl_stream)
250 }
251 (mut conn, _) => {
252 trace!("cid={} send=RejectSsl", conn_id);
253 conn.write_all(&[REJECT_ENCRYPTION]).await?;
254 conn
255 }
256 },
257
258 Some(FrontendStartupMessage::GssEncRequest) => {
259 trace!("cid={} send=RejectGssEnc", conn_id);
260 conn.write_all(&[REJECT_ENCRYPTION]).await?;
261 conn
262 }
263 }
264 }
265 }
266 })()
267 .await;
268 metrics.connection_status(result.is_ok()).inc();
269 result
270 }
271 }
272}