mod columns;
pub(crate) mod config;
mod email_change;
pub(crate) mod external;
mod gate;
pub(crate) mod impersonation;
pub(crate) mod input;
mod login;
pub(crate) mod mail;
pub(crate) mod mount;
mod page;
pub(crate) mod panel_user;
pub(crate) mod password;
pub(crate) mod password_reset;
pub(crate) mod profile;
mod realm;
pub(crate) mod registration;
mod session;
mod tenant_profile;
pub(crate) mod tenant_registration;
pub(crate) mod throttle;
mod token;
pub(crate) mod two_factor;
pub(crate) mod verification;
use std::{
any::{Any, TypeId},
sync::Arc,
};
use topcoat::{
context::{Cx, try_context},
router::{
error::{forbidden, redirect, unauthorized},
request::{method, original_headers, original_method, uri},
},
};
use self::realm::realms;
pub use self::{
columns::ColumnPreference,
email_change::{EmailChange, request_email_change},
impersonation::{IMPERSONATE_FIELD, impersonate_button, impersonator},
login::{LOGIN_FIELD, NEXT_FIELD, PASSWORD_FIELD, TENANT_FIELD, safe_next},
password::{AdminUser, create_admin, hash_password, verify_password},
password_reset::{PasswordResetter, RESET_LIFETIME},
registration::{MIN_PASSWORD_CHARS, new_password_errors},
session::{
AuthSession, SESSION_COOKIE_PREFIX, SESSION_LIFETIME, mint_session,
revoke_sessions_for_user, session_cookie,
},
tenant_profile::TenantEditor,
tenant_registration::TenantRegistrar,
token::{AuthToken, TokenPurpose},
two_factor::{AuthFactor, FactorKind},
verification::{EmailVerifier, VERIFICATION_LIFETIME},
};
pub(crate) use self::{
columns::{load_choice, save_choice},
config::{Auth, AuthConfig},
gate::{PagePanelStamp, PanelGate, RuntimeGate, still_signed_in},
impersonation::{may_impersonate, start as impersonate, stop_url as stop_impersonating_url},
login::{logout_url, tenant_url},
mail::AuthMail,
panel_user::{Authenticator, PanelUser},
password::{PasswordAuth, end_changed_credentials},
password_reset::PasswordReset,
profile::{Profile, ProfileEditor, ProfileUpdate, offered as profile_offered, profile_url},
realm::{
AuthHost, CurrentRealm, Host, Realm, SettingsPage, SettingsPaths, current_realm,
panel_prefix,
},
registration::{Registrar, SignUp},
tenant_profile::{offered as tenant_profile_offered, tenant_profile_url},
tenant_registration::{
TenantRegistration, offered as tenant_registration_offered, register_tenant_url,
},
throttle::LoginThrottle,
two_factor::{TwoFactor, offered as two_factor_offered, settings_url as two_factor_url},
verification::EmailVerification,
};
use crate::{
BoxFuture,
tenancy::{Membership, TenantId},
};
pub(crate) trait DynAuthenticator: Send + Sync {
fn verify<'a>(
&'a self,
cx: &'a Cx,
login: &'a str,
password: &'a str,
) -> BoxFuture<'a, topcoat::Result<Option<Arc<dyn PanelUser>>>>;
fn find_by_id<'a>(
&'a self,
cx: &'a Cx,
id: &'a str,
) -> BoxFuture<'a, topcoat::Result<Option<Arc<dyn PanelUser>>>>;
fn confirm_password<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
password: &'a str,
) -> BoxFuture<'a, topcoat::Result<bool>>;
fn store_locale<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
locale: &'a topcoat::l10n::Locale,
) -> BoxFuture<'a, topcoat::Result<()>>;
fn user_type(&self) -> TypeId;
fn user_type_name(&self) -> &'static str;
}
impl<A: Authenticator> DynAuthenticator for A {
fn verify<'a>(
&'a self,
cx: &'a Cx,
login: &'a str,
password: &'a str,
) -> BoxFuture<'a, topcoat::Result<Option<Arc<dyn PanelUser>>>> {
Box::pin(async move {
let user = Authenticator::verify(self, cx, login, password).await?;
Ok(user.map(|user| Arc::new(user) as Arc<dyn PanelUser>))
})
}
fn find_by_id<'a>(
&'a self,
cx: &'a Cx,
id: &'a str,
) -> BoxFuture<'a, topcoat::Result<Option<Arc<dyn PanelUser>>>> {
Box::pin(async move {
let user = Authenticator::find_by_id(self, cx, id).await?;
Ok(user.map(|user| Arc::new(user) as Arc<dyn PanelUser>))
})
}
fn confirm_password<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
password: &'a str,
) -> BoxFuture<'a, topcoat::Result<bool>> {
Box::pin(async move {
let user: &dyn Any = user;
match user.downcast_ref::<A::User>() {
Some(user) => Authenticator::confirm_password(self, cx, user, password).await,
None => Ok(false),
}
})
}
fn store_locale<'a>(
&'a self,
cx: &'a Cx,
user: &'a dyn PanelUser,
locale: &'a topcoat::l10n::Locale,
) -> BoxFuture<'a, topcoat::Result<()>> {
Box::pin(async move {
let user: &dyn Any = user;
match user.downcast_ref::<A::User>() {
Some(user) => Authenticator::store_locale(self, cx, user, locale).await,
None => Ok(()),
}
})
}
fn user_type(&self) -> TypeId {
TypeId::of::<A::User>()
}
fn user_type_name(&self) -> &'static str {
std::any::type_name::<A::User>()
}
}
#[derive(Clone)]
pub(crate) struct SignedIn {
pub(crate) user: Arc<dyn PanelUser>,
pub(crate) realm: Arc<Realm>,
pub(crate) tenant: Option<TenantId>,
pub(crate) impersonator: Option<Arc<dyn PanelUser>>,
}
impl SignedIn {
pub(crate) fn may_enter(&self) -> bool {
self.user.can_access_panel(&self.realm.prefix)
}
pub(crate) fn sign_into(self, cx: &Cx) -> Cx {
if crate::i18n::chose_locale(cx) {
return cx.with(self);
}
crate::i18n::addressed_to(cx, self.user.locale().as_ref()).with(self)
}
}
pub(crate) fn resolved(cx: &Cx) -> Option<&SignedIn> {
let signed = try_context::<SignedIn>(cx)?;
match current_realm(cx) {
Some(realm) if !Arc::ptr_eq(&signed.realm, realm) => None,
_ => Some(signed),
}
}
pub(crate) fn signed(cx: &Cx) -> Option<&SignedIn> {
resolved(cx).filter(|signed| signed.may_enter())
}
pub fn membership(cx: &Cx) -> Option<&Membership> {
let tenant = crate::tenant_id(cx)?;
signed(cx)?
.user
.tenants()
.iter()
.find(|m| m.tenant == tenant)
}
pub(crate) fn session_tenant(cx: &Cx) -> Option<TenantId> {
let claimed = try_context::<http::request::Parts>(cx)
.and_then(|parts| parts.extensions.get::<crate::tenancy::ClaimedTenant>())
.map(|claimed| claimed.0);
let Some(signed) = signed(cx) else {
return claimed;
};
if !signed.realm.serves_tenants {
return None;
}
let tenants = signed.user.tenants();
let member = |tenant: TenantId| tenants.iter().find(|m| m.tenant == tenant);
claimed
.and_then(member)
.or_else(|| signed.tenant.and_then(member))
.or_else(|| signed.user.default_tenant().and_then(member))
.or_else(|| tenants.first())
.map(|membership| membership.tenant)
}
pub(crate) fn session_time_zone(cx: &Cx) -> Option<jiff::tz::TimeZone> {
signed(cx)
.and_then(|signed| signed.user.time_zone())
.or_else(|| current_realm(cx).map(|realm| realm.time_zone.clone()))
}
pub(crate) fn session_catalog(cx: &Cx) -> Option<&crate::i18n::Catalog> {
current_realm(cx).map(|realm| &*realm.catalog)
}
pub fn user<U: PanelUser>(cx: &Cx) -> Option<&U> {
let found = signed(cx).and_then(|signed| {
let user: &dyn Any = &*signed.user;
user.downcast_ref()
});
if found.is_none() {
report_unloaded::<U>(cx);
}
found
}
fn report_unloaded<U: PanelUser>(cx: &Cx) {
static REPORTED: std::sync::Mutex<Vec<TypeId>> = std::sync::Mutex::new(Vec::new());
let Some(message) = unloaded_type::<U>(cx) else {
return;
};
let mut reported = REPORTED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !reported.contains(&TypeId::of::<U>()) {
reported.push(TypeId::of::<U>());
tracing::error!("{message}");
}
}
fn unloaded_type<U: PanelUser>(cx: &Cx) -> Option<String> {
let mut mounted = realms(cx);
if mounted.is_empty() {
mounted.extend(current_realm(cx).cloned());
}
let loaded: Vec<&dyn DynAuthenticator> = mounted
.iter()
.filter_map(|realm| realm.auth.authenticator())
.collect();
if loaded.is_empty()
|| loaded
.iter()
.any(|auth| auth.user_type() == TypeId::of::<U>())
{
return None;
}
let names: std::collections::BTreeSet<_> =
loaded.iter().map(|auth| auth.user_type_name()).collect();
Some(format!(
"auth::user::<{}> can never find a user: the mounted panels sign in {}",
std::any::type_name::<U>(),
names.into_iter().collect::<Vec<_>>().join(", "),
))
}
pub fn require_user<U: PanelUser>(cx: &Cx) -> topcoat::Result<&U> {
match signed(cx) {
Some(_) => user(cx).ok_or_else(|| forbidden().into()),
None => Err(unauthenticated_error(cx)),
}
}
pub fn signed_in(cx: &Cx) -> bool {
signed(cx).is_some()
}
pub fn enforced(cx: &Cx) -> bool {
match current_realm(cx) {
Some(realm) => realm.gates(),
None => realms(cx).iter().any(|realm| realm.gates()),
}
}
pub fn guard(cx: &Cx) -> topcoat::Result<()> {
if enforced(cx) && !signed_in(cx) {
return Err(unauthenticated_error(cx));
}
Ok(())
}
fn unauthenticated_error(cx: &Cx) -> topcoat::Error {
if page_load(cx) {
redirect(login::login_url_with_next(cx)).into()
} else {
unauthorized().into()
}
}
pub(crate) fn page_load(cx: &Cx) -> bool {
let page_method = matches!(*method(cx), http::Method::GET | http::Method::HEAD);
let rerun = !matches!(*original_method(cx), http::Method::GET | http::Method::HEAD)
&& original_headers(cx).get(&topcoat::runtime::RUNTIME_HEADER) == Some(&RERUN_MARKER);
page_method
&& !rerun
&& !uri(cx)
.path()
.starts_with(crate::topcoat_compat::RUNTIME_PREFIX)
}
fn panel_root(cx: &Cx) -> String {
panel_prefix(cx)
}
fn infrastructure_failure(error: impl Into<topcoat::Error>) -> topcoat::Error {
crate::error::driver_failure(error, UNAVAILABLE_ERROR)
}
const UNAVAILABLE_ERROR: &str = "Sign-in is unavailable right now. Try again shortly.";
static RERUN_MARKER: http::HeaderValue = http::HeaderValue::from_static("true");
#[cfg(test)]
mod tests;