pub mod api_key;
mod check_middleware;
#[cfg(feature = "local_auth")]
pub mod endpoints;
mod extractors;
pub mod lookup;
pub mod middleware;
#[cfg(feature = "local_auth")]
pub mod oauth;
#[cfg(feature = "local_auth")]
pub mod password;
#[cfg(feature = "local_auth")]
pub mod passwordless_email_login;
mod sessions;
use std::borrow::Cow;
use async_trait::async_trait;
use axum::{http::StatusCode, response::IntoResponse};
pub use check_middleware::*;
use clap::ValueEnum;
use error_stack::Report;
pub use extractors::*;
use http::request::Parts;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use sessions::*;
use thiserror::Error;
use uuid::Uuid;
pub use self::lookup::FallbackAnonymousUser;
use crate::errors::{ErrorKind, ForceObfuscate, HttpError};
mod object_ids {
use crate::make_object_id;
make_object_id!(UserId, usr);
make_object_id!(OrganizationId, org);
make_object_id!(RoleId, rol);
}
pub use object_ids::*;
#[derive(Clone, Debug, Error)]
pub enum AuthError {
#[error("Not authenticated")]
Unauthenticated,
#[error("User not found")]
UserNotFound,
#[error("Incorrect password")]
IncorrectPassword,
#[error("Invalid API Key")]
InvalidApiKey,
#[error("User is not verified")]
NotVerified,
#[error("User or org is disabled")]
Disabled,
#[error("Database error")]
Db,
#[error("Error hashing password")]
PasswordHasherError(String),
#[error("API key format does not match")]
ApiKeyFormat,
#[error("Missing permission {0}")]
MissingPermission(Cow<'static, str>),
#[error("Auth error: {0}")]
FailedPredicate(Cow<'static, str>),
#[error("Session backend error")]
SessionBackend,
#[error("Email send failure")]
EmailSendFailure,
#[error("Missing or expired token")]
InvalidToken,
#[error("Passwords do not match")]
PasswordConfirmMismatch,
}
impl AuthError {
pub fn is_unauthenticated(&self) -> bool {
matches!(
self,
Self::Unauthenticated
| Self::UserNotFound
| Self::IncorrectPassword
| Self::InvalidToken
)
}
}
impl HttpError for AuthError {
type Detail = ();
fn status_code(&self) -> StatusCode {
match self {
Self::InvalidApiKey
| Self::InvalidToken
| Self::Unauthenticated
| Self::UserNotFound
| Self::IncorrectPassword => StatusCode::UNAUTHORIZED,
Self::NotVerified
| Self::Disabled
| Self::MissingPermission(_)
| Self::FailedPredicate(_) => StatusCode::FORBIDDEN,
Self::ApiKeyFormat | Self::PasswordConfirmMismatch => StatusCode::BAD_REQUEST,
Self::Db
| Self::EmailSendFailure
| Self::PasswordHasherError(_)
| Self::SessionBackend => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_detail(&self) -> Self::Detail {
()
}
fn obfuscate(&self) -> Option<ForceObfuscate> {
self.is_unauthenticated()
.then(ForceObfuscate::unauthenticated)
}
fn error_kind(&self) -> &'static str {
match self {
Self::InvalidApiKey => ErrorKind::InvalidApiKey,
Self::InvalidToken => ErrorKind::InvalidToken,
Self::Unauthenticated => ErrorKind::Unauthenticated,
Self::UserNotFound => ErrorKind::UserNotFound,
Self::IncorrectPassword => ErrorKind::IncorrectPassword,
Self::NotVerified => ErrorKind::NotVerified,
Self::Disabled => ErrorKind::Disabled,
Self::ApiKeyFormat => ErrorKind::ApiKeyFormat,
Self::PasswordConfirmMismatch => ErrorKind::PasswordConfirmMismatch,
Self::MissingPermission(_) => ErrorKind::MissingPermission,
Self::FailedPredicate(_) => ErrorKind::FailedPredicate,
Self::Db => ErrorKind::Database,
Self::EmailSendFailure => ErrorKind::EmailSendFailure,
Self::PasswordHasherError(_) => ErrorKind::PasswordHasherError,
Self::SessionBackend => ErrorKind::SessionBackend,
}
.as_str()
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> axum::response::Response {
self.to_response()
}
}
pub enum UserFromRequestPartsValue<T: AuthInfo> {
Found(T),
NotFound,
NotImplemented,
}
impl<T: AuthInfo> UserFromRequestPartsValue<T> {
pub fn into_option(self) -> Option<T> {
match self {
Self::Found(t) => Some(t),
Self::NotFound | Self::NotImplemented => None,
}
}
}
#[async_trait]
pub trait AuthQueries: Send + Sync {
type AuthInfo: AuthInfo;
async fn get_user_by_api_key(
&self,
api_key: Uuid,
key_hash: Vec<u8>,
) -> Result<Option<Self::AuthInfo>, Report<AuthError>>;
async fn get_user_by_session_id(
&self,
session_key: &SessionKey,
) -> Result<Option<Self::AuthInfo>, Report<AuthError>>;
async fn anonymous_user(
&self,
user: UserId,
) -> Result<Option<Self::AuthInfo>, Report<AuthError>> {
Ok(None)
}
async fn get_user_from_request_parts(
&self,
parts: &Parts,
) -> Result<UserFromRequestPartsValue<Self::AuthInfo>, Report<AuthError>> {
Ok(UserFromRequestPartsValue::NotImplemented)
}
}
pub trait AuthInfo: 'static + Send + Sync {
fn check_valid(&self) -> Result<(), AuthError>;
fn is_anonymous(&self) -> bool;
fn has_permission(&self, permission: &str) -> bool;
fn require_permission(&self, permission: &'static str) -> Result<(), AuthError> {
if self.has_permission(permission) {
Ok(())
} else {
Err(AuthError::MissingPermission(permission.into()))
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, sqlx::Type)]
#[sqlx(rename_all = "snake_case", type_name = "text")]
#[serde(rename_all = "snake_case")]
pub enum ObjectPermission {
Read,
Write,
Owner,
}
impl ObjectPermission {
pub fn from_str_infallible(perm: &str) -> Option<ObjectPermission> {
match perm {
"owner" => Some(ObjectPermission::Owner),
"read" => Some(ObjectPermission::Read),
"write" => Some(ObjectPermission::Write),
_ => None,
}
}
pub fn can_write(&self) -> bool {
matches!(self, ObjectPermission::Write | ObjectPermission::Owner)
}
pub fn must_be_writable(&self, permission_name: &'static str) -> Result<(), AuthError> {
if self.can_write() {
Ok(())
} else {
Err(AuthError::MissingPermission(Cow::Borrowed(permission_name)))
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct EmailAndPassword {
#[validate(email)]
pub email: String,
#[validate(length(min = 1))]
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResult {
pub message: Cow<'static, str>,
pub redirect_to: Option<String>,
}
#[derive(Serialize, Deserialize, Default, Clone, Copy, Debug, ValueEnum)]
pub enum CorsSetting {
#[default]
None,
AllowHostList,
AllowAll,
}