LCOV - code coverage report
Current view: top level - safekeeper/src - handler.rs (source / functions) Coverage Total Hit
Test: 20b6afc7b7f34578dcaab2b3acdaecfe91cd8bf1.info Lines: 0.0 % 261 0
Test Date: 2024-11-25 17:48:16 Functions: 0.0 % 30 0

            Line data    Source code
       1              : //! Part of Safekeeper pretending to be Postgres, i.e. handling Postgres
       2              : //! protocol commands.
       3              : 
       4              : use anyhow::Context;
       5              : use std::future::Future;
       6              : use std::str::{self, FromStr};
       7              : use std::sync::Arc;
       8              : use tokio::io::{AsyncRead, AsyncWrite};
       9              : use tracing::{debug, info, info_span, Instrument};
      10              : 
      11              : use crate::auth::check_permission;
      12              : use crate::json_ctrl::{handle_json_ctrl, AppendLogicalMessage};
      13              : 
      14              : use crate::metrics::{TrafficMetrics, PG_QUERIES_GAUGE};
      15              : use crate::safekeeper::Term;
      16              : use crate::timeline::TimelineError;
      17              : use crate::wal_service::ConnectionId;
      18              : use crate::{GlobalTimelines, SafeKeeperConf};
      19              : use postgres_backend::PostgresBackend;
      20              : use postgres_backend::QueryError;
      21              : use postgres_ffi::PG_TLI;
      22              : use pq_proto::{BeMessage, FeStartupPacket, RowDescriptor, INT4_OID, TEXT_OID};
      23              : use regex::Regex;
      24              : use utils::auth::{Claims, JwtAuth, Scope};
      25              : use utils::{
      26              :     id::{TenantId, TenantTimelineId, TimelineId},
      27              :     lsn::Lsn,
      28              : };
      29              : 
      30              : /// Safekeeper handler of postgres commands
      31              : pub struct SafekeeperPostgresHandler {
      32              :     pub conf: SafeKeeperConf,
      33              :     /// assigned application name
      34              :     pub appname: Option<String>,
      35              :     pub tenant_id: Option<TenantId>,
      36              :     pub timeline_id: Option<TimelineId>,
      37              :     pub ttid: TenantTimelineId,
      38              :     /// Unique connection id is logged in spans for observability.
      39              :     pub conn_id: ConnectionId,
      40              :     /// Auth scope allowed on the connections and public key used to check auth tokens. None if auth is not configured.
      41              :     auth: Option<(Scope, Arc<JwtAuth>)>,
      42              :     claims: Option<Claims>,
      43              :     io_metrics: Option<TrafficMetrics>,
      44              : }
      45              : 
      46              : /// Parsed Postgres command.
      47              : enum SafekeeperPostgresCommand {
      48              :     StartWalPush,
      49              :     StartReplication { start_lsn: Lsn, term: Option<Term> },
      50              :     IdentifySystem,
      51              :     TimelineStatus,
      52              :     JSONCtrl { cmd: AppendLogicalMessage },
      53              : }
      54              : 
      55            0 : fn parse_cmd(cmd: &str) -> anyhow::Result<SafekeeperPostgresCommand> {
      56            0 :     if cmd.starts_with("START_WAL_PUSH") {
      57            0 :         Ok(SafekeeperPostgresCommand::StartWalPush)
      58            0 :     } else if cmd.starts_with("START_REPLICATION") {
      59            0 :         let re = Regex::new(
      60            0 :             // We follow postgres START_REPLICATION LOGICAL options to pass term.
      61            0 :             r"START_REPLICATION(?: SLOT [^ ]+)?(?: PHYSICAL)? ([[:xdigit:]]+/[[:xdigit:]]+)(?: \(term='(\d+)'\))?",
      62            0 :         )
      63            0 :         .unwrap();
      64            0 :         let caps = re
      65            0 :             .captures(cmd)
      66            0 :             .context(format!("failed to parse START_REPLICATION command {}", cmd))?;
      67            0 :         let start_lsn =
      68            0 :             Lsn::from_str(&caps[1]).context("parse start LSN from START_REPLICATION command")?;
      69            0 :         let term = if let Some(m) = caps.get(2) {
      70            0 :             Some(m.as_str().parse::<u64>().context("invalid term")?)
      71              :         } else {
      72            0 :             None
      73              :         };
      74            0 :         Ok(SafekeeperPostgresCommand::StartReplication { start_lsn, term })
      75            0 :     } else if cmd.starts_with("IDENTIFY_SYSTEM") {
      76            0 :         Ok(SafekeeperPostgresCommand::IdentifySystem)
      77            0 :     } else if cmd.starts_with("TIMELINE_STATUS") {
      78            0 :         Ok(SafekeeperPostgresCommand::TimelineStatus)
      79            0 :     } else if cmd.starts_with("JSON_CTRL") {
      80            0 :         let cmd = cmd.strip_prefix("JSON_CTRL").context("invalid prefix")?;
      81              :         Ok(SafekeeperPostgresCommand::JSONCtrl {
      82            0 :             cmd: serde_json::from_str(cmd)?,
      83              :         })
      84              :     } else {
      85            0 :         anyhow::bail!("unsupported command {cmd}");
      86              :     }
      87            0 : }
      88              : 
      89            0 : fn cmd_to_string(cmd: &SafekeeperPostgresCommand) -> &str {
      90            0 :     match cmd {
      91            0 :         SafekeeperPostgresCommand::StartWalPush => "START_WAL_PUSH",
      92            0 :         SafekeeperPostgresCommand::StartReplication { .. } => "START_REPLICATION",
      93            0 :         SafekeeperPostgresCommand::TimelineStatus => "TIMELINE_STATUS",
      94            0 :         SafekeeperPostgresCommand::IdentifySystem => "IDENTIFY_SYSTEM",
      95            0 :         SafekeeperPostgresCommand::JSONCtrl { .. } => "JSON_CTRL",
      96              :     }
      97            0 : }
      98              : 
      99              : impl<IO: AsyncRead + AsyncWrite + Unpin + Send> postgres_backend::Handler<IO>
     100              :     for SafekeeperPostgresHandler
     101              : {
     102              :     // tenant_id and timeline_id are passed in connection string params
     103            0 :     fn startup(
     104            0 :         &mut self,
     105            0 :         _pgb: &mut PostgresBackend<IO>,
     106            0 :         sm: &FeStartupPacket,
     107            0 :     ) -> Result<(), QueryError> {
     108            0 :         if let FeStartupPacket::StartupMessage { params, .. } = sm {
     109            0 :             if let Some(options) = params.options_raw() {
     110            0 :                 for opt in options {
     111              :                     // FIXME `ztenantid` and `ztimelineid` left for compatibility during deploy,
     112              :                     // remove these after the PR gets deployed:
     113              :                     // https://github.com/neondatabase/neon/pull/2433#discussion_r970005064
     114            0 :                     match opt.split_once('=') {
     115            0 :                         Some(("ztenantid", value)) | Some(("tenant_id", value)) => {
     116            0 :                             self.tenant_id = Some(value.parse().with_context(|| {
     117            0 :                                 format!("Failed to parse {value} as tenant id")
     118            0 :                             })?);
     119              :                         }
     120            0 :                         Some(("ztimelineid", value)) | Some(("timeline_id", value)) => {
     121            0 :                             self.timeline_id = Some(value.parse().with_context(|| {
     122            0 :                                 format!("Failed to parse {value} as timeline id")
     123            0 :                             })?);
     124              :                         }
     125            0 :                         Some(("availability_zone", client_az)) => {
     126            0 :                             if let Some(metrics) = self.io_metrics.as_ref() {
     127            0 :                                 metrics.set_client_az(client_az)
     128            0 :                             }
     129              :                         }
     130            0 :                         _ => continue,
     131              :                     }
     132              :                 }
     133            0 :             }
     134              : 
     135            0 :             if let Some(app_name) = params.get("application_name") {
     136            0 :                 self.appname = Some(app_name.to_owned());
     137            0 :                 if let Some(metrics) = self.io_metrics.as_ref() {
     138            0 :                     metrics.set_app_name(app_name)
     139            0 :                 }
     140            0 :             }
     141              : 
     142            0 :             let ttid = TenantTimelineId::new(
     143            0 :                 self.tenant_id.unwrap_or(TenantId::from([0u8; 16])),
     144            0 :                 self.timeline_id.unwrap_or(TimelineId::from([0u8; 16])),
     145            0 :             );
     146            0 :             tracing::Span::current()
     147            0 :                 .record("ttid", tracing::field::display(ttid))
     148            0 :                 .record(
     149            0 :                     "application_name",
     150            0 :                     tracing::field::debug(self.appname.clone()),
     151            0 :                 );
     152            0 : 
     153            0 :             Ok(())
     154              :         } else {
     155            0 :             Err(QueryError::Other(anyhow::anyhow!(
     156            0 :                 "Safekeeper received unexpected initial message: {sm:?}"
     157            0 :             )))
     158              :         }
     159            0 :     }
     160              : 
     161            0 :     fn check_auth_jwt(
     162            0 :         &mut self,
     163            0 :         _pgb: &mut PostgresBackend<IO>,
     164            0 :         jwt_response: &[u8],
     165            0 :     ) -> Result<(), QueryError> {
     166            0 :         // this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT
     167            0 :         // which requires auth to be present
     168            0 :         let (allowed_auth_scope, auth) = self
     169            0 :             .auth
     170            0 :             .as_ref()
     171            0 :             .expect("auth_type is configured but .auth of handler is missing");
     172            0 :         let data = auth
     173            0 :             .decode(str::from_utf8(jwt_response).context("jwt response is not UTF-8")?)
     174            0 :             .map_err(|e| QueryError::Unauthorized(e.0))?;
     175              : 
     176              :         // The handler might be configured to allow only tenant scope tokens.
     177            0 :         if matches!(allowed_auth_scope, Scope::Tenant)
     178            0 :             && !matches!(data.claims.scope, Scope::Tenant)
     179              :         {
     180            0 :             return Err(QueryError::Unauthorized(
     181            0 :                 "passed JWT token is for full access, but only tenant scope is allowed".into(),
     182            0 :             ));
     183            0 :         }
     184              : 
     185            0 :         if matches!(data.claims.scope, Scope::Tenant) && data.claims.tenant_id.is_none() {
     186            0 :             return Err(QueryError::Unauthorized(
     187            0 :                 "jwt token scope is Tenant, but tenant id is missing".into(),
     188            0 :             ));
     189            0 :         }
     190            0 : 
     191            0 :         debug!(
     192            0 :             "jwt scope check succeeded for scope: {:#?} by tenant id: {:?}",
     193              :             data.claims.scope, data.claims.tenant_id,
     194              :         );
     195              : 
     196            0 :         self.claims = Some(data.claims);
     197            0 :         Ok(())
     198            0 :     }
     199              : 
     200            0 :     fn process_query(
     201            0 :         &mut self,
     202            0 :         pgb: &mut PostgresBackend<IO>,
     203            0 :         query_string: &str,
     204            0 :     ) -> impl Future<Output = Result<(), QueryError>> {
     205            0 :         Box::pin(async move {
     206            0 :             if query_string
     207            0 :                 .to_ascii_lowercase()
     208            0 :                 .starts_with("set datestyle to ")
     209              :             {
     210              :                 // important for debug because psycopg2 executes "SET datestyle TO 'ISO'" on connect
     211            0 :                 pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?;
     212            0 :                 return Ok(());
     213            0 :             }
     214              : 
     215            0 :             let cmd = parse_cmd(query_string)?;
     216            0 :             let cmd_str = cmd_to_string(&cmd);
     217            0 : 
     218            0 :             let _guard = PG_QUERIES_GAUGE.with_label_values(&[cmd_str]).guard();
     219            0 : 
     220            0 :             info!("got query {:?}", query_string);
     221              : 
     222            0 :             let tenant_id = self.tenant_id.context("tenantid is required")?;
     223            0 :             let timeline_id = self.timeline_id.context("timelineid is required")?;
     224            0 :             self.check_permission(Some(tenant_id))?;
     225            0 :             self.ttid = TenantTimelineId::new(tenant_id, timeline_id);
     226            0 : 
     227            0 :             match cmd {
     228              :                 SafekeeperPostgresCommand::StartWalPush => {
     229            0 :                     self.handle_start_wal_push(pgb)
     230            0 :                         .instrument(info_span!("WAL receiver"))
     231            0 :                         .await
     232              :                 }
     233            0 :                 SafekeeperPostgresCommand::StartReplication { start_lsn, term } => {
     234            0 :                     self.handle_start_replication(pgb, start_lsn, term)
     235            0 :                         .instrument(info_span!("WAL sender"))
     236            0 :                         .await
     237              :                 }
     238            0 :                 SafekeeperPostgresCommand::IdentifySystem => self.handle_identify_system(pgb).await,
     239            0 :                 SafekeeperPostgresCommand::TimelineStatus => self.handle_timeline_status(pgb).await,
     240            0 :                 SafekeeperPostgresCommand::JSONCtrl { ref cmd } => {
     241            0 :                     handle_json_ctrl(self, pgb, cmd).await
     242              :                 }
     243              :             }
     244            0 :         })
     245            0 :     }
     246              : }
     247              : 
     248              : impl SafekeeperPostgresHandler {
     249            0 :     pub fn new(
     250            0 :         conf: SafeKeeperConf,
     251            0 :         conn_id: u32,
     252            0 :         io_metrics: Option<TrafficMetrics>,
     253            0 :         auth: Option<(Scope, Arc<JwtAuth>)>,
     254            0 :     ) -> Self {
     255            0 :         SafekeeperPostgresHandler {
     256            0 :             conf,
     257            0 :             appname: None,
     258            0 :             tenant_id: None,
     259            0 :             timeline_id: None,
     260            0 :             ttid: TenantTimelineId::empty(),
     261            0 :             conn_id,
     262            0 :             claims: None,
     263            0 :             auth,
     264            0 :             io_metrics,
     265            0 :         }
     266            0 :     }
     267              : 
     268              :     // when accessing management api supply None as an argument
     269              :     // when using to authorize tenant pass corresponding tenant id
     270            0 :     fn check_permission(&self, tenant_id: Option<TenantId>) -> Result<(), QueryError> {
     271            0 :         if self.auth.is_none() {
     272              :             // auth is set to Trust, nothing to check so just return ok
     273            0 :             return Ok(());
     274            0 :         }
     275            0 :         // auth is some, just checked above, when auth is some
     276            0 :         // then claims are always present because of checks during connection init
     277            0 :         // so this expect won't trigger
     278            0 :         let claims = self
     279            0 :             .claims
     280            0 :             .as_ref()
     281            0 :             .expect("claims presence already checked");
     282            0 :         check_permission(claims, tenant_id).map_err(|e| QueryError::Unauthorized(e.0))
     283            0 :     }
     284              : 
     285            0 :     async fn handle_timeline_status<IO: AsyncRead + AsyncWrite + Unpin>(
     286            0 :         &mut self,
     287            0 :         pgb: &mut PostgresBackend<IO>,
     288            0 :     ) -> Result<(), QueryError> {
     289              :         // Get timeline, handling "not found" error
     290            0 :         let tli = match GlobalTimelines::get(self.ttid) {
     291            0 :             Ok(tli) => Ok(Some(tli)),
     292            0 :             Err(TimelineError::NotFound(_)) => Ok(None),
     293            0 :             Err(e) => Err(QueryError::Other(e.into())),
     294            0 :         }?;
     295              : 
     296              :         // Write row description
     297            0 :         pgb.write_message_noflush(&BeMessage::RowDescription(&[
     298            0 :             RowDescriptor::text_col(b"flush_lsn"),
     299            0 :             RowDescriptor::text_col(b"commit_lsn"),
     300            0 :         ]))?;
     301              : 
     302              :         // Write row if timeline exists
     303            0 :         if let Some(tli) = tli {
     304            0 :             let (inmem, _state) = tli.get_state().await;
     305            0 :             let flush_lsn = tli.get_flush_lsn().await;
     306            0 :             let commit_lsn = inmem.commit_lsn;
     307            0 :             pgb.write_message_noflush(&BeMessage::DataRow(&[
     308            0 :                 Some(flush_lsn.to_string().as_bytes()),
     309            0 :                 Some(commit_lsn.to_string().as_bytes()),
     310            0 :             ]))?;
     311            0 :         }
     312              : 
     313            0 :         pgb.write_message_noflush(&BeMessage::CommandComplete(b"TIMELINE_STATUS"))?;
     314            0 :         Ok(())
     315            0 :     }
     316              : 
     317              :     ///
     318              :     /// Handle IDENTIFY_SYSTEM replication command
     319              :     ///
     320            0 :     async fn handle_identify_system<IO: AsyncRead + AsyncWrite + Unpin>(
     321            0 :         &mut self,
     322            0 :         pgb: &mut PostgresBackend<IO>,
     323            0 :     ) -> Result<(), QueryError> {
     324            0 :         let tli = GlobalTimelines::get(self.ttid).map_err(|e| QueryError::Other(e.into()))?;
     325              : 
     326            0 :         let lsn = if self.is_walproposer_recovery() {
     327              :             // walproposer should get all local WAL until flush_lsn
     328            0 :             tli.get_flush_lsn().await
     329              :         } else {
     330              :             // other clients shouldn't get any uncommitted WAL
     331            0 :             tli.get_state().await.0.commit_lsn
     332              :         }
     333            0 :         .to_string();
     334              : 
     335            0 :         let sysid = tli.get_state().await.1.server.system_id.to_string();
     336            0 :         let lsn_bytes = lsn.as_bytes();
     337            0 :         let tli = PG_TLI.to_string();
     338            0 :         let tli_bytes = tli.as_bytes();
     339            0 :         let sysid_bytes = sysid.as_bytes();
     340            0 : 
     341            0 :         pgb.write_message_noflush(&BeMessage::RowDescription(&[
     342            0 :             RowDescriptor {
     343            0 :                 name: b"systemid",
     344            0 :                 typoid: TEXT_OID,
     345            0 :                 typlen: -1,
     346            0 :                 ..Default::default()
     347            0 :             },
     348            0 :             RowDescriptor {
     349            0 :                 name: b"timeline",
     350            0 :                 typoid: INT4_OID,
     351            0 :                 typlen: 4,
     352            0 :                 ..Default::default()
     353            0 :             },
     354            0 :             RowDescriptor {
     355            0 :                 name: b"xlogpos",
     356            0 :                 typoid: TEXT_OID,
     357            0 :                 typlen: -1,
     358            0 :                 ..Default::default()
     359            0 :             },
     360            0 :             RowDescriptor {
     361            0 :                 name: b"dbname",
     362            0 :                 typoid: TEXT_OID,
     363            0 :                 typlen: -1,
     364            0 :                 ..Default::default()
     365            0 :             },
     366            0 :         ]))?
     367            0 :         .write_message_noflush(&BeMessage::DataRow(&[
     368            0 :             Some(sysid_bytes),
     369            0 :             Some(tli_bytes),
     370            0 :             Some(lsn_bytes),
     371            0 :             None,
     372            0 :         ]))?
     373            0 :         .write_message_noflush(&BeMessage::CommandComplete(b"IDENTIFY_SYSTEM"))?;
     374            0 :         Ok(())
     375            0 :     }
     376              : 
     377              :     /// Returns true if current connection is a replication connection, originating
     378              :     /// from a walproposer recovery function. This connection gets a special handling:
     379              :     /// safekeeper must stream all local WAL till the flush_lsn, whether committed or not.
     380            0 :     pub fn is_walproposer_recovery(&self) -> bool {
     381            0 :         match &self.appname {
     382            0 :             None => false,
     383            0 :             Some(appname) => {
     384            0 :                 appname == "wal_proposer_recovery" ||
     385              :                 // set by safekeeper peer recovery
     386            0 :                 appname.starts_with("safekeeper")
     387              :             }
     388              :         }
     389            0 :     }
     390              : }
        

Generated by: LCOV version 2.1-beta