mod columns;
pub(crate) mod config;
mod email_change;
mod gate;
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_request_context},
router::{
error::{forbidden, redirect, unauthorized},
request::{method, original_headers, original_method, uri},
},
};
use uuid::Uuid;
use self::realm::realms;
pub use self::{
columns::ColumnPreference,
email_change::{EmailChange, request_email_change},
login::{LOGIN_FIELD, NEXT_FIELD, PASSWORD_FIELD, TENANT_FIELD},
password::{AdminUser, create_admin, hash_password, verify_password},
password_reset::{PasswordResetter, RESET_LIFETIME},
registration::{MIN_PASSWORD_CHARS, new_password_errors},
session::{AuthSession, SESSION_LIFETIME, mint_session, revoke_sessions_for_user},
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::{PanelGate, RuntimeGate, still_signed_in},
login::{logout_url, safe_next, 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, 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};
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 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 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<Uuid>,
}
impl SignedIn {
pub(crate) fn sign_into(self, cx: &Cx) -> Cx {
crate::i18n::addressed_to(cx, self.user.locale().as_ref()).with(self)
}
}
pub(crate) fn resolved(cx: &Cx) -> Option<&SignedIn> {
let signed = try_request_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.user.can_access_panel())
}
pub fn membership(cx: &Cx) -> Option<&Membership> {
let tenant = crate::tenant_id(cx)?;
signed(cx)?
.user
.tenants()
.iter()
.find(|m| m.tenant.get() == tenant)
}
pub(crate) fn session_tenant(cx: &Cx) -> Option<Uuid> {
let signed = signed(cx)?;
let tenants = signed.user.tenants();
let member = |tenant: Uuid| tenants.iter().find(|m| m.tenant.get() == tenant);
signed
.tenant
.and_then(member)
.or_else(|| {
signed
.user
.default_tenant()
.and_then(|tenant| member(tenant.get()))
})
.or_else(|| tenants.first())
.map(|membership| membership.tenant.get())
}
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> {
if cfg!(debug_assertions) {
assert_some_panel_loads::<U>(cx);
}
let user: &dyn Any = &*signed(cx)?.user;
user.downcast_ref()
}
fn assert_some_panel_loads<U: PanelUser>(cx: &Cx) {
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;
}
let names: std::collections::BTreeSet<_> =
loaded.iter().map(|auth| auth.user_type_name()).collect();
panic!(
"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;