mod gate;
mod login;
mod password;
mod session;
use std::{
any::{Any, TypeId},
future::Future,
pin::Pin,
sync::Arc,
};
use topcoat::{
context::{Cx, try_request_context},
router::{
error::{forbidden, redirect, unauthorized},
request::{method, original_headers, original_method, uri},
},
};
use uuid::Uuid;
pub(crate) use self::{
gate::{PanelGate, RuntimeGate},
login::{
MAX_LOGIN_BYTES, login_page, login_post, logout_post, logout_url, safe_next, tenant_post,
tenant_url,
},
};
pub use self::{
login::{LOGIN_FIELD, NEXT_FIELD, PASSWORD_FIELD, TENANT_FIELD},
password::{AdminUser, PasswordAuth, hash_password, verify_password},
session::{AuthSession, SESSION_LIFETIME, revoke_sessions_for_user},
};
use crate::{
panel::{
panel_prefix,
state::{PanelState, Panels, current, panels},
},
tenancy::Membership,
};
pub trait PanelUser: Any + Send + Sync {
fn user_id(&self) -> String;
fn display_name(&self) -> &str;
fn can_access_panel(&self) -> bool {
true
}
fn tenants(&self) -> &[Membership] {
&[]
}
}
pub trait Authenticator: Send + Sync + 'static {
type User: PanelUser;
fn verify(
&self,
cx: &Cx,
login: &str,
password: &str,
) -> impl Future<Output = topcoat::Result<Option<Self::User>>> + Send;
fn find_by_id(
&self,
cx: &Cx,
id: &str,
) -> impl Future<Output = topcoat::Result<Option<Self::User>>> + Send;
}
type UserFuture<'a> =
Pin<Box<dyn Future<Output = topcoat::Result<Option<Arc<dyn PanelUser>>>> + Send + 'a>>;
pub(crate) trait DynAuthenticator: Send + Sync {
fn verify<'a>(&'a self, cx: &'a Cx, login: &'a str, password: &'a str) -> UserFuture<'a>;
fn find_by_id<'a>(&'a self, cx: &'a Cx, id: &'a str) -> UserFuture<'a>;
fn user_type(&self) -> TypeId;
}
impl<A: Authenticator> DynAuthenticator for A {
fn verify<'a>(&'a self, cx: &'a Cx, login: &'a str, password: &'a str) -> UserFuture<'a> {
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) -> UserFuture<'a> {
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 user_type(&self) -> TypeId {
TypeId::of::<A::User>()
}
}
pub struct Auth(Option<Box<dyn DynAuthenticator>>);
impl Auth {
#[must_use]
pub fn password() -> Self {
Self::custom(PasswordAuth)
}
#[must_use]
pub fn custom(authenticator: impl Authenticator) -> Self {
Self(Some(Box::new(authenticator)))
}
#[must_use]
pub fn disabled() -> Self {
Self(None)
}
#[must_use]
pub fn is_disabled(&self) -> bool {
self.0.is_none()
}
pub(crate) fn authenticator(&self) -> Option<&dyn DynAuthenticator> {
self.0.as_deref()
}
}
impl Default for Auth {
fn default() -> Self {
Self::password()
}
}
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.is_disabled() {
"Auth::disabled"
} else {
"Auth"
})
}
}
#[derive(Clone)]
pub(crate) struct SignedIn {
pub(crate) user: Arc<dyn PanelUser>,
pub(crate) panel: Arc<PanelState>,
pub(crate) tenant: Option<Uuid>,
}
pub(crate) fn resolved(cx: &Cx) -> Option<&SignedIn> {
let signed = try_request_context::<SignedIn>(cx)?;
match current(cx) {
Some(panel) if !Arc::ptr_eq(&signed.panel, panel) => 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 == tenant)
}
pub(crate) fn session_tenant(cx: &Cx) -> Option<Uuid> {
let signed = signed(cx)?;
let tenants = signed.user.tenants();
signed
.tenant
.and_then(|tenant| tenants.iter().find(|m| m.tenant == tenant))
.or_else(|| tenants.first())
.map(|membership| membership.tenant)
}
pub fn user<U: PanelUser>(cx: &Cx) -> Option<&U> {
let user: &dyn Any = &*signed(cx)?.user;
user.downcast_ref()
}
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(cx) {
Some(panel) => panel.gates(),
None => panels(cx).is_some_and(Panels::any_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 {
let path = uri(cx).path();
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);
if path.starts_with(crate::topcoat_compat::RUNTIME_PREFIX) || !page_method || rerun {
unauthorized().into()
} else {
redirect(login::login_url_with_next(cx)).into()
}
}
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");
pub(crate) fn check_models_registered(
db: &toasty::Db,
auth: &Auth,
) -> Result<(), crate::DeclarationErrorKind> {
let Some(authenticator) = auth.authenticator() else {
return Ok(());
};
let registered = |name: &str| {
db.schema()
.app
.models()
.any(|model| model.name().upper_camel_case() == name)
};
let shipped_user = authenticator.user_type() == TypeId::of::<AdminUser>();
let models: Vec<&'static str> = [
(!registered("AuthSession")).then_some("AuthSession"),
(shipped_user && !registered("AdminUser")).then_some("AdminUser"),
]
.into_iter()
.flatten()
.collect();
if models.is_empty() {
Ok(())
} else {
Err(crate::DeclarationErrorKind::MissingAuthModels { models })
}
}
#[cfg(test)]
mod tests;