use std::net::IpAddr;
use http::HeaderMap;
use super::{Auth, Result, require_admin};
use crate::store::{Session, User};
use crate::types::{AuthError, AuthRequest, AuthResponse, CookieChange, CreatedUser, Invalid};
#[derive(Debug, Clone, Copy, Default)]
pub struct Caller<'a> {
signed_in: Option<(&'a User, &'a Session)>,
session_id: Option<&'a str>,
headers: Option<&'a HeaderMap>,
address: Option<IpAddr>,
}
impl<'a> Caller<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn session(mut self, session_id: &'a str) -> Self {
self.session_id = Some(session_id);
self
}
pub fn signed_in(mut self, user: &'a User, session: &'a Session) -> Self {
self.signed_in = Some((user, session));
self
}
pub fn headers(mut self, headers: &'a HeaderMap) -> Self {
self.headers = Some(headers);
self
}
#[cfg(feature = "axum08")]
pub(crate) fn http_headers(&self) -> Option<&'a HeaderMap> {
self.headers
}
pub fn address(mut self, address: IpAddr) -> Self {
self.address = Some(address);
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Dispatched {
pub response: AuthResponse,
pub cookie: Option<SessionCookie>,
}
impl Dispatched {
fn new(response: AuthResponse) -> Self {
Self {
response,
cookie: None,
}
}
fn set(response: AuthResponse, session: Session) -> Self {
Self {
response,
cookie: Some(SessionCookie::Set(session)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionCookie {
Set(Session),
Clear,
}
impl Auth {
pub async fn dispatch(&self, caller: &Caller<'_>, request: AuthRequest) -> Result<Dispatched> {
let change = request.cookie();
let dispatched = match request {
AuthRequest::Config => Dispatched::new(AuthResponse::Config(crate::AuthConfig {
password: true,
google: self.offers_google(),
cloudflare: self.cloudflare_available_for(caller).await?,
logins: self.uses_logins(),
})),
AuthRequest::Me => {
let (user, session) = self.caller_session(caller).await?;
let info = self.session_info(&user, &session).await?;
Dispatched::new(AuthResponse::User(info))
}
AuthRequest::Login(request) => {
let Some(address) = caller.address else {
tracing::error!(
"auth: sign-in needs the client's address, which the caller lacks"
);
return Err(AuthError::Forbidden.into());
};
let (user, session) = self.sign_in_password(address, &request).await?;
Dispatched::set(AuthResponse::User(user.info()), session)
}
AuthRequest::CloudflareSignIn => {
let (user, session) = self.sign_in_cloudflare_for(caller).await?;
Dispatched::set(AuthResponse::User(user.info()), session)
}
AuthRequest::Logout => {
if let Some(id) = self.caller_session_id(caller) {
self.sign_out(&id).await?;
}
Dispatched {
response: AuthResponse::Done,
cookie: Some(SessionCookie::Clear),
}
}
AuthRequest::RegisterInfo { token } => Dispatched::new(AuthResponse::RegisterInfo(
self.register_info(&token).await?,
)),
AuthRequest::Register { token, request } => {
let (user, session) = self.register(&token, &request).await?;
Dispatched::set(AuthResponse::User(user.info()), session)
}
AuthRequest::ChangePassword(change) => {
let (user, session) = self.caller_session(caller).await?;
let user = self.change_password(&user, &session, &change).await?;
let info = self.session_info(&user, &session).await?;
Dispatched::new(AuthResponse::User(info))
}
AuthRequest::UpdateAccount(change) => {
let (user, session) = self.caller_session(caller).await?;
if change.role.is_some() {
return Err(AuthError::Invalid(Invalid::RoleNotChangeable).into());
}
let user = self.update_user(&user, &session, user.id, &change).await?;
let info = self.session_info(&user, &session).await?;
Dispatched::new(AuthResponse::User(info))
}
AuthRequest::Users => {
let (admin, _) = self.caller_admin(caller).await?;
Dispatched::new(AuthResponse::Users(self.users(&admin).await?))
}
AuthRequest::CreateUser(new) => {
let (admin, session) = self.caller_admin(caller).await?;
let (entry, link) = self.create_user(&admin, &session, &new).await?;
Dispatched::new(AuthResponse::CreatedUser(CreatedUser { entry, link }))
}
AuthRequest::UpdateUser { id, change } => {
let (admin, session) = self.caller_admin(caller).await?;
let user = self.update_user(&admin, &session, id, &change).await?;
Dispatched::new(AuthResponse::User(user.info()))
}
AuthRequest::DeleteUser { id } => {
let (admin, session) = self.caller_admin(caller).await?;
self.delete_user(&admin, &session, id).await?;
Dispatched::new(AuthResponse::Done)
}
AuthRequest::IssueLink { id } => {
let (admin, session) = self.caller_admin(caller).await?;
Dispatched::new(AuthResponse::Link(
self.issue_link(&admin, &session, id).await?,
))
}
AuthRequest::RevokeLink { id } => {
let (admin, session) = self.caller_admin(caller).await?;
self.revoke_link(&admin, &session, id).await?;
Dispatched::new(AuthResponse::Done)
}
AuthRequest::SignOutUser { id } => {
let (admin, session) = self.caller_admin(caller).await?;
self.sign_out_everywhere(&admin, &session, id).await?;
Dispatched::new(AuthResponse::Done)
}
AuthRequest::RevokeAccess { id } => {
let (admin, session) = self.caller_admin(caller).await?;
let user = self.revoke_access(&admin, &session, id).await?;
Dispatched::new(AuthResponse::User(user.info()))
}
AuthRequest::Impersonate { id } => {
let (admin, session) = self.caller_admin(caller).await?;
let (user, session) = self.impersonate(&admin, &session, id).await?;
let info = self.session_info(&user, &session).await?;
Dispatched::set(AuthResponse::User(info), session)
}
AuthRequest::StopImpersonating => {
let (_, session) = self.caller_session(caller).await?;
let (admin, session) = self.stop_impersonating(&session).await?;
Dispatched::set(AuthResponse::User(admin.info()), session)
}
AuthRequest::Cloudflare => {
self.caller_admin(caller).await?;
Dispatched::new(AuthResponse::Cloudflare(self.cloudflare_settings().await?))
}
AuthRequest::SetCloudflare(config) => {
let (admin, session) = self.caller_admin(caller).await?;
Dispatched::new(AuthResponse::Cloudflare(
self.set_cloudflare_settings(&admin, &session, config)
.await?,
))
}
_ => return Err(AuthError::NotFound.into()),
};
debug_assert_eq!(
change.map(|change| change == CookieChange::Clear),
dispatched
.cookie
.as_ref()
.map(|cookie| *cookie == SessionCookie::Clear),
);
Ok(dispatched)
}
async fn caller_session(&self, caller: &Caller<'_>) -> Result<(User, Session)> {
if let Some((user, session)) = caller.signed_in {
return Ok((user.clone(), session.clone()));
}
let Some(id) = self.caller_session_id(caller) else {
return Err(AuthError::Unauthorized.into());
};
match self.session(&id).await? {
Some(signed_in) => Ok(signed_in),
None => Err(AuthError::Unauthorized.into()),
}
}
async fn caller_admin(&self, caller: &Caller<'_>) -> Result<(User, Session)> {
let (user, session) = self.caller_session(caller).await?;
require_admin(&user)?;
Ok((user, session))
}
fn caller_session_id(&self, caller: &Caller<'_>) -> Option<std::string::String> {
if let Some((_, session)) = caller.signed_in {
return Some(session.id.clone());
}
if let Some(id) = caller.session_id {
return Some(id.into());
}
self.session_id(caller.headers?)
}
#[cfg(feature = "cloudflare")]
async fn cloudflare_available_for(&self, caller: &Caller<'_>) -> Result<bool> {
match caller.headers {
Some(headers) => self.cloudflare_available(headers).await,
None => Ok(false),
}
}
#[cfg(not(feature = "cloudflare"))]
async fn cloudflare_available_for(&self, _: &Caller<'_>) -> Result<bool> {
Ok(false)
}
#[cfg(feature = "cloudflare")]
async fn sign_in_cloudflare_for(&self, caller: &Caller<'_>) -> Result<(User, Session)> {
match caller.headers {
Some(headers) => self.sign_in_cloudflare(headers).await,
None => self.sign_in_cloudflare(&HeaderMap::new()).await,
}
}
#[cfg(not(feature = "cloudflare"))]
async fn sign_in_cloudflare_for(&self, _: &Caller<'_>) -> Result<(User, Session)> {
Err(AuthError::NotFound.into())
}
#[cfg(feature = "cloudflare")]
async fn cloudflare_settings(&self) -> Result<crate::types::CloudflareConfig> {
Ok(self.cloudflare().await?.unwrap_or_default())
}
#[cfg(not(feature = "cloudflare"))]
async fn cloudflare_settings(&self) -> Result<crate::types::CloudflareConfig> {
Err(AuthError::NotFound.into())
}
#[cfg(feature = "cloudflare")]
async fn set_cloudflare_settings(
&self,
admin: &User,
session: &Session,
config: crate::types::CloudflareConfig,
) -> Result<crate::types::CloudflareConfig> {
self.set_cloudflare(admin, session, config).await
}
#[cfg(not(feature = "cloudflare"))]
async fn set_cloudflare_settings(
&self,
_: &User,
_: &Session,
_: crate::types::CloudflareConfig,
) -> Result<crate::types::CloudflareConfig> {
Err(AuthError::NotFound.into())
}
}
#[cfg(test)]
mod tests;