Registry testing - part 5
This commit is contained in:
@@ -28,7 +28,7 @@ use std::{
|
||||
};
|
||||
use store::{query::acl::AclQuery, rand, write::now};
|
||||
use tinyvec::TinyVec;
|
||||
use trc::AddContext;
|
||||
use trc::{AddContext, StoreEvent};
|
||||
use types::{acl::Acl, collection::Collection};
|
||||
use utils::map::bitmap::{Bitmap, BitmapItem};
|
||||
|
||||
@@ -249,25 +249,40 @@ impl Server {
|
||||
.get_value_or_guard_async(&account_id)
|
||||
.await
|
||||
{
|
||||
Ok(token) => Ok(token),
|
||||
Ok(token) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = account_id,
|
||||
Collection = "accessToken",
|
||||
);
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
Err(guard) => {
|
||||
let account = self
|
||||
.registry()
|
||||
.object::<Account>(account_id.into())
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details("Account not found")
|
||||
.account_id(account_id)
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
let revision = rand::random::<u64>();
|
||||
let revision_account = hash_account(&account);
|
||||
let token: Arc<AccessTokenInner> = self
|
||||
.build_access_token(account, account_id, revision, revision_account)
|
||||
.await?
|
||||
.into();
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = account_id,
|
||||
Collection = "accessToken",
|
||||
);
|
||||
|
||||
let token: Arc<AccessTokenInner> = if let Some(account) =
|
||||
self.registry().object::<Account>(account_id.into()).await?
|
||||
{
|
||||
let revision = rand::random::<u64>();
|
||||
let revision_account = hash_account(&account);
|
||||
self.build_access_token(account, account_id, revision, revision_account)
|
||||
.await?
|
||||
.into()
|
||||
} else if account_id == FALLBACK_ADMIN_ID {
|
||||
AccessTokenInner::new_admin().into()
|
||||
} else {
|
||||
return Err(trc::SecurityEvent::Unauthorized
|
||||
.into_err()
|
||||
.details("Account not found")
|
||||
.account_id(account_id)
|
||||
.caused_by(trc::location!()));
|
||||
};
|
||||
|
||||
let _ = guard.insert(token.clone());
|
||||
Ok(token)
|
||||
}
|
||||
@@ -289,9 +304,21 @@ impl Server {
|
||||
{
|
||||
Ok(token) => {
|
||||
if token.revision_account == revision_account {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = account_id,
|
||||
Collection = "accessToken",
|
||||
);
|
||||
|
||||
Ok(token)
|
||||
} else {
|
||||
// Token is stale, rebuild it
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheStale),
|
||||
Key = account_id,
|
||||
Collection = "accessToken",
|
||||
);
|
||||
|
||||
debug_assert!(
|
||||
false,
|
||||
"Token is stale, invalidation should have been triggered"
|
||||
@@ -309,6 +336,12 @@ impl Server {
|
||||
}
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = account_id,
|
||||
Collection = "accessToken",
|
||||
);
|
||||
|
||||
let revision = rand::random::<u64>();
|
||||
let token: Arc<AccessTokenInner> = self
|
||||
.build_access_token(account, account_id, revision, revision_account)
|
||||
@@ -598,19 +631,7 @@ impl AccessToken {
|
||||
pub fn new_admin() -> AccessToken {
|
||||
AccessToken {
|
||||
scope_idx: 0,
|
||||
inner: Arc::new(AccessTokenInner {
|
||||
account_id: FALLBACK_ADMIN_ID,
|
||||
tenant_id: Default::default(),
|
||||
member_of: Default::default(),
|
||||
access_to: Default::default(),
|
||||
scopes: Box::new([AccessScope::new(Permissions::all(), u32::MAX)]),
|
||||
concurrent_http_requests: Default::default(),
|
||||
concurrent_imap_requests: Default::default(),
|
||||
concurrent_uploads: Default::default(),
|
||||
revision: Default::default(),
|
||||
revision_account: Default::default(),
|
||||
obj_size: Default::default(),
|
||||
}),
|
||||
inner: Arc::new(AccessTokenInner::new_admin()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +687,30 @@ impl AccessTokenInner {
|
||||
as u64;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new_admin() -> Self {
|
||||
AccessTokenInner {
|
||||
account_id: FALLBACK_ADMIN_ID,
|
||||
tenant_id: Default::default(),
|
||||
member_of: Default::default(),
|
||||
access_to: Default::default(),
|
||||
scopes: Box::new([AccessScope::new(Permissions::all(), u32::MAX)]),
|
||||
concurrent_http_requests: Default::default(),
|
||||
concurrent_imap_requests: Default::default(),
|
||||
concurrent_uploads: Default::default(),
|
||||
revision: Default::default(),
|
||||
revision_account: Default::default(),
|
||||
obj_size: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub fn revision_account(&self) -> u64 {
|
||||
self.revision_account
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessScope {
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use directory::{
|
||||
Credentials,
|
||||
core::secret::{verify_mfa_secret_hash, verify_secret_hash},
|
||||
Credentials, Directory,
|
||||
core::secret::{SecretVerificationResult, verify_mfa_secret_hash, verify_secret_hash},
|
||||
};
|
||||
use registry::schema::{
|
||||
enums::Permission,
|
||||
@@ -63,7 +63,11 @@ impl Server {
|
||||
|
||||
async fn route_auth_request(&self, req: &AuthRequest) -> trc::Result<AccessToken> {
|
||||
match &req.credentials {
|
||||
Credentials::Basic { username, secret } => {
|
||||
Credentials::Basic {
|
||||
username,
|
||||
secret,
|
||||
mfa_token,
|
||||
} => {
|
||||
let username = UsernameParts::new(username);
|
||||
|
||||
// Try to authenticate as fallback admin if configured
|
||||
@@ -140,13 +144,8 @@ impl Server {
|
||||
}
|
||||
|
||||
// Obtain external directory, if any
|
||||
let directory = domain
|
||||
.id_directory
|
||||
.and_then(|domain_id| self.core.storage.directories.get(&domain_id))
|
||||
.or_else(|| self.get_default_directory());
|
||||
|
||||
let mut is_alias_login = false;
|
||||
let token = if let Some(directory) = directory {
|
||||
let token = if let Some(directory) = self.get_directory_for_cached_domain(&domain) {
|
||||
let directory_account = directory.authenticate(&req.credentials).await?;
|
||||
|
||||
is_alias_login = directory_account.email != auth_as_address;
|
||||
@@ -160,37 +159,55 @@ impl Server {
|
||||
.await?
|
||||
.and_then(|account| account.into_user())
|
||||
{
|
||||
if let Some(credential) = account.password_credential()
|
||||
&& verify_mfa_secret_hash(
|
||||
credential.otp_auth.as_deref(),
|
||||
credential.secret.as_str(),
|
||||
secret,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if credential
|
||||
.expires_at
|
||||
.as_ref()
|
||||
.is_none_or(|exp| exp.timestamp() > now() as i64)
|
||||
{
|
||||
is_alias_login = account.name != auth_as_address;
|
||||
self.access_token(account_id).await.map(AccessToken::new)
|
||||
} else {
|
||||
Err(trc::AuthEvent::Failed
|
||||
.into_err()
|
||||
.ctx(trc::Key::AccountName, account.name.to_string())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
.ctx(trc::Key::Id, credential.credential_id.id())
|
||||
.ctx(trc::Key::SpanId, req.session_id)
|
||||
.reason("Password credential has expired"))
|
||||
}
|
||||
} else {
|
||||
Err(trc::AuthEvent::Failed
|
||||
let Some(credential) = account.password_credential() else {
|
||||
return Err(trc::AuthEvent::Failed
|
||||
.into_err()
|
||||
.ctx(trc::Key::AccountName, auth_as_address.to_string())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
.ctx(trc::Key::SpanId, req.session_id)
|
||||
.reason("Authentication failed"))
|
||||
.reason("Password credential not found for account"));
|
||||
};
|
||||
|
||||
match verify_mfa_secret_hash(
|
||||
credential.otp_auth.as_deref(),
|
||||
mfa_token.as_deref(),
|
||||
credential.secret.as_str(),
|
||||
secret,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
SecretVerificationResult::Valid => {
|
||||
if credential
|
||||
.expires_at
|
||||
.as_ref()
|
||||
.is_none_or(|exp| exp.timestamp() > now() as i64)
|
||||
{
|
||||
is_alias_login = account.name != auth_as_address;
|
||||
self.access_token(account_id).await.map(AccessToken::new)
|
||||
} else {
|
||||
Err(trc::AuthEvent::Failed
|
||||
.into_err()
|
||||
.ctx(trc::Key::AccountName, account.name.to_string())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
.ctx(trc::Key::Id, credential.credential_id.id())
|
||||
.ctx(trc::Key::SpanId, req.session_id)
|
||||
.reason("Password credential has expired"))
|
||||
}
|
||||
}
|
||||
SecretVerificationResult::Invalid => Err(trc::AuthEvent::Failed
|
||||
.into_err()
|
||||
.ctx(trc::Key::AccountName, auth_as_address.to_string())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
.ctx(trc::Key::SpanId, req.session_id)
|
||||
.reason("Authentication failed")),
|
||||
SecretVerificationResult::MissingMfaToken => {
|
||||
Err(trc::AuthEvent::MfaRequired
|
||||
.into_err()
|
||||
.ctx(trc::Key::AccountName, auth_as_address.to_string())
|
||||
.ctx(trc::Key::AccountId, account_id)
|
||||
.ctx(trc::Key::SpanId, req.session_id)
|
||||
.reason("MFA token required"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(trc::AuthEvent::Error
|
||||
@@ -270,11 +287,7 @@ impl Server {
|
||||
let directory = if let Some(username) = username.as_deref().map(UsernameParts::new)
|
||||
{
|
||||
if let Some(domain_name) = username.auth_as().domain() {
|
||||
self.domain(domain_name)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.and_then(|domain| self.core.storage.directories.get(&domain.id))
|
||||
.or_else(|| self.get_default_directory())
|
||||
self.get_directory_for_domain(domain_name).await?
|
||||
} else {
|
||||
self.get_default_directory()
|
||||
}
|
||||
@@ -434,6 +447,43 @@ impl Server {
|
||||
.await
|
||||
.map(AccessToken::new)
|
||||
}
|
||||
|
||||
pub async fn get_directory_for_domain(
|
||||
&self,
|
||||
domain_name: &str,
|
||||
) -> trc::Result<Option<&Arc<Directory>>> {
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
if self.core.is_enterprise_edition() {
|
||||
return Ok(self
|
||||
.domain(domain_name)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.and_then(|domain| self.core.storage.directories.get(&domain.id))
|
||||
.or_else(|| self.get_default_directory()));
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
|
||||
Ok(self.get_default_directory())
|
||||
}
|
||||
|
||||
pub fn get_directory_for_cached_domain(&self, domain: &DomainCache) -> Option<&Arc<Directory>> {
|
||||
// SPDX-SnippetBegin
|
||||
// SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
// SPDX-License-Identifier: LicenseRef-SEL
|
||||
#[cfg(feature = "enterprise")]
|
||||
if self.core.is_enterprise_edition() {
|
||||
return domain
|
||||
.id_directory
|
||||
.and_then(|domain_id| self.core.storage.directories.get(&domain_id))
|
||||
.or_else(|| self.get_default_directory());
|
||||
}
|
||||
// SPDX-SnippetEnd
|
||||
|
||||
self.get_default_directory()
|
||||
}
|
||||
}
|
||||
|
||||
impl UsernameParts {
|
||||
@@ -517,6 +567,7 @@ impl AuthRequest {
|
||||
Credentials::Basic {
|
||||
username: user.into(),
|
||||
secret: pass.into(),
|
||||
mfa_token: None,
|
||||
},
|
||||
session_id,
|
||||
remote_ip,
|
||||
|
||||
@@ -168,9 +168,9 @@ pub struct AccountTenantIds {
|
||||
}
|
||||
|
||||
pub struct AuthRequest {
|
||||
credentials: Credentials,
|
||||
session_id: u64,
|
||||
remote_ip: IpAddr,
|
||||
pub credentials: Credentials,
|
||||
pub session_id: u64,
|
||||
pub remote_ip: IpAddr,
|
||||
}
|
||||
|
||||
impl CacheItemWeight for AccessTokenInner {
|
||||
|
||||
116
crates/common/src/cache/principals.rs
vendored
116
crates/common/src/cache/principals.rs
vendored
@@ -39,7 +39,7 @@ use store::{
|
||||
registry::{RegistryQuery, bootstrap::Bootstrap},
|
||||
write::{key::KeySerializer, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use trc::{AddContext, StoreEvent};
|
||||
use types::id::Id;
|
||||
|
||||
impl Server {
|
||||
@@ -47,6 +47,12 @@ impl Server {
|
||||
let domain_names = &self.inner.cache.domain_names;
|
||||
|
||||
if let Some(domain_id) = domain_names.get(domain) {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = domain.to_string(),
|
||||
Collection = "domainName",
|
||||
);
|
||||
|
||||
let result = self.domain_by_id(domain_id).await?;
|
||||
if result.is_none() {
|
||||
// Domain no longer exists, remove from name cache
|
||||
@@ -83,9 +89,22 @@ impl Server {
|
||||
(),
|
||||
self.inner.cache.negative_cache_ttl,
|
||||
);
|
||||
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = domain.to_string(),
|
||||
Collection = "domainName",
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = domain.to_string(),
|
||||
Collection = "domainNameNegative",
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -99,8 +118,21 @@ impl Server {
|
||||
.get_value_or_guard_async(&domain_id)
|
||||
.await
|
||||
{
|
||||
Ok(domain) => Ok(Some(domain)),
|
||||
Ok(domain) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = domain_id,
|
||||
Collection = "domainId",
|
||||
);
|
||||
|
||||
Ok(Some(domain))
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = domain_id,
|
||||
Collection = "domainId",
|
||||
);
|
||||
let Some(domain) = self.registry().object::<Domain>(domain_id.into()).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -162,6 +194,13 @@ impl Server {
|
||||
let emails = &self.inner.cache.emails;
|
||||
|
||||
if let Some(email) = emails.get(&EmailAddressRef::new(local_part, domain_id)) {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = local_part.to_string(),
|
||||
Domain = domain_id,
|
||||
Collection = "email",
|
||||
);
|
||||
|
||||
Ok(Some(email))
|
||||
} else {
|
||||
let emails_negative = &self.inner.cache.emails_negative;
|
||||
@@ -169,6 +208,13 @@ impl Server {
|
||||
.get(&EmailAddressRef::new(local_part, domain_id))
|
||||
.is_none()
|
||||
{
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = local_part.to_string(),
|
||||
Domain = domain_id,
|
||||
Collection = "email",
|
||||
);
|
||||
|
||||
if let Some(object) = self
|
||||
.registry()
|
||||
.primary_key(
|
||||
@@ -209,6 +255,12 @@ impl Server {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = local_part.to_string(),
|
||||
Domain = domain_id,
|
||||
Collection = "emailNegative",
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -265,8 +317,22 @@ impl Server {
|
||||
.get_value_or_guard_async(&account_id)
|
||||
.await
|
||||
{
|
||||
Ok(account) => Ok(Some(account)),
|
||||
Ok(account) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = account_id,
|
||||
Collection = "account",
|
||||
);
|
||||
|
||||
Ok(Some(account))
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = account_id,
|
||||
Collection = "account",
|
||||
);
|
||||
|
||||
let Some(account) = self.registry().object::<Account>(account_id.into()).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -566,8 +632,14 @@ impl Server {
|
||||
pub async fn role(&self, id: u32) -> trc::Result<Arc<RoleCache>> {
|
||||
let cache = &self.inner.cache.roles;
|
||||
match cache.get_value_or_guard_async(&id).await {
|
||||
Ok(role) => Ok(role),
|
||||
Ok(role) => {
|
||||
trc::event!(Store(StoreEvent::CacheHit), Key = id, Collection = "role");
|
||||
|
||||
Ok(role)
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(Store(StoreEvent::CacheMiss), Key = id, Collection = "role");
|
||||
|
||||
let Some(role) = self.registry().object::<Role>(id.into()).await? else {
|
||||
return Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
@@ -602,8 +674,18 @@ impl Server {
|
||||
pub async fn tenant(&self, id: u32) -> trc::Result<Arc<TenantCache>> {
|
||||
let cache = &self.inner.cache.tenants;
|
||||
match cache.get_value_or_guard_async(&id).await {
|
||||
Ok(tenant) => Ok(tenant),
|
||||
Ok(tenant) => {
|
||||
trc::event!(Store(StoreEvent::CacheHit), Key = id, Collection = "tenant");
|
||||
|
||||
Ok(tenant)
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = id,
|
||||
Collection = "tenant"
|
||||
);
|
||||
|
||||
let Some(tenant) = self.registry().object::<Tenant>(id.into()).await? else {
|
||||
return Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
@@ -663,8 +745,14 @@ impl Server {
|
||||
pub async fn try_list(&self, id: u32) -> trc::Result<Option<Arc<MailingListCache>>> {
|
||||
let cache = &self.inner.cache.lists;
|
||||
match cache.get_value_or_guard_async(&id).await {
|
||||
Ok(list) => Ok(Some(list)),
|
||||
Ok(list) => {
|
||||
trc::event!(Store(StoreEvent::CacheHit), Key = id, Collection = "list");
|
||||
|
||||
Ok(Some(list))
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(Store(StoreEvent::CacheMiss), Key = id, Collection = "list");
|
||||
|
||||
let Some(list) = self.registry().object::<MailingList>(id.into()).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -683,8 +771,22 @@ impl Server {
|
||||
};
|
||||
let cache = &self.inner.cache.dkim_signers;
|
||||
match cache.get_value_or_guard_async(&domain.id).await {
|
||||
Ok(signers) => Ok(Some(signers)),
|
||||
Ok(signers) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheHit),
|
||||
Key = domain.id,
|
||||
Collection = "dkimSigners",
|
||||
);
|
||||
|
||||
Ok(Some(signers))
|
||||
}
|
||||
Err(guard) => {
|
||||
trc::event!(
|
||||
Store(StoreEvent::CacheMiss),
|
||||
Key = domain.id,
|
||||
Collection = "dkimSigners",
|
||||
);
|
||||
|
||||
let ids = self
|
||||
.registry()
|
||||
.query::<Vec<Id>>(
|
||||
|
||||
@@ -132,10 +132,8 @@ impl Server {
|
||||
}
|
||||
|
||||
// Obtain external directory, if configured
|
||||
if let Some(directory) = domain
|
||||
.id_directory
|
||||
.and_then(|id| self.core.storage.directories.get(&id))
|
||||
.or_else(|| self.get_default_directory())
|
||||
if let Some(directory) = self
|
||||
.get_directory_for_cached_domain(&domain)
|
||||
.filter(|directory| directory.can_lookup_recipients())
|
||||
{
|
||||
let address = if local_part.as_ref() == local_part_orig {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
use ahash::AHashSet;
|
||||
use registry::{
|
||||
schema::{
|
||||
enums::{BlockReason, PasswordHashAlgorithm},
|
||||
enums::{BlockReason, PasswordHashAlgorithm, PasswordStrength},
|
||||
prelude::{Object, ObjectType},
|
||||
structs::{self, AllowedIp, BlockedIp, Rate, SystemSettings},
|
||||
},
|
||||
@@ -29,6 +29,7 @@ use store::{
|
||||
use trc::AddContext;
|
||||
use types::id::Id;
|
||||
use utils::glob::{GlobPattern, MatchType};
|
||||
use zxcvbn::Score;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Security {
|
||||
@@ -51,7 +52,7 @@ pub struct Security {
|
||||
pub password_hash_algorithm: PasswordHashAlgorithm,
|
||||
pub password_max_length: u32,
|
||||
pub password_min_length: u32,
|
||||
pub password_min_strength: u8,
|
||||
pub password_min_strength: Score,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -153,7 +154,13 @@ impl Security {
|
||||
password_hash_algorithm: auth.password_hash_algorithm,
|
||||
password_max_length: auth.password_max_length as u32,
|
||||
password_min_length: auth.password_min_length as u32,
|
||||
password_min_strength: auth.password_min_strength as u8,
|
||||
password_min_strength: match auth.password_min_strength {
|
||||
PasswordStrength::Zero => Score::Zero,
|
||||
PasswordStrength::One => Score::One,
|
||||
PasswordStrength::Two => Score::Two,
|
||||
PasswordStrength::Three => Score::Three,
|
||||
PasswordStrength::Four => Score::Four,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,9 +349,9 @@ impl Server {
|
||||
"Password must be at least {} characters long.",
|
||||
self.core.network.security.password_min_length
|
||||
))
|
||||
} else if self.core.network.security.password_min_strength > 0 {
|
||||
} else if self.core.network.security.password_min_strength > Score::Zero {
|
||||
let entropy = zxcvbn::zxcvbn(password, user_inputs);
|
||||
if u8::from(entropy.score()) >= self.core.network.security.password_min_strength {
|
||||
if entropy.score() >= self.core.network.security.password_min_strength {
|
||||
Ok(())
|
||||
} else if let Some(feedback) = entropy.feedback() {
|
||||
Err(format!("Password is too weak. {feedback}"))
|
||||
|
||||
Reference in New Issue
Block a user