pub type SessionStore = axum_session::Session<crate::pool::SessionPool>;
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len().saturating_add(2));
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
#[derive(Debug, Clone, Default)]
pub struct AuditCtx {
pub config: crate::config::AuditConfig,
pub ip: Option<String>,
pub user_agent: Option<String>,
}
impl AuditCtx {
pub async fn record(
&self,
db: &crate::pool::Pool,
event_type: &str,
actor_id: Option<i64>,
target_id: Option<i64>,
details: Option<&str>,
) {
crate::auth::audit::record_or_log(
db,
crate::auth::audit::RecordInput {
event_type,
actor_id,
target_id,
ip: self.ip.as_deref(),
user_agent: self.user_agent.as_deref(),
details,
},
)
.await
}
}
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for AuditCtx {
type Rejection = std::convert::Infallible;
fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
let config = parts
.extensions
.get::<crate::config::AuditConfig>()
.cloned()
.unwrap_or_default();
let ip = if config.capture_ip {
parts
.extensions
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|ci| ci.0.ip().to_string())
.or_else(|| {
parts
.headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.map(|s| s.trim().to_string())
})
.or_else(|| {
parts
.headers
.get("x-real-ip")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
} else {
None
};
let user_agent = if config.capture_user_agent {
parts
.headers
.get(axum::http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
} else {
None
};
std::future::ready(Ok(AuditCtx {
config,
ip,
user_agent,
}))
}
}
async fn resolve_user_id<S: Send + Sync>(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Option<i64> {
#[cfg(feature = "tokens")]
if let Some(api) = parts.extensions.get::<crate::api_key::ApiKeyUser>() {
return Some(api.user_id);
}
match <crate::auth::Session as axum::extract::FromRequestParts<S>>::from_request_parts(
parts, state,
)
.await
{
Ok(session) => session
.current_user
.as_ref()
.filter(|u| !u.anonymous)
.map(|u| u.id as i64),
Err(_) => None,
}
}
#[derive(Clone, Copy, Debug)]
pub struct AuthUser {
pub id: i64,
}
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for AuthUser {
type Rejection = (axum::http::StatusCode, &'static str);
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
match resolve_user_id(parts, state).await {
Some(id) => Ok(AuthUser { id }),
None => Err((axum::http::StatusCode::UNAUTHORIZED, "not logged in")),
}
}
}
pub struct AuthzCtx {
user_id: Option<i64>,
db: crate::pool::Pool,
authority: crate::authz::SharedResourceAuthority,
audit: AuditCtx,
}
impl AuthzCtx {
pub fn user_id(&self) -> Option<i64> {
self.user_id
}
pub async fn require(
&self,
kind: &str,
id: i64,
min_role: crate::wire::ResourceRole,
) -> Result<i64, crate::authz::ResourceAuthzError> {
let uid = match self.user_id {
Some(uid) => uid,
None => return Err(crate::authz::ResourceAuthzError::Forbidden),
};
let res = crate::authz::require_resource(
&*self.authority,
&self.db,
uid,
crate::authz::ResourceRef::new(kind, id),
min_role,
)
.await;
if matches!(res, Err(crate::authz::ResourceAuthzError::Forbidden)) {
let details = format!(
r#"{{"kind":"{}","id":{},"min_role":"{}"}}"#,
json_escape(kind),
id,
min_role.as_str(),
);
self.audit
.record(
&self.db,
crate::auth::audit::RESOURCE_ACCESS_DENIED,
Some(uid),
None,
Some(&details),
)
.await;
}
res
}
}
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for AuthzCtx {
type Rejection = (axum::http::StatusCode, &'static str);
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
let audit = AuditCtx::from_request_parts(parts, state).await.unwrap();
let db = parts
.extensions
.get::<crate::pool::Pool>()
.cloned()
.ok_or((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"db pool not registered",
))?;
let authority = parts
.extensions
.get::<crate::authz::SharedResourceAuthority>()
.cloned()
.ok_or((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"resource authority not registered",
))?;
let user_id = resolve_user_id(parts, state).await;
Ok(AuthzCtx {
user_id,
db,
authority,
audit,
})
}
}
pub struct ResourceAuthorityExt(pub crate::authz::SharedResourceAuthority);
impl<S: Send + Sync> axum::extract::FromRequestParts<S> for ResourceAuthorityExt {
type Rejection = (axum::http::StatusCode, &'static str);
fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
let found = parts
.extensions
.get::<crate::authz::SharedResourceAuthority>()
.cloned();
std::future::ready(found.map(ResourceAuthorityExt).ok_or((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"resource authority not registered",
)))
}
}