style: apply rustfmt after clippy let-chain fixes
This commit is contained in:
1 parent
b25a015898
commit
312c78dbbb
16 files changed
+211
-173
No files matched your search
+12
-9
@@ -69,9 +69,10 @@ pub async fn login(
|
||||
|
||||
// Rate limit check (per IP)
|
||||
if let Some(ip_str) = &ctx.ip_address
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() {
|
||||
state.rate_limiter.check(ip_addr)?;
|
||||
}
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
|
||||
{
|
||||
state.rate_limiter.check(ip_addr)?;
|
||||
}
|
||||
|
||||
// Look up user — always run comparable work on failure paths (timing).
|
||||
let user_opt = state
|
||||
@@ -103,9 +104,10 @@ pub async fn login(
|
||||
if !is_authed {
|
||||
record_login_failure(&state, body.username.trim(), ip, ctx.user_agent.as_deref()).await;
|
||||
if let Some(ip_str) = &ctx.ip_address
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() {
|
||||
state.rate_limiter.record_failure(ip_addr);
|
||||
}
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
|
||||
{
|
||||
state.rate_limiter.record_failure(ip_addr);
|
||||
}
|
||||
// Non-enumerating error for both unknown user and bad password.
|
||||
return Err(AppError::InvalidCredentials);
|
||||
}
|
||||
@@ -117,9 +119,10 @@ pub async fn login(
|
||||
|
||||
// Clear rate limit on success
|
||||
if let Some(ip_str) = &ctx.ip_address
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() {
|
||||
state.rate_limiter.record_success(ip_addr);
|
||||
}
|
||||
&& let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
|
||||
{
|
||||
state.rate_limiter.record_success(ip_addr);
|
||||
}
|
||||
|
||||
// Session fixation mitigation: revoke prior sessions + refresh tokens.
|
||||
let _ = state
|
||||
|
||||
+6
-5
@@ -104,11 +104,12 @@ pub async fn terminate_session(
|
||||
) -> Result<Json<Value>> {
|
||||
// If the user is trying to terminate the current session, disallow it
|
||||
if let Some(current_id) = auth.session_id.as_deref()
|
||||
&& id == current_id {
|
||||
return Err(AppError::InvalidInput(
|
||||
"Cannot terminate current session".into(),
|
||||
));
|
||||
}
|
||||
&& id == current_id
|
||||
{
|
||||
return Err(AppError::InvalidInput(
|
||||
"Cannot terminate current session".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Admins can terminate any session, users can only terminate their own
|
||||
let is_admin = state
|
||||
|
||||
+8
-6
@@ -54,9 +54,10 @@ pub async fn create_tenant(
|
||||
require(&state.provider, &auth.user.id, "roles:manage").await?;
|
||||
|
||||
if let Some(ref s) = body.slug
|
||||
&& !s.trim().is_empty() {
|
||||
crate::identity::slug::validate_slug(s)?;
|
||||
}
|
||||
&& !s.trim().is_empty()
|
||||
{
|
||||
crate::identity::slug::validate_slug(s)?;
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let tenant = state
|
||||
@@ -124,9 +125,10 @@ pub async fn update_tenant(
|
||||
require(&state.provider, &auth.user.id, "roles:manage").await?;
|
||||
|
||||
if let Some(ref s) = body.slug
|
||||
&& !s.trim().is_empty() {
|
||||
crate::identity::slug::validate_slug(s)?;
|
||||
}
|
||||
&& !s.trim().is_empty()
|
||||
{
|
||||
crate::identity::slug::validate_slug(s)?;
|
||||
}
|
||||
|
||||
state
|
||||
.provider
|
||||
|
||||
+7
-6
@@ -27,14 +27,15 @@ pub fn ui_dist_dir() -> PathBuf {
|
||||
}
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe()
|
||||
&& let Some(dir) = exe.parent() {
|
||||
for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] {
|
||||
let candidate = dir.join(rel);
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
&& let Some(dir) = exe.parent()
|
||||
{
|
||||
for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] {
|
||||
let candidate = dir.join(rel);
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ui/dist")
|
||||
}
|
||||
|
||||
|
||||
+17
-12
@@ -567,9 +567,10 @@ async fn cmd_init(
|
||||
let db_path = std::path::Path::new(&sqlite_path);
|
||||
println!("Creating database directory...");
|
||||
if let Some(parent) = db_path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Create state directory
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
@@ -664,9 +665,11 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re
|
||||
let db_path = std::path::Path::new(&sqlite_path);
|
||||
let mut dirs_ok = true;
|
||||
if let Some(parent) = db_path.parent()
|
||||
&& !parent.as_os_str().is_empty() && std::fs::create_dir_all(parent).is_err() {
|
||||
dirs_ok = false;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
&& std::fs::create_dir_all(parent).is_err()
|
||||
{
|
||||
dirs_ok = false;
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let state_dir = std::path::Path::new(&home).join(".local/state/nx9-auth");
|
||||
if std::fs::create_dir_all(&state_dir).is_err() {
|
||||
@@ -891,9 +894,10 @@ async fn cmd_backup(config: &Config, path: &std::path::Path) -> anyhow::Result<(
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
@@ -956,9 +960,10 @@ async fn cmd_restore(config: &Config, path: &std::path::Path) -> anyhow::Result<
|
||||
let sqlite_path = config.database.sqlite_path();
|
||||
let target_path = std::path::Path::new(&sqlite_path);
|
||||
if let Some(parent) = target_path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::copy(path, target_path).with_context(|| {
|
||||
format!("failed to restore backup to {}", target_path.display())
|
||||
})?;
|
||||
|
||||
+16
-13
@@ -294,12 +294,13 @@ impl Default for ShutdownConfig {
|
||||
|
||||
fn resolve_home_path(path: &str) -> String {
|
||||
if let Some(stripped) = path.strip_prefix("~/")
|
||||
&& let Ok(home) = std::env::var("HOME") {
|
||||
return Path::new(&home)
|
||||
.join(stripped)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
&& let Ok(home) = std::env::var("HOME")
|
||||
{
|
||||
return Path::new(&home)
|
||||
.join(stripped)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
@@ -312,10 +313,11 @@ impl Config {
|
||||
*path = resolve_home_path(path);
|
||||
}
|
||||
if let Some(ref mut url) = self.database.url
|
||||
&& let Some(stripped) = url.strip_prefix("sqlite://") {
|
||||
let clean = resolve_home_path(stripped);
|
||||
*url = format!("sqlite://{clean}");
|
||||
}
|
||||
&& let Some(stripped) = url.strip_prefix("sqlite://")
|
||||
{
|
||||
let clean = resolve_home_path(stripped);
|
||||
*url = format!("sqlite://{clean}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Load and parse config from a TOML file.
|
||||
@@ -371,9 +373,10 @@ impl Config {
|
||||
/// Default user configuration path (~/.config/nx9-auth/config.toml)
|
||||
pub fn default_user_config_path() -> Option<PathBuf> {
|
||||
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
|
||||
&& !xdg.is_empty() {
|
||||
return Some(PathBuf::from(xdg).join("nx9-auth/config.toml"));
|
||||
}
|
||||
&& !xdg.is_empty()
|
||||
{
|
||||
return Some(PathBuf::from(xdg).join("nx9-auth/config.toml"));
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return Some(PathBuf::from(home).join(".config/nx9-auth/config.toml"));
|
||||
}
|
||||
|
||||
+12
-10
@@ -58,11 +58,12 @@ pub async fn init_provider(
|
||||
DatabaseBackend::Sqlite => {
|
||||
let path = config.database.sqlite_path();
|
||||
if let Some(parent) = std::path::Path::new(&path).parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("failed to create database directory: {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("failed to create database directory: {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let max_conn = config.database.max_connections.unwrap_or(16);
|
||||
let min_conn = config.database.min_connections.unwrap_or(1);
|
||||
@@ -177,11 +178,12 @@ pub async fn init_provider(
|
||||
#[cfg(feature = "sqlite")]
|
||||
pub async fn create_pool(path: &str) -> Result<SqlitePool> {
|
||||
if let Some(parent) = std::path::Path::new(path).parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("failed to create database directory: {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
std::fs::create_dir_all(parent).with_context(|| {
|
||||
format!("failed to create database directory: {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
let url = if path.starts_with("sqlite://") {
|
||||
path.to_string()
|
||||
} else {
|
||||
|
||||
@@ -222,39 +222,40 @@ pub async fn update(
|
||||
}
|
||||
|
||||
if let Some(new_enabled) = enabled
|
||||
&& new_enabled != existing.enabled {
|
||||
let action = if new_enabled {
|
||||
"application.member_enabled"
|
||||
} else {
|
||||
"application.member_disabled"
|
||||
};
|
||||
&& new_enabled != existing.enabled
|
||||
{
|
||||
let action = if new_enabled {
|
||||
"application.member_enabled"
|
||||
} else {
|
||||
"application.member_disabled"
|
||||
};
|
||||
|
||||
let metadata = serde_json::json!({
|
||||
"application_id": application_id,
|
||||
"user_id": user_id,
|
||||
"role": existing.role,
|
||||
"enabled": new_enabled,
|
||||
})
|
||||
.to_string();
|
||||
let metadata = serde_json::json!({
|
||||
"application_id": application_id,
|
||||
"user_id": user_id,
|
||||
"role": existing.role,
|
||||
"enabled": new_enabled,
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let audit_event = crate::audit::AuditEvent {
|
||||
actor_id: audit_actor_id,
|
||||
target_id: Some(user_id),
|
||||
action,
|
||||
resource_type: "application",
|
||||
resource_id: Some(application_id),
|
||||
severity: crate::db::models::AuditSeverity::Info,
|
||||
ip: audit_ip,
|
||||
ua: audit_ua,
|
||||
metadata: Some(&metadata),
|
||||
};
|
||||
let audit_event = crate::audit::AuditEvent {
|
||||
actor_id: audit_actor_id,
|
||||
target_id: Some(user_id),
|
||||
action,
|
||||
resource_type: "application",
|
||||
resource_id: Some(application_id),
|
||||
severity: crate::db::models::AuditSeverity::Info,
|
||||
ip: audit_ip,
|
||||
ua: audit_ua,
|
||||
metadata: Some(&metadata),
|
||||
};
|
||||
|
||||
provider
|
||||
.application_members()
|
||||
.set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event))
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
}
|
||||
provider
|
||||
.application_members()
|
||||
.set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event))
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
}
|
||||
|
||||
provider
|
||||
.application_members()
|
||||
|
||||
@@ -324,9 +324,10 @@ pub async fn update(
|
||||
.find_by_slug(slug)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
&& (other.entity_id != id || other.entity_type != "application") {
|
||||
return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
|
||||
}
|
||||
&& (other.entity_id != id || other.entity_type != "application")
|
||||
{
|
||||
return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
|
||||
}
|
||||
|
||||
let redirect_json = redirect_uris.map(|v| serde_json::to_string(&v).unwrap_or_default());
|
||||
let scopes_json = scopes.map(|v| serde_json::to_string(&v).unwrap_or_default());
|
||||
|
||||
@@ -191,9 +191,10 @@ pub async fn update_role(
|
||||
.find_by_name(name)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
&& other.id != id {
|
||||
return Err(AppError::Conflict(format!("role '{name}' already exists")));
|
||||
}
|
||||
&& other.id != id
|
||||
{
|
||||
return Err(AppError::Conflict(format!("role '{name}' already exists")));
|
||||
}
|
||||
|
||||
provider
|
||||
.roles()
|
||||
|
||||
+46
-48
@@ -74,58 +74,56 @@ where
|
||||
// cannot rely solely on the HttpOnly cookie.
|
||||
if let Some(auth_header) = parts.headers.get(axum::http::header::AUTHORIZATION)
|
||||
&& let Ok(value) = auth_header.to_str()
|
||||
&& let Some(raw) = value.strip_prefix("Bearer ") {
|
||||
let raw = raw.trim();
|
||||
&& let Some(raw) = value.strip_prefix("Bearer ")
|
||||
{
|
||||
let raw = raw.trim();
|
||||
|
||||
// 2a. Personal access token
|
||||
if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? {
|
||||
let user = app_state
|
||||
.provider
|
||||
.users()
|
||||
.find_by_id(&token.user_id)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
// 2a. Personal access token
|
||||
if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? {
|
||||
let user = app_state
|
||||
.provider
|
||||
.users()
|
||||
.find_by_id(&token.user_id)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
if !user.is_active() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
return Ok(AuthUser {
|
||||
user,
|
||||
method: AuthMethod::Token,
|
||||
session_id: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 2b. Session token (same value as nx9_session cookie)
|
||||
if let Some(session) = sessions::validate_session(
|
||||
&app_state.provider,
|
||||
raw,
|
||||
&app_state.config.security,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let user = app_state
|
||||
.provider
|
||||
.users()
|
||||
.find_by_id(&session.user_id)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
if !user.is_active() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
return Ok(AuthUser {
|
||||
user,
|
||||
method: AuthMethod::Session,
|
||||
session_id: Some(session.id),
|
||||
});
|
||||
}
|
||||
if !user.is_active() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
return Ok(AuthUser {
|
||||
user,
|
||||
method: AuthMethod::Token,
|
||||
session_id: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 2b. Session token (same value as nx9_session cookie)
|
||||
if let Some(session) =
|
||||
sessions::validate_session(&app_state.provider, raw, &app_state.config.security)
|
||||
.await?
|
||||
{
|
||||
let user = app_state
|
||||
.provider
|
||||
.users()
|
||||
.find_by_id(&session.user_id)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
if !user.is_active() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
return Ok(AuthUser {
|
||||
user,
|
||||
method: AuthMethod::Session,
|
||||
session_id: Some(session.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
}
|
||||
@@ -163,11 +163,12 @@ impl Lifecycle for Application {
|
||||
}
|
||||
|
||||
if self.router.is_none()
|
||||
&& let Some(provider) = &self.provider {
|
||||
let app_state = crate::state::AppState::new(provider.clone(), config);
|
||||
let router = crate::api::router::build(app_state);
|
||||
self.router = Some(router);
|
||||
}
|
||||
&& let Some(provider) = &self.provider
|
||||
{
|
||||
let app_state = crate::state::AppState::new(provider.clone(), config);
|
||||
let router = crate::api::router::build(app_state);
|
||||
self.router = Some(router);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -75,10 +75,22 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(),
|
||||
|
||||
// 1. Exact matches for highly common passwords
|
||||
let exact_weak: HashSet<&str> = [
|
||||
"password", "admin123", "qwerty", "12345678", "123456789",
|
||||
"administrator", "nx9-auth", "nx9auth", "password123", "admin",
|
||||
"letmein", "welcome", "password12345"
|
||||
].into_iter().collect();
|
||||
"password",
|
||||
"admin123",
|
||||
"qwerty",
|
||||
"12345678",
|
||||
"123456789",
|
||||
"administrator",
|
||||
"nx9-auth",
|
||||
"nx9auth",
|
||||
"password123",
|
||||
"admin",
|
||||
"letmein",
|
||||
"welcome",
|
||||
"password12345",
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if exact_weak.contains(normalized.as_str()) {
|
||||
return Err(AppError::InvalidInput(
|
||||
@@ -98,11 +110,14 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(),
|
||||
}
|
||||
|
||||
// 3. Reject single repeated characters
|
||||
if password.chars().all(|c| c == password.chars().next().unwrap()) {
|
||||
if password
|
||||
.chars()
|
||||
.all(|c| c == password.chars().next().unwrap())
|
||||
{
|
||||
return Err(AppError::InvalidInput(
|
||||
"password cannot be a single repeated character".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,10 @@ impl RateLimiter {
|
||||
pub fn check(&self, ip: IpAddr) -> Result<(), AppError> {
|
||||
if let Some(s) = self.state.get(&ip)
|
||||
&& let Some(until) = s.locked_until
|
||||
&& Instant::now() < until {
|
||||
return Err(AppError::RateLimited);
|
||||
}
|
||||
&& Instant::now() < until
|
||||
{
|
||||
return Err(AppError::RateLimited);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -86,9 +87,10 @@ impl RateLimiter {
|
||||
|
||||
// Clear the lockout if it has expired
|
||||
if let Some(until) = s.locked_until
|
||||
&& now >= until {
|
||||
s.locked_until = None;
|
||||
}
|
||||
&& now >= until
|
||||
{
|
||||
s.locked_until = None;
|
||||
}
|
||||
|
||||
// Prune old failures outside the window
|
||||
let cutoff = now - self.window;
|
||||
|
||||
@@ -78,14 +78,15 @@ pub async fn validate_session(
|
||||
|
||||
// Check absolute expiry
|
||||
if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&session.expires_at)
|
||||
&& now > expires {
|
||||
provider
|
||||
.sessions()
|
||||
.revoke(&session.id)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
return Ok(None);
|
||||
}
|
||||
&& now > expires
|
||||
{
|
||||
provider
|
||||
.sessions()
|
||||
.revoke(&session.id)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Check idle timeout
|
||||
if let Ok(last_seen) = chrono::DateTime::parse_from_rfc3339(&session.last_seen_at) {
|
||||
|
||||
@@ -132,9 +132,10 @@ pub async fn validate_token(
|
||||
// Check expiry if set
|
||||
if let Some(ref exp) = token.expires_at
|
||||
&& let Ok(expires) = chrono::DateTime::parse_from_rfc3339(exp)
|
||||
&& chrono::Utc::now() > expires {
|
||||
return Ok(None);
|
||||
}
|
||||
&& chrono::Utc::now() > expires
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Touch last_used_at (fire-and-forget)
|
||||
let _ = provider.tokens().update_last_used(&token.id).await;
|
||||
|
||||
Reference in new issue
Block a user