kumo_server_common/http_server/
auth.rs1use 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! {
18static 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#[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 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 auth_info.set_peer_address(Some(ip));
148
149 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 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 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 if let Some(ident) = info.identities.first() {
224 attempted_identity = ident.clone();
225 }
226 }
227
228 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 if let Some(kind) = auth_kind.take() {
254 request.extensions_mut().insert(kind);
255 }
256 request.extensions_mut().insert(auth_info.clone());
257
258 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 return (
271 StatusCode::UNAUTHORIZED,
272 format!("{auth_info} denied {method} on {resource_id}"),
273 )
274 .into_response();
275 }
276 ACLQueryDisposition::DenyByDefault => {
277 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 next.run(request).await
302}
303
304#[derive(Clone)]
305pub struct HttpEndpointResource {
306 ident: String,
307 iter: std::vec::IntoIter<String>,
308}
309
310const 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}