LCOV - code coverage report
Current view: top level - proxy/src/auth - backend.rs (source / functions) Coverage Total Hit
Test: 8ac049b474321fdc72ddcb56d7165153a1a900e8.info Lines: 80.2 % 96 77
Test Date: 2023-09-06 10:18:01 Functions: 36.7 % 49 18

            Line data    Source code
       1              : mod classic;
       2              : mod hacks;
       3              : mod link;
       4              : 
       5              : pub use link::LinkAuthError;
       6              : 
       7              : use crate::{
       8              :     auth::{self, ClientCredentials},
       9              :     console::{
      10              :         self,
      11              :         provider::{CachedNodeInfo, ConsoleReqExtra},
      12              :         Api,
      13              :     },
      14              :     stream, url,
      15              : };
      16              : use futures::TryFutureExt;
      17              : use std::borrow::Cow;
      18              : use tokio::io::{AsyncRead, AsyncWrite};
      19              : use tracing::info;
      20              : 
      21              : /// A product of successful authentication.
      22              : pub struct AuthSuccess<T> {
      23              :     /// Did we send [`pq_proto::BeMessage::AuthenticationOk`] to client?
      24              :     pub reported_auth_ok: bool,
      25              :     /// Something to be considered a positive result.
      26              :     pub value: T,
      27              : }
      28              : 
      29              : impl<T> AuthSuccess<T> {
      30              :     /// Very similar to [`std::option::Option::map`].
      31              :     /// Maps [`AuthSuccess<T>`] to [`AuthSuccess<R>`] by applying
      32              :     /// a function to a contained value.
      33            3 :     pub fn map<R>(self, f: impl FnOnce(T) -> R) -> AuthSuccess<R> {
      34            3 :         AuthSuccess {
      35            3 :             reported_auth_ok: self.reported_auth_ok,
      36            3 :             value: f(self.value),
      37            3 :         }
      38            3 :     }
      39              : }
      40              : 
      41              : /// This type serves two purposes:
      42              : ///
      43              : /// * When `T` is `()`, it's just a regular auth backend selector
      44              : ///   which we use in [`crate::config::ProxyConfig`].
      45              : ///
      46              : /// * However, when we substitute `T` with [`ClientCredentials`],
      47              : ///   this helps us provide the credentials only to those auth
      48              : ///   backends which require them for the authentication process.
      49              : pub enum BackendType<'a, T> {
      50              :     /// Current Cloud API (V2).
      51              :     Console(Cow<'a, console::provider::neon::Api>, T),
      52              :     /// Local mock of Cloud API (V2).
      53              :     Postgres(Cow<'a, console::provider::mock::Api>, T),
      54              :     /// Authentication via a web browser.
      55              :     Link(Cow<'a, url::ApiUrl>),
      56              :     /// Test backend.
      57              :     Test(&'a dyn TestBackend),
      58              : }
      59              : 
      60              : pub trait TestBackend: Send + Sync + 'static {
      61              :     fn wake_compute(&self) -> Result<CachedNodeInfo, console::errors::WakeComputeError>;
      62              : }
      63              : 
      64              : impl std::fmt::Display for BackendType<'_, ()> {
      65           14 :     fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
      66           14 :         use BackendType::*;
      67           14 :         match self {
      68            0 :             Console(endpoint, _) => fmt.debug_tuple("Console").field(&endpoint.url()).finish(),
      69           11 :             Postgres(endpoint, _) => fmt.debug_tuple("Postgres").field(&endpoint.url()).finish(),
      70            3 :             Link(url) => fmt.debug_tuple("Link").field(&url.as_str()).finish(),
      71            0 :             Test(_) => fmt.debug_tuple("Test").finish(),
      72              :         }
      73           14 :     }
      74              : }
      75              : 
      76              : impl<T> BackendType<'_, T> {
      77              :     /// Very similar to [`std::option::Option::as_ref`].
      78              :     /// This helps us pass structured config to async tasks.
      79           51 :     pub fn as_ref(&self) -> BackendType<'_, &T> {
      80           51 :         use BackendType::*;
      81           51 :         match self {
      82            0 :             Console(c, x) => Console(Cow::Borrowed(c), x),
      83           48 :             Postgres(c, x) => Postgres(Cow::Borrowed(c), x),
      84            3 :             Link(c) => Link(Cow::Borrowed(c)),
      85            0 :             Test(x) => Test(*x),
      86              :         }
      87           51 :     }
      88              : }
      89              : 
      90              : impl<'a, T> BackendType<'a, T> {
      91              :     /// Very similar to [`std::option::Option::map`].
      92              :     /// Maps [`BackendType<T>`] to [`BackendType<R>`] by applying
      93              :     /// a function to a contained value.
      94           51 :     pub fn map<R>(self, f: impl FnOnce(T) -> R) -> BackendType<'a, R> {
      95           51 :         use BackendType::*;
      96           51 :         match self {
      97            0 :             Console(c, x) => Console(c, f(x)),
      98           48 :             Postgres(c, x) => Postgres(c, f(x)),
      99            3 :             Link(c) => Link(c),
     100            0 :             Test(x) => Test(x),
     101              :         }
     102           51 :     }
     103              : }
     104              : 
     105              : impl<'a, T, E> BackendType<'a, Result<T, E>> {
     106              :     /// Very similar to [`std::option::Option::transpose`].
     107              :     /// This is most useful for error handling.
     108           51 :     pub fn transpose(self) -> Result<BackendType<'a, T>, E> {
     109           51 :         use BackendType::*;
     110           51 :         match self {
     111            0 :             Console(c, x) => x.map(|x| Console(c, x)),
     112           48 :             Postgres(c, x) => x.map(|x| Postgres(c, x)),
     113            3 :             Link(c) => Ok(Link(c)),
     114            0 :             Test(x) => Ok(Test(x)),
     115              :         }
     116           51 :     }
     117              : }
     118              : 
     119              : /// True to its name, this function encapsulates our current auth trade-offs.
     120              : /// Here, we choose the appropriate auth flow based on circumstances.
     121           28 : async fn auth_quirks(
     122           28 :     api: &impl console::Api,
     123           28 :     extra: &ConsoleReqExtra<'_>,
     124           28 :     creds: &mut ClientCredentials<'_>,
     125           28 :     client: &mut stream::PqStream<impl AsyncRead + AsyncWrite + Unpin>,
     126           28 :     allow_cleartext: bool,
     127           28 : ) -> auth::Result<AuthSuccess<CachedNodeInfo>> {
     128           28 :     // If there's no project so far, that entails that client doesn't
     129           28 :     // support SNI or other means of passing the endpoint (project) name.
     130           28 :     // We now expect to see a very specific payload in the place of password.
     131           28 :     if creds.project.is_none() {
     132              :         // Password will be checked by the compute node later.
     133            3 :         return hacks::password_hack(api, extra, creds, client).await;
     134           25 :     }
     135           25 : 
     136           25 :     // Password hack should set the project name.
     137           25 :     // TODO: make `creds.project` more type-safe.
     138           25 :     assert!(creds.project.is_some());
     139              : 
     140              :     // Perform cleartext auth if we're allowed to do that.
     141              :     // Currently, we use it for websocket connections (latency).
     142           25 :     if allow_cleartext {
     143              :         // Password will be checked by the compute node later.
     144            0 :         return hacks::cleartext_hack(api, extra, creds, client).await;
     145           25 :     }
     146           25 : 
     147           25 :     // Finally, proceed with the main auth flow (SCRAM-based).
     148          166 :     classic::authenticate(api, extra, creds, client).await
     149           28 : }
     150              : 
     151              : impl BackendType<'_, ClientCredentials<'_>> {
     152              :     /// Get compute endpoint name from the credentials.
     153           31 :     pub fn get_endpoint(&self) -> Option<String> {
     154           31 :         use BackendType::*;
     155           31 : 
     156           31 :         match self {
     157            0 :             Console(_, creds) => creds.project.clone(),
     158           28 :             Postgres(_, creds) => creds.project.clone(),
     159            3 :             Link(_) => Some("link".to_owned()),
     160            0 :             Test(_) => Some("test".to_owned()),
     161              :         }
     162           31 :     }
     163              :     /// Authenticate the client via the requested backend, possibly using credentials.
     164           93 :     #[tracing::instrument(fields(allow_cleartext = allow_cleartext), skip_all)]
     165              :     pub async fn authenticate(
     166              :         &mut self,
     167              :         extra: &ConsoleReqExtra<'_>,
     168              :         client: &mut stream::PqStream<impl AsyncRead + AsyncWrite + Unpin>,
     169              :         allow_cleartext: bool,
     170              :     ) -> auth::Result<AuthSuccess<CachedNodeInfo>> {
     171              :         use BackendType::*;
     172              : 
     173              :         let res = match self {
     174              :             Console(api, creds) => {
     175            0 :                 info!(
     176            0 :                     user = creds.user,
     177            0 :                     project = creds.project(),
     178            0 :                     "performing authentication using the console"
     179            0 :                 );
     180              : 
     181              :                 let api = api.as_ref();
     182              :                 auth_quirks(api, extra, creds, client, allow_cleartext).await?
     183              :             }
     184              :             Postgres(api, creds) => {
     185           28 :                 info!(
     186           28 :                     user = creds.user,
     187           28 :                     project = creds.project(),
     188           28 :                     "performing authentication using a local postgres instance"
     189           28 :                 );
     190              : 
     191              :                 let api = api.as_ref();
     192              :                 auth_quirks(api, extra, creds, client, allow_cleartext).await?
     193              :             }
     194              :             // NOTE: this auth backend doesn't use client credentials.
     195              :             Link(url) => {
     196            3 :                 info!("performing link authentication");
     197              : 
     198              :                 link::authenticate(url, client)
     199              :                     .await?
     200              :                     .map(CachedNodeInfo::new_uncached)
     201              :             }
     202              :             Test(_) => {
     203              :                 unreachable!("this function should never be called in the test backend")
     204              :             }
     205              :         };
     206              : 
     207           27 :         info!("user successfully authenticated");
     208              :         Ok(res)
     209              :     }
     210              : 
     211              :     /// When applicable, wake the compute node, gaining its connection info in the process.
     212              :     /// The link auth flow doesn't support this, so we return [`None`] in that case.
     213           20 :     pub async fn wake_compute(
     214           20 :         &self,
     215           20 :         extra: &ConsoleReqExtra<'_>,
     216           20 :     ) -> Result<Option<CachedNodeInfo>, console::errors::WakeComputeError> {
     217           20 :         use BackendType::*;
     218           20 : 
     219           20 :         match self {
     220            0 :             Console(api, creds) => api.wake_compute(extra, creds).map_ok(Some).await,
     221           20 :             Postgres(api, creds) => api.wake_compute(extra, creds).map_ok(Some).await,
     222            0 :             Link(_) => Ok(None),
     223            0 :             Test(x) => x.wake_compute().map(Some),
     224              :         }
     225           20 :     }
     226              : }
        

Generated by: LCOV version 2.1-beta