Line data Source code
1 : mod classic;
2 : mod hacks;
3 : mod link;
4 :
5 : pub use link::LinkAuthError;
6 : use tokio_postgres::config::AuthKeys;
7 :
8 : use crate::auth::credentials::check_peer_addr_is_in_list;
9 : use crate::auth::validate_password_and_exchange;
10 : use crate::cache::Cached;
11 : use crate::console::errors::GetAuthInfoError;
12 : use crate::console::provider::{CachedRoleSecret, ConsoleBackend};
13 : use crate::console::AuthSecret;
14 : use crate::context::RequestMonitoring;
15 : use crate::proxy::wake_compute::wake_compute;
16 : use crate::proxy::NeonOptions;
17 : use crate::stream::Stream;
18 : use crate::{
19 : auth::{self, ComputeUserInfoMaybeEndpoint},
20 : config::AuthenticationConfig,
21 : console::{
22 : self,
23 : provider::{CachedAllowedIps, CachedNodeInfo},
24 : Api,
25 : },
26 : stream, url,
27 : };
28 : use crate::{scram, EndpointCacheKey, EndpointId, RoleName};
29 : use futures::TryFutureExt;
30 : use std::sync::Arc;
31 : use tokio::io::{AsyncRead, AsyncWrite};
32 : use tracing::info;
33 :
34 : /// Alternative to [`std::borrow::Cow`] but doesn't need `T: ToOwned` as we don't need that functionality
35 : pub enum MaybeOwned<'a, T> {
36 : Owned(T),
37 : Borrowed(&'a T),
38 : }
39 :
40 : impl<T> std::ops::Deref for MaybeOwned<'_, T> {
41 : type Target = T;
42 :
43 318 : fn deref(&self) -> &Self::Target {
44 318 : match self {
45 147 : MaybeOwned::Owned(t) => t,
46 171 : MaybeOwned::Borrowed(t) => t,
47 : }
48 318 : }
49 : }
50 :
51 : /// This type serves two purposes:
52 : ///
53 : /// * When `T` is `()`, it's just a regular auth backend selector
54 : /// which we use in [`crate::config::ProxyConfig`].
55 : ///
56 : /// * However, when we substitute `T` with [`ComputeUserInfoMaybeEndpoint`],
57 : /// this helps us provide the credentials only to those auth
58 : /// backends which require them for the authentication process.
59 : pub enum BackendType<'a, T> {
60 : /// Cloud API (V2).
61 : Console(MaybeOwned<'a, ConsoleBackend>, T),
62 : /// Authentication via a web browser.
63 : Link(MaybeOwned<'a, url::ApiUrl>),
64 : }
65 :
66 : pub trait TestBackend: Send + Sync + 'static {
67 : fn wake_compute(&self) -> Result<CachedNodeInfo, console::errors::WakeComputeError>;
68 : fn get_allowed_ips_and_secret(
69 : &self,
70 : ) -> Result<(CachedAllowedIps, Option<CachedRoleSecret>), console::errors::GetAuthInfoError>;
71 : }
72 :
73 : impl std::fmt::Display for BackendType<'_, ()> {
74 23 : fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 23 : use BackendType::*;
76 23 : match self {
77 20 : Console(api, _) => match &**api {
78 1 : ConsoleBackend::Console(endpoint) => {
79 1 : fmt.debug_tuple("Console").field(&endpoint.url()).finish()
80 : }
81 : #[cfg(any(test, feature = "testing"))]
82 19 : ConsoleBackend::Postgres(endpoint) => {
83 19 : fmt.debug_tuple("Postgres").field(&endpoint.url()).finish()
84 : }
85 : #[cfg(test)]
86 0 : ConsoleBackend::Test(_) => fmt.debug_tuple("Test").finish(),
87 : },
88 3 : Link(url) => fmt.debug_tuple("Link").field(&url.as_str()).finish(),
89 : }
90 23 : }
91 : }
92 :
93 : impl<T> BackendType<'_, T> {
94 : /// Very similar to [`std::option::Option::as_ref`].
95 : /// This helps us pass structured config to async tasks.
96 92 : pub fn as_ref(&self) -> BackendType<'_, &T> {
97 92 : use BackendType::*;
98 92 : match self {
99 89 : Console(c, x) => Console(MaybeOwned::Borrowed(c), x),
100 3 : Link(c) => Link(MaybeOwned::Borrowed(c)),
101 : }
102 92 : }
103 : }
104 :
105 : impl<'a, T> BackendType<'a, T> {
106 : /// Very similar to [`std::option::Option::map`].
107 : /// Maps [`BackendType<T>`] to [`BackendType<R>`] by applying
108 : /// a function to a contained value.
109 92 : pub fn map<R>(self, f: impl FnOnce(T) -> R) -> BackendType<'a, R> {
110 92 : use BackendType::*;
111 92 : match self {
112 89 : Console(c, x) => Console(c, f(x)),
113 3 : Link(c) => Link(c),
114 : }
115 92 : }
116 : }
117 :
118 : impl<'a, T, E> BackendType<'a, Result<T, E>> {
119 : /// Very similar to [`std::option::Option::transpose`].
120 : /// This is most useful for error handling.
121 50 : pub fn transpose(self) -> Result<BackendType<'a, T>, E> {
122 50 : use BackendType::*;
123 50 : match self {
124 47 : Console(c, x) => x.map(|x| Console(c, x)),
125 3 : Link(c) => Ok(Link(c)),
126 : }
127 50 : }
128 : }
129 :
130 : pub struct ComputeCredentials<T> {
131 : pub info: ComputeUserInfo,
132 : pub keys: T,
133 : }
134 :
135 0 : #[derive(Debug, Clone)]
136 : pub struct ComputeUserInfoNoEndpoint {
137 : pub user: RoleName,
138 : pub options: NeonOptions,
139 : }
140 :
141 85 : #[derive(Debug, Clone)]
142 : pub struct ComputeUserInfo {
143 : pub endpoint: EndpointId,
144 : pub user: RoleName,
145 : pub options: NeonOptions,
146 : }
147 :
148 : impl ComputeUserInfo {
149 38 : pub fn endpoint_cache_key(&self) -> EndpointCacheKey {
150 38 : self.options.get_cache_key(&self.endpoint)
151 38 : }
152 : }
153 :
154 : pub enum ComputeCredentialKeys {
155 : #[cfg(any(test, feature = "testing"))]
156 : Password(Vec<u8>),
157 : AuthKeys(AuthKeys),
158 : }
159 :
160 : impl TryFrom<ComputeUserInfoMaybeEndpoint> for ComputeUserInfo {
161 : // user name
162 : type Error = ComputeUserInfoNoEndpoint;
163 :
164 47 : fn try_from(user_info: ComputeUserInfoMaybeEndpoint) -> Result<Self, Self::Error> {
165 47 : match user_info.endpoint_id {
166 3 : None => Err(ComputeUserInfoNoEndpoint {
167 3 : user: user_info.user,
168 3 : options: user_info.options,
169 3 : }),
170 44 : Some(endpoint) => Ok(ComputeUserInfo {
171 44 : endpoint,
172 44 : user: user_info.user,
173 44 : options: user_info.options,
174 44 : }),
175 : }
176 47 : }
177 : }
178 :
179 : /// True to its name, this function encapsulates our current auth trade-offs.
180 : /// Here, we choose the appropriate auth flow based on circumstances.
181 : ///
182 : /// All authentication flows will emit an AuthenticationOk message if successful.
183 47 : async fn auth_quirks(
184 47 : ctx: &mut RequestMonitoring,
185 47 : api: &impl console::Api,
186 47 : user_info: ComputeUserInfoMaybeEndpoint,
187 47 : client: &mut stream::PqStream<Stream<impl AsyncRead + AsyncWrite + Unpin>>,
188 47 : allow_cleartext: bool,
189 47 : config: &'static AuthenticationConfig,
190 47 : ) -> auth::Result<ComputeCredentials<ComputeCredentialKeys>> {
191 : // If there's no project so far, that entails that client doesn't
192 : // support SNI or other means of passing the endpoint (project) name.
193 : // We now expect to see a very specific payload in the place of password.
194 47 : let (info, unauthenticated_password) = match user_info.try_into() {
195 3 : Err(info) => {
196 3 : let res = hacks::password_hack_no_authentication(info, client, &mut ctx.latency_timer)
197 3 : .await?;
198 :
199 2 : ctx.set_endpoint_id(res.info.endpoint.clone());
200 2 : tracing::Span::current().record("ep", &tracing::field::display(&res.info.endpoint));
201 2 :
202 2 : (res.info, Some(res.keys))
203 : }
204 44 : Ok(info) => (info, None),
205 : };
206 :
207 46 : info!("fetching user's authentication info");
208 290 : let (allowed_ips, maybe_secret) = api.get_allowed_ips_and_secret(ctx, &info).await?;
209 :
210 : // check allowed list
211 42 : if !check_peer_addr_is_in_list(&ctx.peer_addr, &allowed_ips) {
212 3 : return Err(auth::AuthError::ip_address_not_allowed());
213 39 : }
214 39 : let cached_secret = match maybe_secret {
215 0 : Some(secret) => secret,
216 267 : None => api.get_role_secret(ctx, &info).await?,
217 : };
218 :
219 39 : let secret = cached_secret.value.clone().unwrap_or_else(|| {
220 1 : // If we don't have an authentication secret, we mock one to
221 1 : // prevent malicious probing (possible due to missing protocol steps).
222 1 : // This mocked secret will never lead to successful authentication.
223 1 : info!("authentication info not found, mocking it");
224 1 : AuthSecret::Scram(scram::ServerSecret::mock(&info.user, rand::random()))
225 39 : });
226 39 : match authenticate_with_secret(
227 39 : ctx,
228 39 : secret,
229 39 : info,
230 39 : client,
231 39 : unauthenticated_password,
232 39 : allow_cleartext,
233 39 : config,
234 39 : )
235 74 : .await
236 : {
237 36 : Ok(keys) => Ok(keys),
238 3 : Err(e) => {
239 3 : if e.is_auth_failed() {
240 3 : // The password could have been changed, so we invalidate the cache.
241 3 : cached_secret.invalidate();
242 3 : }
243 3 : Err(e)
244 : }
245 : }
246 47 : }
247 :
248 39 : async fn authenticate_with_secret(
249 39 : ctx: &mut RequestMonitoring,
250 39 : secret: AuthSecret,
251 39 : info: ComputeUserInfo,
252 39 : client: &mut stream::PqStream<Stream<impl AsyncRead + AsyncWrite + Unpin>>,
253 39 : unauthenticated_password: Option<Vec<u8>>,
254 39 : allow_cleartext: bool,
255 39 : config: &'static AuthenticationConfig,
256 39 : ) -> auth::Result<ComputeCredentials<ComputeCredentialKeys>> {
257 39 : if let Some(password) = unauthenticated_password {
258 2 : let auth_outcome = validate_password_and_exchange(&password, secret)?;
259 2 : let keys = match auth_outcome {
260 2 : crate::sasl::Outcome::Success(key) => key,
261 0 : crate::sasl::Outcome::Failure(reason) => {
262 0 : info!("auth backend failed with an error: {reason}");
263 0 : return Err(auth::AuthError::auth_failed(&*info.user));
264 : }
265 : };
266 :
267 : // we have authenticated the password
268 2 : client.write_message_noflush(&pq_proto::BeMessage::AuthenticationOk)?;
269 :
270 2 : return Ok(ComputeCredentials { info, keys });
271 37 : }
272 37 :
273 37 : // -- the remaining flows are self-authenticating --
274 37 :
275 37 : // Perform cleartext auth if we're allowed to do that.
276 37 : // Currently, we use it for websocket connections (latency).
277 37 : if allow_cleartext {
278 0 : return hacks::authenticate_cleartext(info, client, &mut ctx.latency_timer, secret).await;
279 37 : }
280 37 :
281 37 : // Finally, proceed with the main auth flow (SCRAM-based).
282 74 : classic::authenticate(info, client, config, &mut ctx.latency_timer, secret).await
283 39 : }
284 :
285 : impl<'a> BackendType<'a, ComputeUserInfoMaybeEndpoint> {
286 : /// Get compute endpoint name from the credentials.
287 50 : pub fn get_endpoint(&self) -> Option<EndpointId> {
288 50 : use BackendType::*;
289 50 :
290 50 : match self {
291 47 : Console(_, user_info) => user_info.endpoint_id.clone(),
292 3 : Link(_) => Some("link".into()),
293 : }
294 50 : }
295 :
296 : /// Get username from the credentials.
297 50 : pub fn get_user(&self) -> &str {
298 50 : use BackendType::*;
299 50 :
300 50 : match self {
301 47 : Console(_, user_info) => &user_info.user,
302 3 : Link(_) => "link",
303 : }
304 50 : }
305 :
306 : /// Authenticate the client via the requested backend, possibly using credentials.
307 0 : #[tracing::instrument(fields(allow_cleartext = allow_cleartext), skip_all)]
308 : pub async fn authenticate(
309 : self,
310 : ctx: &mut RequestMonitoring,
311 : client: &mut stream::PqStream<Stream<impl AsyncRead + AsyncWrite + Unpin>>,
312 : allow_cleartext: bool,
313 : config: &'static AuthenticationConfig,
314 : ) -> auth::Result<(CachedNodeInfo, BackendType<'a, ComputeUserInfo>)> {
315 : use BackendType::*;
316 :
317 : let res = match self {
318 : Console(api, user_info) => {
319 47 : info!(
320 47 : user = &*user_info.user,
321 47 : project = user_info.endpoint(),
322 47 : "performing authentication using the console"
323 47 : );
324 :
325 : let compute_credentials =
326 : auth_quirks(ctx, &*api, user_info, client, allow_cleartext, config).await?;
327 :
328 : let mut num_retries = 0;
329 : let mut node =
330 : wake_compute(&mut num_retries, ctx, &api, &compute_credentials.info).await?;
331 :
332 : ctx.set_project(node.aux.clone());
333 :
334 : match compute_credentials.keys {
335 : #[cfg(any(test, feature = "testing"))]
336 : ComputeCredentialKeys::Password(password) => node.config.password(password),
337 : ComputeCredentialKeys::AuthKeys(auth_keys) => node.config.auth_keys(auth_keys),
338 : };
339 :
340 : (node, BackendType::Console(api, compute_credentials.info))
341 : }
342 : // NOTE: this auth backend doesn't use client credentials.
343 : Link(url) => {
344 3 : info!("performing link authentication");
345 :
346 : let node_info = link::authenticate(ctx, &url, client).await?;
347 :
348 : (
349 : CachedNodeInfo::new_uncached(node_info),
350 : BackendType::Link(url),
351 : )
352 : }
353 : };
354 :
355 39 : info!("user successfully authenticated");
356 : Ok(res)
357 : }
358 : }
359 :
360 : impl BackendType<'_, ComputeUserInfo> {
361 42 : pub async fn get_allowed_ips_and_secret(
362 42 : &self,
363 42 : ctx: &mut RequestMonitoring,
364 42 : ) -> Result<(CachedAllowedIps, Option<CachedRoleSecret>), GetAuthInfoError> {
365 42 : use BackendType::*;
366 42 : match self {
367 320 : Console(api, user_info) => api.get_allowed_ips_and_secret(ctx, user_info).await,
368 0 : Link(_) => Ok((Cached::new_uncached(Arc::new(vec![])), None)),
369 : }
370 42 : }
371 :
372 : /// When applicable, wake the compute node, gaining its connection info in the process.
373 : /// The link auth flow doesn't support this, so we return [`None`] in that case.
374 41 : pub async fn wake_compute(
375 41 : &self,
376 41 : ctx: &mut RequestMonitoring,
377 41 : ) -> Result<Option<CachedNodeInfo>, console::errors::WakeComputeError> {
378 41 : use BackendType::*;
379 41 :
380 41 : match self {
381 41 : Console(api, user_info) => api.wake_compute(ctx, user_info).map_ok(Some).await,
382 0 : Link(_) => Ok(None),
383 : }
384 41 : }
385 : }
|