LCOV - code coverage report
Current view: top level - safekeeper/src - handler.rs (source / functions) Coverage Total Hit
Test: 32f4a56327bc9da697706839ed4836b2a00a408f.info Lines: 93.0 % 256 238
Test Date: 2024-02-07 07:37:29 Functions: 47.1 % 34 16

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

Generated by: LCOV version 2.1-beta