use crate::bearer_auth::parse_bearer_header;
use crate::login_required::current_session_user_id;
use crate::token::AuthToken;
use crate::{AuthUser, auth_user};
use axum_core::extract::FromRequestParts;
use axum_core::response::{IntoResponse, Response};
use http::request::Parts;
use http::{HeaderMap, StatusCode};
use umbral::auth::Identity;
pub struct OptionalIdentity(pub Option<Identity>);
impl<S> FromRequestParts<S> for OptionalIdentity
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self(resolve_identity(&parts.headers).await))
}
}
pub struct CurrentIdentity(pub Identity);
impl<S> FromRequestParts<S> for CurrentIdentity
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
match resolve_identity(&parts.headers).await {
Some(id) => Ok(Self(id)),
None => Err((
StatusCode::UNAUTHORIZED,
axum_core::body::Body::from(
r#"{"error":"authentication required","code":"unauthenticated"}"#,
),
)
.into_response()),
}
}
}
pub async fn resolve_identity(headers: &HeaderMap) -> Option<Identity> {
if let Some(id) = identity_from_session(headers).await {
return Some(id);
}
identity_from_bearer(headers).await
}
async fn identity_from_session(headers: &HeaderMap) -> Option<Identity> {
let user_id = current_session_user_id(headers).await?;
let user: AuthUser = AuthUser::objects()
.filter(auth_user::ID.eq(user_id) & auth_user::IS_ACTIVE.eq(true))
.first()
.await
.ok()
.flatten()?;
Some(
Identity::user(crate::UserModel::id_string(&user))
.with_staff(user.is_staff)
.with_superuser(user.is_superuser)
.with_extra("auth", serde_json::json!("session")),
)
}
async fn identity_from_bearer(headers: &HeaderMap) -> Option<Identity> {
let plaintext = parse_bearer_header(headers)?;
let token = AuthToken::lookup(plaintext).await.ok().flatten()?;
let user: AuthUser = AuthUser::objects()
.filter(auth_user::ID.eq(token.user_id.id()) & auth_user::IS_ACTIVE.eq(true))
.first()
.await
.ok()
.flatten()?;
token.touch_last_used().await;
Some(
Identity::user(crate::UserModel::id_string(&user))
.with_staff(user.is_staff)
.with_superuser(user.is_superuser)
.with_extra("auth", serde_json::json!("bearer")),
)
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum StaffReject {
Unauthenticated,
NotStaff,
BadPk,
}
pub(crate) fn require_staff_decision<T: std::str::FromStr>(
identity: Option<&Identity>,
) -> Result<T, StaffReject> {
let id = identity.ok_or(StaffReject::Unauthenticated)?;
if !id.is_staff {
return Err(StaffReject::NotStaff);
}
id.user_pk::<T>().map_err(|_| StaffReject::BadPk)
}
pub struct RequireStaff<T = i64>(pub T);
impl<T, S> FromRequestParts<S> for RequireStaff<T>
where
T: std::str::FromStr + Send,
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let identity = resolve_identity(&parts.headers).await;
match require_staff_decision::<T>(identity.as_ref()) {
Ok(pk) => Ok(Self(pk)),
Err(StaffReject::Unauthenticated) => Err((
StatusCode::UNAUTHORIZED,
axum_core::body::Body::from(
r#"{"error":"authentication required","code":"unauthenticated"}"#,
),
)
.into_response()),
Err(StaffReject::NotStaff) => Err((
StatusCode::FORBIDDEN,
axum_core::body::Body::from(
r#"{"error":"staff access required","code":"forbidden"}"#,
),
)
.into_response()),
Err(StaffReject::BadPk) => Err((
StatusCode::BAD_REQUEST,
axum_core::body::Body::from(r#"{"error":"invalid user id","code":"bad_identity"}"#),
)
.into_response()),
}
}
}
#[cfg(test)]
mod require_staff_tests {
use super::{StaffReject, require_staff_decision};
use umbral::auth::Identity;
#[test]
fn gates_unauthenticated_staff_and_typed_pk() {
assert!(matches!(
require_staff_decision::<i64>(None),
Err(StaffReject::Unauthenticated)
));
let member = Identity::user(5);
assert!(matches!(
require_staff_decision::<i64>(Some(&member)),
Err(StaffReject::NotStaff)
));
let staff = Identity::user(7).with_staff(true);
assert_eq!(require_staff_decision::<i64>(Some(&staff)).unwrap(), 7);
let named = Identity::user("codename").with_staff(true);
assert!(matches!(
require_staff_decision::<i64>(Some(&named)),
Err(StaffReject::BadPk)
));
let named_ok = Identity::user("codename").with_staff(true);
assert_eq!(
require_staff_decision::<String>(Some(&named_ok)).unwrap(),
"codename"
);
}
}