mirror of
https://github.com/stoatchat/rust-authifier.git
synced 2026-07-01 22:04:53 -04:00
feat: implement dummy db using in-memory maps
This commit is contained in:
@@ -11,7 +11,7 @@ repository = "https://github.com/authifier/authifier"
|
||||
|
||||
[features]
|
||||
async-std-runtime = [ "async-std", "mongodb/async-std-runtime" ]
|
||||
database-mongodb = [ "mongodb", "bson", "futures" ]
|
||||
database-mongodb = [ "mongodb", "bson" ]
|
||||
rocket_impl = [ "rocket" ]
|
||||
okapi_impl = [ "revolt_rocket_okapi", "revolt_okapi", "schemas" ]
|
||||
schemas = [ "schemars" ]
|
||||
@@ -38,7 +38,7 @@ chrono = "0.4.19"
|
||||
lazy_static = "1.4.0"
|
||||
async-trait = "0.1.56"
|
||||
|
||||
futures = { version = "0.3.21", optional = true }
|
||||
futures = { version = "0.3.21" }
|
||||
|
||||
# Serde
|
||||
serde_json = { version = "1.0.81" }
|
||||
|
||||
@@ -2,21 +2,16 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
pub enum Captcha {
|
||||
/// Don't require captcha verification
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Use hCaptcha to validate sensitive requests
|
||||
#[cfg(feature = "hcaptcha")]
|
||||
HCaptcha { secret: String },
|
||||
}
|
||||
|
||||
impl Default for Captcha {
|
||||
fn default() -> Captcha {
|
||||
Captcha::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Captcha {
|
||||
/// Check that a given token is valid for the in-use Captcha service
|
||||
pub async fn check(&self, token: Option<String>) -> Result<()> {
|
||||
|
||||
@@ -86,10 +86,11 @@ impl Default for EmailExpiryConfig {
|
||||
}
|
||||
|
||||
/// Email verification config
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum EmailVerificationConfig {
|
||||
/// Don't require email verification
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Use email verification
|
||||
Enabled {
|
||||
@@ -99,12 +100,6 @@ pub enum EmailVerificationConfig {
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for EmailVerificationConfig {
|
||||
fn default() -> EmailVerificationConfig {
|
||||
EmailVerificationConfig::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
impl SMTPSettings {
|
||||
/// Create SMTP transport
|
||||
pub fn create_transport(&self) -> SmtpTransport {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
pub enum ResolveIp {
|
||||
/// Use remote IP
|
||||
#[default]
|
||||
Remote,
|
||||
|
||||
/// Use Cloudflare headers
|
||||
Cloudflare,
|
||||
}
|
||||
|
||||
impl Default for ResolveIp {
|
||||
fn default() -> ResolveIp {
|
||||
ResolveIp::Remote
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,22 @@ use std::collections::HashSet;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
pub enum PasswordScanning {
|
||||
/// Disable password scanning
|
||||
#[cfg_attr(not(feature = "pwned100k"), default)]
|
||||
None,
|
||||
/// Use a custom password list
|
||||
Custom { passwords: HashSet<String> },
|
||||
/// Block the top 100k passwords from HIBP
|
||||
#[cfg(feature = "pwned100k")]
|
||||
#[default]
|
||||
Top100k,
|
||||
/// Use the Have I Been Pwned? API
|
||||
#[cfg(feature = "have_i_been_pwned")]
|
||||
HIBP { api_key: String },
|
||||
}
|
||||
|
||||
impl Default for PasswordScanning {
|
||||
fn default() -> PasswordScanning {
|
||||
PasswordScanning::Top100k
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pwned100k")]
|
||||
lazy_static! {
|
||||
|
||||
@@ -2,9 +2,10 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||
pub enum Shield {
|
||||
/// Disable Authifier Shield
|
||||
#[default]
|
||||
Disabled,
|
||||
|
||||
/// Use Authifier Shield to block malicious actors
|
||||
@@ -17,12 +18,6 @@ pub enum Shield {
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Shield {
|
||||
fn default() -> Shield {
|
||||
Shield::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct ShieldValidationInput {
|
||||
/// Remote user IP
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
use crate::{
|
||||
models::{Account, Invite, MFATicket, Session},
|
||||
Result, Success,
|
||||
models::{Account, Invite, MFATicket, Session, EmailVerification, DeletionInfo},
|
||||
Result, Success, Error
|
||||
};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{definition::AbstractDatabase, Migration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DummyDb;
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DummyDb {
|
||||
pub accounts: Arc<Mutex<HashMap<String, Account>>>,
|
||||
pub invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
pub sessions: Arc<Mutex<HashMap<String, Session>>>,
|
||||
pub tickets: Arc<Mutex<HashMap<String, MFATicket>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractDatabase for DummyDb {
|
||||
/// Run a database migration
|
||||
async fn run_migration(&self, migration: Migration) -> Success {
|
||||
todo!("{migration:?}")
|
||||
println!("skip migration {:?}", migration);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find account by id
|
||||
async fn find_account(&self, id: &str) -> Result<Account> {
|
||||
todo!("{id}")
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts.get(id).cloned().ok_or(Error::UnknownUser)
|
||||
}
|
||||
|
||||
/// Find account by normalised email
|
||||
@@ -25,86 +36,161 @@ impl AbstractDatabase for DummyDb {
|
||||
&self,
|
||||
normalised_email: &str,
|
||||
) -> Result<Option<Account>> {
|
||||
todo!("{normalised_email}")
|
||||
let accounts = self.accounts.lock().await;
|
||||
Ok(accounts.values()
|
||||
.find(|account| account.email_normalised == normalised_email)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
/// Find account with active pending email verification
|
||||
async fn find_account_with_email_verification(&self, token: &str) -> Result<Account> {
|
||||
todo!("{token}")
|
||||
async fn find_account_with_email_verification(&self, token_to_match: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts.values()
|
||||
.find(|account| match &account.verification {
|
||||
EmailVerification::Pending { token, .. } | EmailVerification::Moving { token, .. } => token == token_to_match,
|
||||
_ => false
|
||||
})
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidToken)
|
||||
}
|
||||
|
||||
/// Find account with active password reset
|
||||
async fn find_account_with_password_reset(&self, token: &str) -> Result<Account> {
|
||||
todo!("{token}")
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts.values()
|
||||
.find(|account| if let Some(reset) = &account.password_reset {
|
||||
reset.token == token
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidToken)
|
||||
}
|
||||
|
||||
/// Find account with active deletion token
|
||||
async fn find_account_with_deletion_token(&self, token: &str) -> Result<Account> {
|
||||
todo!("{token}")
|
||||
async fn find_account_with_deletion_token(&self, token_to_match: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts.values()
|
||||
.find(|account| if let Some(DeletionInfo::WaitingForVerification { token, .. }) = &account.deletion {
|
||||
token == token_to_match
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.cloned()
|
||||
.ok_or(Error::InvalidToken)
|
||||
}
|
||||
|
||||
/// Find invite by id
|
||||
async fn find_invite(&self, id: &str) -> Result<Invite> {
|
||||
todo!("{id}")
|
||||
let invites = self.invites.lock().await;
|
||||
invites.get(id).cloned().ok_or(Error::InvalidInvite)
|
||||
}
|
||||
|
||||
/// Find session by id
|
||||
async fn find_session(&self, id: &str) -> Result<Session> {
|
||||
todo!("{id}")
|
||||
let sessions = self.sessions.lock().await;
|
||||
sessions.get(id).cloned().ok_or(Error::UnknownUser)
|
||||
}
|
||||
|
||||
/// Find sessions by user id
|
||||
async fn find_sessions(&self, user_id: &str) -> Result<Vec<Session>> {
|
||||
todo!("{user_id}")
|
||||
let sessions = self.sessions.lock().await;
|
||||
Ok(sessions
|
||||
.values()
|
||||
.filter(|session| session.user_id == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Find sessions by user ids
|
||||
async fn find_sessions_with_subscription(&self, user_ids: &[String]) -> Result<Vec<Session>> {
|
||||
todo!("{user_ids:?}")
|
||||
let sessions = self.sessions.lock().await;
|
||||
Ok(sessions
|
||||
.values()
|
||||
.filter(|session| session.subscription.is_some() && user_ids.contains(&session.id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Find session by token
|
||||
async fn find_session_by_token(&self, token: &str) -> Result<Option<Session>> {
|
||||
todo!("{token}")
|
||||
let sessions = self.sessions.lock().await;
|
||||
Ok(sessions.values()
|
||||
.find(|session| session.token == token)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
/// Find ticket by token
|
||||
async fn find_ticket_by_token(&self, token: &str) -> Result<Option<MFATicket>> {
|
||||
todo!("{token}")
|
||||
let tickets = self.tickets.lock().await;
|
||||
Ok(tickets.values()
|
||||
.find(|ticket| ticket.token == token)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
// Save account
|
||||
async fn save_account(&self, account: &Account) -> Success {
|
||||
todo!("{account:?}")
|
||||
let mut accounts = self.accounts.lock().await;
|
||||
accounts.insert(account.id.to_string(), account.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save session
|
||||
async fn save_session(&self, session: &Session) -> Success {
|
||||
todo!("{session:?}")
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
sessions.insert(session.id.to_string(), session.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save invite
|
||||
async fn save_invite(&self, invite: &Invite) -> Success {
|
||||
todo!("{invite:?}")
|
||||
let mut invites = self.invites.lock().await;
|
||||
invites.insert(invite.id.to_string(), invite.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save ticket
|
||||
async fn save_ticket(&self, ticket: &MFATicket) -> Success {
|
||||
todo!("{ticket:?}")
|
||||
let mut tickets = self.tickets.lock().await;
|
||||
tickets.insert(ticket.id.to_string(), ticket.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete session
|
||||
async fn delete_session(&self, id: &str) -> Success {
|
||||
todo!("{id}")
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if sessions.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidSession)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete session
|
||||
async fn delete_all_sessions(&self, user_id: &str, ignore: Option<String>) -> Success {
|
||||
todo!("{user_id} {ignore:?}")
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
sessions.retain(|_, session|
|
||||
if session.user_id == user_id {
|
||||
if let Some(ignore) = &ignore {
|
||||
ignore == &session.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete ticket
|
||||
async fn delete_ticket(&self, id: &str) -> Success {
|
||||
todo!("{id}")
|
||||
let mut tickets = self.tickets.lock().await;
|
||||
if tickets.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use self::{definition::AbstractDatabase, dummy::DummyDb};
|
||||
use self::{definition::AbstractDatabase};
|
||||
|
||||
pub mod definition;
|
||||
|
||||
@@ -14,6 +14,8 @@ pub enum Migration {
|
||||
|
||||
mod dummy;
|
||||
|
||||
pub use dummy::DummyDb;
|
||||
|
||||
#[cfg(feature = "database-mongodb")]
|
||||
mod mongo;
|
||||
|
||||
@@ -29,7 +31,7 @@ pub enum Database {
|
||||
|
||||
impl Default for Database {
|
||||
fn default() -> Self {
|
||||
Self::Dummy(DummyDb)
|
||||
Self::Dummy(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,3 @@ impl Totp {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Totp {
|
||||
fn default() -> Totp {
|
||||
Totp::Disabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ fn is_false(t: &bool) -> bool {
|
||||
}
|
||||
|
||||
/// Invite ticket
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Invite {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/// Time-based one-time password configuration
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum Totp {
|
||||
/// Disabled
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Waiting for user activation
|
||||
Pending { secret: String },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// Multi-factor auth ticket
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct MFATicket {
|
||||
/// Unique Id
|
||||
|
||||
@@ -10,25 +10,35 @@ repository = "https://github.com/authifier/authifier"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
test = [ "reqwest", "regex", "serde_json", "chrono", "base32", "example" ]
|
||||
example = [ "authifier/async-std-runtime", "authifier/database-mongodb", "async-std", "mongodb" ]
|
||||
test = ["reqwest", "regex", "serde_json", "chrono", "base32", "example"]
|
||||
example = [
|
||||
"authifier/async-std-runtime",
|
||||
"authifier/database-mongodb",
|
||||
"async-std",
|
||||
"mongodb",
|
||||
]
|
||||
|
||||
default = [ ]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
# Authifier
|
||||
authifier = { version = "1.0.5", path = "../authifier", features = [ "rocket_impl", "okapi_impl" ] }
|
||||
authifier = { version = "1.0.5", path = "../authifier", features = [
|
||||
"rocket_impl",
|
||||
"okapi_impl",
|
||||
] }
|
||||
|
||||
# Rocket
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
|
||||
rocket_empty = { version = "0.1.1", features = [ "schema" ] }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||
"json",
|
||||
] }
|
||||
rocket_empty = { version = "0.1.1", features = ["schema"] }
|
||||
|
||||
# Serde
|
||||
iso8601-timestamp = { version = "0.1.10" }
|
||||
serde = { version = "1.0.116", features = [ "derive" ] }
|
||||
serde = { version = "1.0.116", features = ["derive"] }
|
||||
|
||||
# Schemas
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
|
||||
revolt_okapi = { version = "0.9.1" }
|
||||
schemars = { version = "0.8.8" }
|
||||
|
||||
@@ -39,4 +49,8 @@ chrono = { version = "0.4.19", optional = true }
|
||||
serde_json = { version = "1.0.81", optional = true }
|
||||
reqwest = { version = "0.11.10", features = ["json"], optional = true }
|
||||
mongodb = { version = "2.2.1", default-features = false, optional = true }
|
||||
async-std = { version = "1.9.0", features = ["tokio02", "tokio1", "attributes"], optional = true }
|
||||
async-std = { version = "1.9.0", features = [
|
||||
"tokio02",
|
||||
"tokio1",
|
||||
"attributes",
|
||||
], optional = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub use authifier::{
|
||||
config::*, database::MongoDb, models::totp::*, models::*, Authifier, AuthifierEvent, Config,
|
||||
config::*, database::{MongoDb, DummyDb}, models::totp::*, models::*, Authifier, AuthifierEvent, Config,
|
||||
Database, Error, Migration, Result,
|
||||
};
|
||||
pub use mongodb::Client;
|
||||
@@ -108,9 +108,12 @@ pub async fn for_test_with_config(
|
||||
test: &str,
|
||||
config: Config,
|
||||
) -> (Authifier, Receiver<AuthifierEvent>) {
|
||||
let client = connect_db().await;
|
||||
|
||||
let database = Database::MongoDb(MongoDb(client.database(&format!("test::{}", test))));
|
||||
let database = if std::env::var("TEST_DB_DUMMY").is_ok() {
|
||||
Database::Dummy(Default::default())
|
||||
} else {
|
||||
let client = connect_db().await;
|
||||
Database::MongoDb(MongoDb(client.database(&format!("test::{}", test))))
|
||||
};
|
||||
|
||||
for migration in [
|
||||
Migration::WipeAll,
|
||||
|
||||
Reference in New Issue
Block a user