use crate::error::Error;
use crate::state::AppState;
use axum::extract::{FromRequestParts, OptionalFromRequestParts, OriginalUri};
use http::request::Parts;
use indexmap::IndexMap;
use postrust_auth::AuthResult;
use postrust_core::trace::{TraceContext, TraceId};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct User {
auth: AuthResult,
anonymous: bool,
method: String,
path: String,
headers: IndexMap<String, String>,
cookies: IndexMap<String, String>,
trace_id: Option<TraceId>,
}
impl User {
pub fn role(&self) -> &str {
&self.auth.role
}
pub fn claims(&self) -> &HashMap<String, serde_json::Value> {
&self.auth.claims
}
pub fn claim(&self, name: &str) -> Option<&serde_json::Value> {
self.auth.claims.get(name)
}
pub fn is_anonymous(&self) -> bool {
self.anonymous
}
pub fn trace_id(&self) -> Option<TraceId> {
self.trace_id
}
pub(crate) fn session<'a>(
&self,
config: &'a postrust_core::AppConfig,
) -> postrust_core::Session<'a> {
postrust_core::Session::new(config, self.auth.role.clone(), self.auth.claim_settings())
.request(self.method.clone(), self.path.clone())
.headers(&self.headers)
.cookies(&self.cookies)
.trace_id(self.trace_id.map(|t| t.to_string()))
}
async fn from_parts(parts: &Parts, state: &AppState) -> Result<Self, Error> {
let auth = postrust_engine::authenticate(state, &parts.headers).await?;
let path = parts
.extensions
.get::<OriginalUri>()
.map(|uri| uri.path())
.unwrap_or_else(|| parts.uri.path())
.to_string();
Ok(Self {
auth,
anonymous: postrust_engine::authorization(&parts.headers).is_none(),
method: parts.method.to_string(),
path,
headers: request_headers(&parts.headers),
cookies: request_cookies(&parts.headers),
trace_id: parts
.extensions
.get::<TraceContext>()
.map(|context| context.trace_id),
})
}
}
impl FromRequestParts<Arc<AppState>> for User {
type Rejection = Error;
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<AppState>,
) -> Result<Self, Self::Rejection> {
Self::from_parts(parts, state).await
}
}
impl OptionalFromRequestParts<Arc<AppState>> for User {
type Rejection = Error;
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<AppState>,
) -> Result<Option<Self>, Self::Rejection> {
match postrust_engine::authorization(&parts.headers) {
None => Ok(None),
Some(_) => Self::from_parts(parts, state).await.map(Some),
}
}
}
fn request_headers(headers: &http::HeaderMap) -> IndexMap<String, String> {
headers
.iter()
.filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
.collect()
}
fn request_cookies(headers: &http::HeaderMap) -> IndexMap<String, String> {
headers
.get(http::header::COOKIE)
.and_then(|v| v.to_str().ok())
.map(|s| {
s.split(';')
.filter_map(|cookie| {
let (key, value) = cookie.trim().split_once('=')?;
Some((key.to_string(), value.to_string()))
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headers_and_cookies_are_read_as_rest_reads_them() {
let request = http::Request::get("/items")
.header("accept", "application/json")
.header("X-Tenant", "acme")
.header("x-repeated", "one")
.header("x-repeated", "two")
.header("cookie", "session=abc; theme=dark;broken; a=b=c")
.body(Vec::<u8>::new())
.unwrap();
let rest = postrust_core::parse_request(&request, "public", &["public".to_string()], None)
.expect("REST reads the request");
assert_eq!(request_headers(request.headers()), rest.headers);
assert_eq!(request_cookies(request.headers()), rest.cookies);
assert_eq!(rest.cookies.get("a").map(String::as_str), Some("b=c"));
}
}