kumo_server_common/http_server/
auth.rs

1use crate::acct::{log_authn, AuthnAuditRecord};
2use crate::authn_authz::{
3    ACLQueryDisposition, AccessControlList, AuthInfo, Identity, IdentityContext, Resource,
4};
5use crate::http_server::AppState;
6use async_trait::async_trait;
7use axum::extract::{FromRequestParts, Request, State};
8use axum::http::{StatusCode, Uri};
9use axum::middleware::Next;
10use axum::response::{IntoResponse, Response};
11use chrono::Utc;
12use config::{declare_event, load_config, SerdeWrappedValue};
13use serde::{Deserialize, Serialize};
14use std::net::{IpAddr, SocketAddr};
15use tokio::time::{Duration, Instant};
16
17lruttl::declare_cache! {
18/// Caches the results of the http server auth validation by auth credential
19static AUTH_CACHE: LruCacheWithTtl<AuthKind, Result<AuthKindResult, String>>::new("http_server_auth", 128);
20}
21
22declare_event! {
23static HTTP_AUTH_BASIC: Single("http_server_validate_auth_basic",
24    username: String,
25    password: Option<String>
26) -> SerdeWrappedValue<AuthKindResult>;
27}
28
29declare_event! {
30static HTTP_AUTH_BEARER: Single("http_server_validate_auth_bearer",
31    token: String
32) -> SerdeWrappedValue<AuthKindResult>;
33}
34
35/// Represents some authenticated identity.
36/// Use this as an extractor parameter when you need to reference
37/// that identity in the handler.
38#[derive(Debug, Clone, Hash, Eq, PartialEq)]
39enum AuthKind {
40    TrustedIp(IpAddr),
41    Basic {
42        user: String,
43        password: Option<String>,
44    },
45    Bearer {
46        token: String,
47    },
48}
49
50#[derive(Deserialize, Serialize, Clone, Debug)]
51#[serde(untagged)]
52pub enum AuthKindResult {
53    Bool(bool),
54    AuthInfo(AuthInfo),
55}
56
57impl Default for AuthKindResult {
58    fn default() -> Self {
59        Self::Bool(false)
60    }
61}
62
63impl AuthKind {
64    pub fn from_header(authorization: &str) -> Option<Self> {
65        let (kind, contents) = authorization.split_once(' ')?;
66        match kind {
67            "Basic" => {
68                let decoded = data_encoding::BASE64.decode(contents.as_bytes()).ok()?;
69                let decoded = String::from_utf8(decoded).ok()?;
70                let (user, password) = if let Some((id, password)) = decoded.split_once(':') {
71                    (id.to_string(), Some(password.to_string()))
72                } else {
73                    (decoded.to_string(), None)
74                };
75                Some(Self::Basic { user, password })
76            }
77            "Bearer" => Some(Self::Bearer {
78                token: contents.to_string(),
79            }),
80            _ => None,
81        }
82    }
83
84    async fn check_authentication_impl(&self) -> anyhow::Result<AuthKindResult> {
85        let mut config = load_config().await?;
86        let result = match self {
87            Self::TrustedIp(_) => AuthKindResult::Bool(true),
88            Self::Basic { user, password } => {
89                config
90                    .async_call_callback(&HTTP_AUTH_BASIC, (user.to_string(), password.clone()))
91                    .await?
92                    .0
93            }
94            Self::Bearer { token } => {
95                config
96                    .async_call_callback(&HTTP_AUTH_BEARER, token.to_string())
97                    .await?
98                    .0
99            }
100        };
101        config.put();
102        Ok(result)
103    }
104
105    async fn lookup_cache(&self) -> Option<Result<AuthKindResult, String>> {
106        AUTH_CACHE.get(self)
107    }
108
109    pub async fn check_authentication(&self) -> anyhow::Result<AuthKindResult> {
110        match self.lookup_cache().await {
111            Some(res) => res.map_err(|err| anyhow::anyhow!("{err}")),
112            None => {
113                let res = self
114                    .check_authentication_impl()
115                    .await
116                    .map_err(|err| format!("{err:#}"));
117
118                let res = AUTH_CACHE
119                    .insert(self.clone(), res, Instant::now() + Duration::from_secs(60))
120                    .await;
121
122                res.map_err(|err| anyhow::anyhow!("{err}"))
123            }
124        }
125    }
126}
127
128pub async fn auth_middleware(
129    State(state): State<AppState>,
130    mut request: Request,
131    next: Next,
132) -> Response {
133    let mut auth_info = AuthInfo::default();
134    let mut auth_kind = None;
135
136    // Gather peer address info
137    if let Some(remote_addr) = request
138        .extensions()
139        .get::<axum::extract::ConnectInfo<SocketAddr>>()
140        .map(|ci| ci.0)
141    {
142        let ip = remote_addr.ip();
143
144        // This is the authentic (as far as we're able to tell)
145        // peer address, so record that as an identity.
146        // There is no implicit trust associated with this fact.
147        auth_info.set_peer_address(Some(ip));
148
149        // If it is marked as trusted, update the auth kind state,
150        // and populate an appropriate group that can later be
151        // referenced in an ACL
152        if state.is_trusted_host(ip) {
153            auth_kind.replace(AuthKind::TrustedIp(ip));
154            auth_info.add_group("kumomta:http-listener-trusted-ip");
155        }
156    }
157
158    // Get authorization header
159    if let Some(authorization) = request.headers().get(axum::http::header::AUTHORIZATION) {
160        match authorization.to_str() {
161            Err(_) => {
162                return (StatusCode::BAD_REQUEST, "Malformed Authorization header").into_response()
163            }
164            Ok(authorization) => match AuthKind::from_header(authorization) {
165                None => {
166                    return (
167                        StatusCode::BAD_REQUEST,
168                        "Malformed or unsupported Authorization header",
169                    )
170                        .into_response()
171                }
172                Some(kind) => {
173                    let mut attempted_identity = match &kind {
174                        AuthKind::Basic { user, .. } => Identity {
175                            identity: user.to_string(),
176                            context: IdentityContext::HttpBasicAuth,
177                        },
178                        AuthKind::Bearer { .. } => Identity {
179                            identity: String::new(),
180                            context: IdentityContext::BearerToken,
181                        },
182                        AuthKind::TrustedIp(_) => {
183                            unreachable!();
184                        }
185                    };
186                    match kind.check_authentication().await {
187                        Ok(AuthKindResult::Bool(true)) => {
188                            // Store the authentication info for later retrieval
189                            auth_info.add_identity(attempted_identity.clone());
190                            log_authn(AuthnAuditRecord {
191                                attempted_identity,
192                                success: true,
193                                auth_info: auth_info.clone(),
194                                timestamp: Utc::now(),
195                            })
196                            .await
197                            .ok();
198                            auth_kind.replace(kind);
199                        }
200                        Ok(AuthKindResult::Bool(false)) => {
201                            let reason = format!(
202                                "{:?} Authentication Failed for {}",
203                                attempted_identity.context, attempted_identity.identity
204                            );
205                            log_authn(AuthnAuditRecord {
206                                attempted_identity,
207                                success: false,
208                                auth_info,
209                                timestamp: Utc::now(),
210                            })
211                            .await
212                            .ok();
213                            return (StatusCode::UNAUTHORIZED, reason).into_response();
214                        }
215                        Ok(AuthKindResult::AuthInfo(info)) => {
216                            if attempted_identity.identity.is_empty() {
217                                // eg: bearer token.
218                                // We assume that they expanded the token into
219                                // a usable identity string, so let's grab it
220                                // out of the auth_info they returned and update
221                                // it here
222
223                                if let Some(ident) = info.identities.first() {
224                                    attempted_identity = ident.clone();
225                                }
226                            }
227
228                            // Copy identities and groups across
229                            auth_info.merge_from(info);
230
231                            log_authn(AuthnAuditRecord {
232                                attempted_identity,
233                                success: true,
234                                auth_info: auth_info.clone(),
235                                timestamp: Utc::now(),
236                            })
237                            .await
238                            .ok();
239                            auth_kind.replace(kind);
240                        }
241                        Err(err) => {
242                            tracing::error!("Error validating {kind:?}: {err:#}");
243                            return (StatusCode::INTERNAL_SERVER_ERROR, "try again later")
244                                .into_response();
245                        }
246                    }
247                }
248            },
249        }
250    }
251
252    // Populate authentication information into the request state
253    if let Some(kind) = auth_kind.take() {
254        request.extensions_mut().insert(kind);
255    }
256    request.extensions_mut().insert(auth_info.clone());
257
258    // Check for authorization based on the URI + method
259    match HttpEndpointResource::new(state.local_addr, request.uri()) {
260        Ok(mut resource) => {
261            let resource_id = resource.ident.to_string();
262            let method = request.method().to_string();
263            match AccessControlList::query_resource_access(&mut resource, &auth_info, &method).await
264            {
265                Ok(result) => match result {
266                    ACLQueryDisposition::Allow { .. } => {}
267                    ACLQueryDisposition::Deny { .. } => {
268                        // In the response "denied GET on /something" means that
269                        // there was an explicit Deny
270                        return (
271                            StatusCode::UNAUTHORIZED,
272                            format!("{auth_info} denied {method} on {resource_id}"),
273                        )
274                            .into_response();
275                    }
276                    ACLQueryDisposition::DenyByDefault => {
277                        // In the response "not permitted GET on /something" means
278                        // that there was no explicit rule either way, and thus
279                        // access was not permitted, but was also not explicitly
280                        // denied.
281                        return (
282                            StatusCode::UNAUTHORIZED,
283                            format!("{auth_info} not permitted {method} on {resource_id}"),
284                        )
285                            .into_response();
286                    }
287                },
288                Err(err) => {
289                    tracing::error!("Error querying ACL: {err:#}");
290                    return (StatusCode::INTERNAL_SERVER_ERROR, "try again later").into_response();
291                }
292            }
293        }
294        Err(err) => {
295            tracing::error!("Error building HttpEndpointResource: {err:#}");
296            return (StatusCode::BAD_REQUEST, "malformed URI?").into_response();
297        }
298    }
299
300    // Now allow the request to run
301    next.run(request).await
302}
303
304#[derive(Clone)]
305pub struct HttpEndpointResource {
306    ident: String,
307    iter: std::vec::IntoIter<String>,
308}
309
310/// Basic defense against a potentially abusive network client
311const MAX_ACL_PATH_LEN: usize = 256;
312const MAX_ACL_PATH_COMPONENTS: usize = 10;
313
314impl HttpEndpointResource {
315    pub fn new(local_addr: SocketAddr, uri: &Uri) -> anyhow::Result<Self> {
316        let mut path: String = uri.path().to_string();
317        path.truncate(MAX_ACL_PATH_LEN);
318        let mut path_components: Vec<_> =
319            path[1..].splitn(MAX_ACL_PATH_COMPONENTS + 1, '/').collect();
320        path_components.truncate(MAX_ACL_PATH_COMPONENTS);
321
322        let mut resources_with_host_and_port = vec![];
323        let mut resources_with_path_only = vec![];
324
325        while !path_components.is_empty() {
326            let path = path_components.join("/");
327
328            resources_with_host_and_port.push(format!("http_listener/{local_addr}/{path}"));
329            resources_with_path_only.push(format!("http_listener/*/{path}"));
330
331            path_components.pop();
332        }
333        resources_with_host_and_port.push(format!("http_listener/{local_addr}"));
334
335        let mut resources = resources_with_host_and_port;
336        resources.append(&mut resources_with_path_only);
337        resources.push("http_listener".to_string());
338
339        let ident = resources[0].clone();
340
341        Ok(Self {
342            ident,
343            iter: resources.into_iter(),
344        })
345    }
346}
347
348#[async_trait]
349impl Resource for HttpEndpointResource {
350    fn resource_id(&self) -> &str {
351        &self.ident
352    }
353
354    async fn next_resource_id(&mut self) -> Option<String> {
355        self.iter.next()
356    }
357}
358
359#[cfg(test)]
360#[test]
361fn test_http_endpoint_resource_expansion() {
362    let res = HttpEndpointResource::new(
363        "127.0.0.1:8080".parse().unwrap(),
364        &Uri::from_static("https://user:pass@example.com:8080/foo/bar/baz"),
365    )
366    .unwrap();
367
368    assert_eq!(
369        res.iter.collect::<Vec<String>>(),
370        [
371            "http_listener/127.0.0.1:8080/foo/bar/baz",
372            "http_listener/127.0.0.1:8080/foo/bar",
373            "http_listener/127.0.0.1:8080/foo",
374            "http_listener/127.0.0.1:8080",
375            "http_listener/*/foo/bar/baz",
376            "http_listener/*/foo/bar",
377            "http_listener/*/foo",
378            "http_listener"
379        ]
380        .into_iter()
381        .map(Into::into)
382        .collect::<Vec<String>>(),
383    );
384}
385
386impl<B> FromRequestParts<B> for AuthKind
387where
388    B: Send + Sync,
389{
390    type Rejection = (StatusCode, &'static str);
391
392    async fn from_request_parts(
393        parts: &mut axum::http::request::Parts,
394        _: &B,
395    ) -> Result<Self, Self::Rejection> {
396        let kind = parts
397            .extensions
398            .get::<AuthKind>()
399            .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
400
401        Ok(kind.clone())
402    }
403}