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 : }
|