feat: implement dummy db using in-memory maps

This commit is contained in:
Paul Makles
2023-09-10 11:11:58 +01:00
committed by Paul Makles
parent a4ca2f6365
commit 0c4f021b6e
14 changed files with 162 additions and 85 deletions
+2 -2
View File
@@ -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 -7
View File
@@ -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 {
+2 -7
View File
@@ -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
}
}
+3 -6
View File
@@ -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 -7
View File
@@ -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
+111 -25
View File
@@ -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)
}
}
}
+4 -2
View File
@@ -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())
}
}
-6
View File
@@ -31,9 +31,3 @@ impl Totp {
}
}
}
impl Default for Totp {
fn default() -> Totp {
Totp::Disabled
}
}
+1 -1
View File
@@ -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")]
+2 -1
View File
@@ -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 -1
View File
@@ -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
+23 -9
View File
@@ -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 }
+7 -4
View File
@@ -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,