pub mod common;
use common::TestDbExt;
use ave_http::auth::database::DatabaseError;
use prometheus_client::{encoding::text::encode, registry::Registry};
use test_log::test;
use std::{sync::Arc, time::Duration};
use ave_http::auth::{
AuthDatabase,
admin_handlers::{
RemovePermissionQuery, assign_role, remove_role,
remove_role_permission, remove_user_permission, set_role_permission,
set_user_permission, update_user,
},
middleware::AuthContextExtractor,
models::{
AuthContext, Permission, SetPermissionRequest, SystemConfigValue,
UpdateUserRequest,
},
};
use axum::{
Extension, Json,
extract::{Path, Query},
http::StatusCode,
};
use std::collections::BTreeSet;
fn metric_value(metrics: &str, name: &str) -> f64 {
metrics
.lines()
.find_map(|line| {
if line.starts_with(name) {
line.split_whitespace().nth(1)?.parse::<f64>().ok()
} else {
None
}
})
.unwrap_or(0.0)
}
#[test]
fn security_tests_route_inputs_exist_in_http_catalog() {
let mut catalog = common::server_main_route_catalog();
catalog.extend(common::server_auth_route_catalog());
catalog.extend(common::server_public_auth_route_catalog());
let expected: BTreeSet<(String, String)> = [
("get".to_string(), "/metrics".to_string()),
("get".to_string(), "/peer-id".to_string()),
("post".to_string(), "/login".to_string()),
]
.into_iter()
.collect();
let missing: Vec<_> = expected.difference(&catalog).cloned().collect();
assert!(
missing.is_empty(),
"Security tests reference routes that do not exist in server.rs: {missing:?}"
);
}
#[test(tokio::test)]
async fn test_password_too_short() {
let (db, _dirs) = common::create_test_db();
let result = db.create_user("testuser", "Short1!", None, None, Some(false));
assert!(matches!(result, Err(DatabaseError::Validation(_))));
}
#[test(tokio::test)]
async fn test_password_too_long() {
let (db, _dirs) = common::create_test_db();
let long_pass = "Aa1!".repeat(33); let result =
db.create_user("testuser", &long_pass, None, None, Some(false));
assert!(matches!(result, Err(DatabaseError::Validation(_))));
}
#[test(tokio::test)]
async fn test_password_missing_uppercase() {
let (db, _dirs) = common::create_test_db();
let result = db.create_user("testuser", "lowercase123!", None, None, None);
assert!(matches!(result, Err(DatabaseError::Validation(_))));
}
#[test(tokio::test)]
async fn test_password_missing_lowercase() {
let (db, _dirs) = common::create_test_db();
let result = db.create_user("testuser", "UPPERCASE123!", None, None, None);
assert!(matches!(result, Err(DatabaseError::Validation(_))));
}
#[test(tokio::test)]
async fn test_password_missing_digit() {
let (db, _dirs) = common::create_test_db();
let result = db.create_user("testuser", "NoDigitsHere!", None, None, None);
assert!(matches!(result, Err(DatabaseError::Validation(_))));
}
#[test(tokio::test)]
async fn test_password_with_unicode() {
let (db, _dirs) = common::create_test_db();
let result = db.create_user("testuser", "Pass123🔐中文", None, None, None);
assert!(result.is_ok());
}
#[test(tokio::test)]
async fn test_sql_injection_in_username() {
let (db, _dirs) = common::create_test_db();
let malicious_username = "admin' OR '1'='1";
let result =
db.create_user(malicious_username, "Password123!", None, None, None);
assert!(
result.is_err(),
"Should reject username with SQL injection attempt"
);
let safe_username = "validuser";
db.create_user(safe_username, "Password123!", None, None, Some(false))
.unwrap();
let verify_result = db.verify_credentials(safe_username, "Password123!");
assert!(verify_result.is_ok());
let user = verify_result.unwrap();
assert_eq!(user.username, safe_username);
}
#[test(tokio::test)]
async fn test_sql_injection_in_role_name() {
let (db, _dirs) = common::create_test_db();
let malicious_role_name = "admin'; DROP TABLE users; --";
let result = db.create_role(malicious_role_name, Some("Malicious role"));
assert!(result.is_ok());
let users = db.list_users(false, 100, 0);
assert!(users.is_ok());
}
#[test(tokio::test)]
async fn test_concurrent_user_creation() {
let (db, _dirs) = common::create_test_db();
let db = std::sync::Arc::new(db);
let mut handles = vec![];
for i in 0..10 {
let db_clone = db.clone();
let handle = std::thread::spawn(move || {
db_clone.create_user(
&format!("user{}", i),
"Password123!",
None,
None,
None,
)
});
handles.push(handle);
}
let results: Vec<_> =
handles.into_iter().map(|h| h.join().unwrap()).collect();
let success_count = results.iter().filter(|r| r.is_ok()).count();
assert_eq!(success_count, 10);
}
#[test(tokio::test)]
async fn test_concurrent_duplicate_user_creation() {
let (db, _dirs) = common::create_test_db();
let db = std::sync::Arc::new(db);
let mut handles = vec![];
for _ in 0..10 {
let db_clone = db.clone();
let handle = std::thread::spawn(move || {
db_clone.create_user(
"duplicate_user",
"Password123!",
None,
None,
None,
)
});
handles.push(handle);
}
let results: Vec<_> =
handles.into_iter().map(|h| h.join().unwrap()).collect();
let success_count = results.iter().filter(|r| r.is_ok()).count();
assert_eq!(success_count, 1);
let duplicate_count = results
.iter()
.filter(|r| matches!(r, Err(DatabaseError::Duplicate(_))))
.count();
assert_eq!(duplicate_count, 9);
}
#[test(tokio::test)]
async fn test_concurrent_api_key_verification() {
let (db, _dirs) = common::create_test_db();
let db = std::sync::Arc::new(db);
let user = db
.create_user("test_user", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, _) = db
.create_api_key(user.id, Some("concurrent"), None, None, false)
.unwrap();
let mut handles = vec![];
for _ in 0..20 {
let db_clone = db.clone();
let key_clone = api_key.clone();
let handle = std::thread::spawn(move || {
db_clone.authenticate_api_key_request(&key_clone, None, "/peer-id")
});
handles.push(handle);
}
let results: Vec<_> =
handles.into_iter().map(|h| h.join().unwrap()).collect();
let success_count = results.iter().filter(|r| r.is_ok()).count();
assert_eq!(success_count, 20);
}
#[test(tokio::test)]
async fn test_auth_metrics_are_exposed_in_prometheus() {
let (app, _dirs) = common::TestApp::build(true, true, None).await;
let api_key = common::login_app(&app, "admin", "AdminPass123!")
.await
.expect("admin login");
let (status, body) = common::make_app_request_raw(
&app,
"/metrics",
"GET",
Some(&api_key),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
assert!(body.contains("auth_db_request_seconds"));
assert!(body.contains("auth_db_transaction_seconds"));
assert!(body.contains("auth_db_blocking_in_flight"));
assert!(body.contains("request_kind=\"login\""));
assert!(body.contains("request_kind=\"api_key_auth\""));
assert!(body.contains("operation=\"authenticate_api_key_request\""));
}
#[test(tokio::test)]
async fn test_auth_blocking_in_flight_metric_tracks_running_tasks() {
let (db, _dirs) = common::create_test_db();
let mut registry = Registry::default();
db.register_prometheus_metrics(&mut registry);
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let db_for_task = db.clone();
let handle = tokio::spawn(async move {
db_for_task
.run_blocking("blocking_metric_probe", move |_| {
let _ = started_tx.send(());
release_rx.recv().expect("release signal");
Ok::<(), DatabaseError>(())
})
.await
});
tokio::time::timeout(Duration::from_secs(2), started_rx)
.await
.expect("blocking task started in time")
.expect("blocking task start signal");
tokio::time::sleep(Duration::from_millis(50)).await;
let mut text = String::new();
encode(&mut text, ®istry).expect("encode metrics");
assert_eq!(metric_value(&text, "auth_db_blocking_in_flight"), 1.0);
release_tx.send(()).expect("release task");
handle.await.expect("join task").expect("task result");
let mut text = String::new();
encode(&mut text, ®istry).expect("encode metrics");
assert_eq!(metric_value(&text, "auth_db_blocking_in_flight"), 0.0);
}
#[test(tokio::test)]
async fn test_unicode_username() {
let (db, _dirs) = common::create_test_db();
let unicode_username = "用户名🔐";
let result =
db.create_user(unicode_username, "Password123!", None, None, None);
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.username, unicode_username);
}
#[test(tokio::test)]
async fn test_whitespace_in_names() {
let (db, _dirs) = common::create_test_db();
let username = "user with spaces";
let result =
db.create_user(username, "Password123!", None, None, Some(false));
assert!(result.is_ok());
let role_name = "role\twith\ttabs";
let role = db.create_role(role_name, None);
assert!(role.is_ok());
}
#[test(tokio::test)]
async fn test_very_long_strings() {
let (db, _dirs) = common::create_test_db();
let long_username = "a".repeat(255);
let result =
db.create_user(&long_username, "Password123!", None, None, None);
assert!(
result.is_err(),
"Should reject username longer than 64 chars"
);
let max_username = "a".repeat(64);
let result =
db.create_user(&max_username, "Password123!", None, None, None);
assert!(result.is_ok(), "Should accept username of exactly 64 chars");
let long_role_name = "b".repeat(255);
let role = db.create_role(&long_role_name, None);
assert!(
role.is_err(),
"Should reject role names longer than 100 chars"
);
let max_role_name = "b".repeat(100);
let role = db.create_role(&max_role_name, None);
assert!(role.is_ok(), "Should accept role name of exactly 100 chars");
}
#[test(tokio::test)]
async fn test_zero_ttl_api_key_never_expires() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, _) = db
.create_api_key(user.id, Some("ttl0"), None, Some(0i64), false)
.unwrap();
assert!(
db.authenticate_api_key_request(&api_key, None, "/peer-id")
.is_ok()
);
std::thread::sleep(std::time::Duration::from_secs(1));
assert!(
db.authenticate_api_key_request(&api_key, None, "/peer-id")
.is_ok()
);
}
#[test(tokio::test)]
async fn test_explicit_zero_ttl_overrides_default() {
use ave_bridge::auth::{
ApiKeyConfig, AuthConfig, LockoutConfig, RateLimitConfig, SessionConfig,
};
use tempfile::TempDir;
let _tmp_dir = TempDir::new().unwrap();
let config = AuthConfig {
durability: false,
enable: true,
database_path: _tmp_dir.path().to_path_buf(),
superadmin: "admin".to_string(),
api_key: ApiKeyConfig {
default_ttl_seconds: 2592000, max_keys_per_user: 10,
prefix: "ave_node_".to_string(),
},
lockout: LockoutConfig::default(),
rate_limit: RateLimitConfig::default(),
session: SessionConfig::default(),
};
let db = AuthDatabase::new(config, "TestPass123!", None)
.expect("Failed to create database");
let user = db
.create_user("ttluser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key_zero, key_info_zero) = db
.create_api_key(user.id, Some("never-expire"), None, Some(0), false)
.unwrap();
assert!(
key_info_zero.expires_at.is_none(),
"Key with explicit TTL=0 should never expire (expires_at should be None)"
);
assert!(
db.authenticate_api_key_request(&api_key_zero, None, "/peer-id")
.is_ok()
);
let (_api_key_default, key_info_default) = db
.create_api_key(user.id, Some("use-default"), None, None, false)
.unwrap();
assert!(
key_info_default.expires_at.is_some(),
"Key without explicit TTL should use default_ttl (expires_at should be Some)"
);
let (_api_key_custom, key_info_custom) = db
.create_api_key(user.id, Some("custom-ttl"), None, Some(3600), false)
.unwrap();
assert!(
key_info_custom.expires_at.is_some(),
"Key with explicit positive TTL should have expiration"
);
println!("✓ Explicit TTL=0 correctly overrides default_ttl:");
println!(" - TTL=0 explicit: never expires (None)");
println!(" - TTL=None: uses default_ttl (Some)");
println!(" - TTL=3600: custom expiration (Some)");
}
#[test(tokio::test)]
async fn test_inactive_user_cannot_login() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
db.update_user(user.id, None, Some(false)).unwrap();
let result = db.verify_credentials("testuser", "Password123!");
assert!(matches!(result, Err(DatabaseError::PermissionDenied(_))));
}
#[test(tokio::test)]
async fn test_inactive_user_api_keys_dont_work() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, _) = db
.create_api_key(user.id, Some("lockout"), None, None, false)
.unwrap();
db.update_user(user.id, None, Some(false)).unwrap();
let result = db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(matches!(result, Err(DatabaseError::PermissionDenied(_))));
}
#[test(tokio::test)]
async fn test_deleted_user_api_keys_dont_work() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, _) = db
.create_api_key(user.id, Some("lockout2"), None, None, false)
.unwrap();
db.delete_user(user.id).unwrap();
let result = db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(result.is_err());
}
#[test(tokio::test)]
async fn test_deleted_role_removes_user_permissions() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let role = db.create_role("editor", None).unwrap();
db.set_role_permission(role.id, "node_subject", "get", true)
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
let perms_before = db.get_user_effective_permissions(user.id).unwrap();
assert!(perms_before.iter().any(|p| p.resource == "node_subject"
&& p.action == "get"
&& p.allowed));
db.delete_role(role.id).unwrap();
let perms_after = db.get_user_effective_permissions(user.id).unwrap();
assert!(!perms_after.iter().any(|p| p.resource == "node_subject"
&& p.action == "get"
&& p.allowed));
}
#[test(tokio::test)]
async fn non_superadmin_cannot_modify_permissions_of_admin_via_role_inheritance()
{
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let admin_role = db
.create_role("admin_guard", Some("Admin guard role"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let actor = db
.create_user(
"admin_actor",
"Password123!",
Some(vec![admin_role.id]),
None,
None,
)
.unwrap();
let target = db
.create_user(
"admin_target",
"Password123!",
Some(vec![admin_role.id]),
None,
None,
)
.unwrap();
let permissions = db.get_effective_permissions(actor.id).unwrap();
let roles = db.get_user_roles(actor.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: actor.id,
username: actor.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = set_user_permission(
AuthContextExtractor(auth_ctx),
Extension(db.clone()),
Path(target.id),
Json(Permission {
resource: "admin_system".to_string(),
action: "all".to_string(),
allowed: true,
is_system: None,
source: None,
role_name: None,
}),
)
.await;
assert!(matches!(result, Err((StatusCode::FORBIDDEN, _))));
}
#[test(tokio::test)]
async fn non_superadmin_cannot_remove_permissions_of_admin_via_role_inheritance()
{
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let admin_role = db
.create_role("admin_guard_remove", Some("Admin guard role"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let actor = db
.create_user(
"admin_actor_remove",
"Password123!",
Some(vec![admin_role.id]),
None,
None,
)
.unwrap();
let target = db
.create_user(
"admin_target_remove",
"Password123!",
Some(vec![admin_role.id]),
None,
None,
)
.unwrap();
db.set_user_permission(
target.id,
"node_subject",
"get",
true,
Some(actor.id),
)
.unwrap();
let permissions = db.get_effective_permissions(actor.id).unwrap();
let roles = db.get_user_roles(actor.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: actor.id,
username: actor.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = remove_user_permission(
AuthContextExtractor(auth_ctx),
Extension(db.clone()),
Path(target.id),
Query(RemovePermissionQuery {
resource: "node_subject".to_string(),
action: "get".to_string(),
}),
)
.await;
assert!(matches!(result, Err((StatusCode::FORBIDDEN, _))));
}
#[test(tokio::test)]
async fn superadmin_flag_grants_all_permissions_even_without_overrides() {
let ctx = AuthContext {
user_id: 1,
username: "root".to_string(),
roles: vec!["superadmin".to_string()], permissions: vec![], api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
};
assert!(ctx.has_permission("any_resource", "any_action"));
}
#[test(tokio::test)]
async fn test_get_nonexistent_user() {
let (db, _dirs) = common::create_test_db();
let result = db.get_user_by_id(99999);
assert!(matches!(result, Err(DatabaseError::NotFound(_))));
}
#[test(tokio::test)]
async fn test_get_nonexistent_role() {
let (db, _dirs) = common::create_test_db();
let result = db.get_role_by_name("nonexistent_role");
assert!(matches!(result, Err(DatabaseError::NotFound(_))));
}
#[test(tokio::test)]
async fn test_assign_nonexistent_role() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let result = db.assign_role_to_user(user.id, 99999, None);
assert!(result.is_err());
}
#[test(tokio::test)]
async fn test_assign_role_to_nonexistent_user() {
let (db, _dirs) = common::create_test_db();
let role = db.create_role("editor", None).unwrap();
let result = db.assign_role_to_user(99999, role.id, None);
assert!(result.is_err());
}
#[test(tokio::test)]
async fn test_revoke_nonexistent_api_key() {
let (db, _dirs) = common::create_test_db();
let result =
db.revoke_api_key("99999999-9999-9999-9999-999999999999", None, None);
assert!(result.is_ok());
}
#[test(tokio::test)]
async fn test_update_nonexistent_user() {
let (db, _dirs) = common::create_test_db();
let result = db.update_user(99999, Some("NewPass123!"), None);
assert!(matches!(result, Err(DatabaseError::NotFound(_))));
}
#[test(tokio::test)]
async fn test_delete_nonexistent_user() {
let (db, _dirs) = common::create_test_db();
let result = db.delete_user(99999);
assert!(result.is_ok());
}
#[test(tokio::test)]
async fn test_delete_nonexistent_role() {
let (db, _dirs) = common::create_test_db();
let result = db.delete_role(99999);
assert!(matches!(result, Err(DatabaseError::NotFound(_))));
}
#[test(tokio::test)]
async fn test_superadmin_bootstrap() {
let (db, _dirs) = common::create_test_db();
let result = db.verify_credentials("admin", "AdminPass123!");
assert!(result.is_ok());
let user = result.unwrap();
let roles = db.get_user_roles(user.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
}
#[test(tokio::test)]
async fn test_superadmin_has_all_permissions() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let _perms = db.get_user_effective_permissions(admin.id).unwrap();
let roles = db.get_user_roles(admin.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
}
#[test(tokio::test)]
async fn test_create_regular_user_without_superadmin() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("regularuser", "UserPass123!", None, None, None)
.unwrap();
let roles = db.get_user_roles(user.id).unwrap();
assert!(!roles.contains(&"superadmin".to_string()));
}
#[test(tokio::test)]
async fn test_revoked_api_key_cannot_be_used() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, key_info) = db
.create_api_key(user.id, Some("rl_main"), None, None, false)
.unwrap();
db.revoke_api_key(&key_info.id, None, Some("Security breach"))
.unwrap();
let result = db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(matches!(result, Err(DatabaseError::PermissionDenied(_))));
}
#[test(tokio::test)]
async fn test_double_revoke_api_key() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (_, key_info) = db
.create_api_key(user.id, Some("rl_expire"), None, None, false)
.unwrap();
db.revoke_api_key(&key_info.id, None, None).unwrap();
let result = db.revoke_api_key(&key_info.id, None, None);
assert!(
result.is_ok() || matches!(result, Err(DatabaseError::NotFound(_)))
);
}
#[test(tokio::test)]
async fn test_many_roles_for_user() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
for i in 0..50 {
let role = db.create_role(&format!("role{}", i), None).unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
}
let roles = db.get_user_roles(user.id).unwrap();
assert_eq!(roles.len(), 50);
}
#[test(tokio::test)]
async fn test_many_permissions_for_role() {
let (db, _dirs) = common::create_test_db();
let role = db.create_role("power_user", None).unwrap();
let resources = vec![
"user",
"admin_system",
"admin_api_key",
"admin_roles",
"admin_users",
"node_system",
"node_subject",
"node_request",
];
let actions = vec!["get", "post", "put", "patch", "delete", "all"];
for resource in &resources {
for action in &actions {
db.set_role_permission(role.id, resource, action, true)
.unwrap();
}
}
let perms = db.get_role_permissions(role.id).unwrap();
assert_eq!(perms.len(), resources.len() * actions.len());
}
#[test(tokio::test)]
async fn test_many_api_keys_for_user() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
for i in 0..10 {
db.create_api_key(
user.id,
Some(&format!("key{}", i)),
None,
None,
false,
)
.unwrap();
}
let keys = db.list_user_api_keys(user.id, false).unwrap();
assert_eq!(keys.len(), 10);
}
#[test(tokio::test)]
async fn test_api_key_public_ids_are_uuids_not_sequential() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let mut public_ids = vec![];
for i in 0..5 {
let (_, key_info) = db
.create_api_key(
user.id,
Some(&format!("key{}", i)),
None,
None,
false,
)
.unwrap();
public_ids.push(key_info.id.clone());
}
for public_id in &public_ids {
assert_eq!(public_id.len(), 36, "UUID should be 36 characters");
assert_eq!(
public_id.chars().filter(|c| *c == '-').count(),
4,
"UUID should have 4 dashes"
);
assert!(
public_id.parse::<i64>().is_err(),
"Public ID should not be a simple integer"
);
}
let unique_ids: std::collections::HashSet<_> = public_ids.iter().collect();
assert_eq!(unique_ids.len(), 5, "All public IDs should be unique");
}
#[test(tokio::test)]
async fn test_pre_auth_rate_limiting_on_login() {
use ave_bridge::auth::{
ApiKeyConfig, AuthConfig, LockoutConfig, RateLimitConfig, SessionConfig,
};
use ave_http::auth::database::AuthDatabase;
let dir = tempfile::tempdir().expect("Can not create temporal directory");
let path = dir.path().to_path_buf();
let config = AuthConfig {
durability: false,
enable: true,
database_path: path,
superadmin: "admin".to_string(),
api_key: ApiKeyConfig {
default_ttl_seconds: 0,
max_keys_per_user: 10,
prefix: "ave_node_".to_string(),
},
lockout: LockoutConfig {
max_attempts: 5,
duration_seconds: 900,
},
rate_limit: RateLimitConfig {
enable: true,
window_seconds: 60,
max_requests: 100, limit_by_key: true,
limit_by_ip: true,
cleanup_interval_seconds: 3600,
sensitive_endpoints: vec![],
},
session: SessionConfig {
audit_enable: true,
audit_retention_days: 90,
audit_max_entries: 1_000_000,
},
};
let db = AuthDatabase::new(config, "AdminPass123!", None).unwrap();
db.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let fake_ip = Some("192.168.1.100");
let mut successful_checks = 0;
let mut rate_limited = false;
for _ in 0..110 {
match db.check_rate_limit(None, fake_ip, Some("/login")) {
Ok(_) => successful_checks += 1,
Err(DatabaseError::RateLimitExceeded(_)) => {
rate_limited = true;
break;
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
assert!(
rate_limited,
"Rate limit should be enforced on login endpoint"
);
assert!(
successful_checks <= 100,
"Should not exceed configured rate limit (got {})",
successful_checks
);
}
#[test(tokio::test)]
async fn test_api_keys_have_public_id_after_migration() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (_, key_info) = db
.create_api_key(user.id, Some("test"), None, None, false)
.unwrap();
assert!(!key_info.id.is_empty(), "id should not be empty");
assert_ne!(key_info.id, "0", "id should not be default value");
}
#[test(tokio::test)]
async fn test_concurrent_api_key_creation_respects_max_limit() {
let (db, _dirs) = common::create_test_db();
let db = std::sync::Arc::new(db);
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let mut handles = vec![];
for i in 0..25 {
let db_clone = db.clone();
let user_id = user.id;
let handle = std::thread::spawn(move || {
db_clone.create_api_key(
user_id,
Some(&format!("concurrent_key_{}", i)),
None,
None,
false,
)
});
handles.push(handle);
}
let results: Vec<_> =
handles.into_iter().map(|h| h.join().unwrap()).collect();
let success_count = results.iter().filter(|r| r.is_ok()).count();
assert!(
success_count <= 20,
"Should not create more than max_keys_per_user limit (got {})",
success_count
);
let keys = db.list_user_api_keys(user.id, false).unwrap();
assert!(
keys.len() <= 20,
"Database should not have more than 20 keys (got {})",
keys.len()
);
}
#[test(tokio::test)]
async fn test_dangerous_characters_in_api_key_names_rejected() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let dangerous_names = vec![
"<script>alert('xss')</script>", "'; DROP TABLE users; --", "key`whoami`", "key$(whoami)", "key|whoami", "key;rm -rf /", "../../../etc/passwd", "key\\..\\secrets", "key\0hidden", "key\ninjected", "key\rinjected", "key<>test", "key&test", "key*test", "key?test", "key%00test", "key{test}", "key[test]", ];
for dangerous_name in dangerous_names {
let result =
db.create_api_key(user.id, Some(dangerous_name), None, None, false);
assert!(
result.is_err(),
"Should reject dangerous name: {}",
dangerous_name
);
if let Err(e) = result {
match e {
DatabaseError::Validation(_) => {
}
_ => panic!("Expected ValidationError, got: {:?}", e),
}
}
}
let valid_names = vec![
"my_api_key",
"production-key",
"test key 2024",
"api.key.1",
"Key_123",
"UPPERCASE_KEY",
];
for valid_name in valid_names {
let result =
db.create_api_key(user.id, Some(valid_name), None, None, false);
assert!(
result.is_ok(),
"Should accept valid name: {} (error: {:?})",
valid_name,
result.err()
);
}
}
#[test(tokio::test)]
async fn test_security_headers_prevent_api_key_leakage() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let (api_key, _key_info) = db
.create_api_key(user.id, Some("test-key"), None, None, false)
.unwrap();
assert!(api_key.starts_with("ave_node_"));
assert!(
api_key.len() > 40,
"API key should be long enough to prevent brute force"
);
}
#[test(tokio::test)]
async fn test_crlf_injection_prevented_in_text_fields() {
let (db, _dirs) = common::create_test_db();
let crlf_usernames = vec![
"user\r\nInjected-Header: malicious",
"user\nlog-injection",
"user\rcarriage-return",
"user\r\n\r\nHTTP/1.1 200 OK",
"admin\r\nSet-Cookie: session=hijacked",
];
for username in crlf_usernames {
let result =
db.create_user(username, "Password123!", None, None, Some(false));
assert!(
result.is_err(),
"Should reject username with CRLF: {:?}",
username
);
if let Err(e) = result {
match e {
DatabaseError::Validation(msg) => {
assert!(msg.contains("CRLF") || msg.contains("control"));
}
_ => panic!("Expected ValidationError for CRLF, got: {:?}", e),
}
}
}
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let crlf_descriptions = vec![
"Description\r\nInjected-Header: malicious",
"Description\nlog-injection",
"Description\rcarriage-return",
"Normal desc\r\n\r\nHTTP/1.1 200 OK",
];
for desc in crlf_descriptions {
let result = db.create_api_key(
user.id,
Some("test-key"),
Some(desc),
None,
false,
);
assert!(
result.is_err(),
"Should reject description with CRLF: {:?}",
desc
);
if let Err(e) = result {
match e {
DatabaseError::Validation(msg) => {
assert!(msg.contains("CRLF") || msg.contains("control"));
}
_ => panic!(
"Expected ValidationError for CRLF in description, got: {:?}",
e
),
}
}
}
let null_byte_tests = vec![("user\0hidden", "Password123!")];
for (username, password) in null_byte_tests {
let result =
db.create_user(username, password, None, None, Some(false));
assert!(result.is_err(), "Should reject null bytes in username");
}
let valid_user = db
.create_user("validuser", "Password123!", None, None, Some(false))
.unwrap();
assert_eq!(valid_user.username, "validuser");
let (_, key_info) = db
.create_api_key(
user.id,
Some("valid-key"),
Some("Valid description with normal text"),
None,
false,
)
.unwrap();
assert_eq!(
key_info.description,
Some("Valid description with normal text".to_string())
);
let long_username = "a".repeat(65);
let result =
db.create_user(&long_username, "Password123!", None, None, Some(false));
assert!(
result.is_err(),
"Should reject username longer than 64 chars"
);
let long_description = "a".repeat(501);
let result = db.create_api_key(
user.id,
Some("test-long-desc"),
Some(&long_description),
None,
false,
);
assert!(
result.is_err(),
"Should reject description longer than 500 chars"
);
}
#[test(tokio::test)]
async fn test_cors_configuration_security() {
use ave_bridge::CorsConfig;
let default_config = CorsConfig::default();
assert!(default_config.enabled, "CORS should be enabled by default");
assert!(
default_config.allow_any_origin,
"Default allows any origin (for development)"
);
assert_eq!(default_config.allowed_origins.len(), 0);
assert!(
!default_config.allow_credentials,
"Should never allow credentials with wildcard origin"
);
let secure_config = CorsConfig {
enabled: true,
allow_any_origin: false,
allowed_origins: vec![
"https://app.example.com".to_string(),
"https://dashboard.example.com".to_string(),
],
allow_credentials: false,
};
assert!(
!secure_config.allow_any_origin,
"Production should not allow any origin"
);
assert_eq!(
secure_config.allowed_origins.len(),
2,
"Should have specific origins"
);
let disabled_config = CorsConfig {
enabled: false,
allow_any_origin: false,
allowed_origins: vec![],
allow_credentials: false,
};
assert!(!disabled_config.enabled, "CORS can be disabled");
let dangerous_config = CorsConfig {
enabled: true,
allow_any_origin: true,
allowed_origins: vec![],
allow_credentials: true, };
assert!(
dangerous_config.allow_any_origin && dangerous_config.allow_credentials,
"This combination is dangerous and should be avoided in production"
);
}
#[test(tokio::test)]
async fn test_user_enumeration_prevented_via_error_messages() {
let (db, _dirs) = common::create_test_db();
let _active_user = db
.create_user("active_user", "Password123!", None, None, Some(false))
.unwrap();
let inactive_user = db
.create_user("inactive_user", "Password123!", None, None, Some(false))
.unwrap();
db.update_user(inactive_user.id, None, Some(false)).unwrap();
let _locked_user = db
.create_user("locked_user", "Password123!", None, None, Some(false))
.unwrap();
for _ in 0..5 {
let _ = db.verify_credentials("locked_user", "WrongPassword!");
}
let err1 = db
.verify_credentials("nonexistent", "Password123!")
.unwrap_err();
let err2 = db
.verify_credentials("inactive_user", "Password123!")
.unwrap_err();
let err3 = db
.verify_credentials("locked_user", "Password123!")
.unwrap_err();
let err4 = db
.verify_credentials("active_user", "WrongPassword!")
.unwrap_err();
match (&err1, &err2, &err3, &err4) {
(
DatabaseError::PermissionDenied(msg1),
DatabaseError::PermissionDenied(msg2),
DatabaseError::PermissionDenied(msg3),
DatabaseError::PermissionDenied(msg4),
) => {
assert_eq!(msg1, "Invalid username or password");
assert_eq!(msg2, "Invalid username or password");
assert_eq!(msg3, "Invalid username or password");
assert_eq!(msg4, "Invalid username or password");
assert_eq!(msg1, msg2);
assert_eq!(msg2, msg3);
assert_eq!(msg3, msg4);
}
_ => panic!("All errors should be PermissionDenied with same message"),
}
}
#[test(tokio::test)]
async fn test_only_one_superadmin_allowed() {
let (db, _dirs) = common::create_test_db();
let count_before = db.count_superadmins().unwrap();
assert_eq!(
count_before, 1,
"Should have exactly 1 superadmin after bootstrap"
);
let roles = db.list_roles().unwrap();
let superadmin_role =
roles.iter().find(|r| r.name == "superadmin").unwrap();
let result = db.create_user(
"second_superadmin",
"SuperPass123!",
Some(vec![superadmin_role.id]), None,
None,
);
assert!(
result.is_err(),
"Should not allow creating second superadmin"
);
let count_after = db.count_superadmins().unwrap();
assert_eq!(count_after, 1, "Should still have exactly 1 superadmin");
}
#[test(tokio::test)]
async fn test_superadmin_cannot_be_deleted() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let roles = db.get_user_roles(admin.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
let result = db.delete_user(admin.id);
assert!(result.is_ok(), "DB layer allows deletion");
}
#[test(tokio::test)]
async fn test_superadmin_cannot_be_deactivated() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let roles = db.get_user_roles(admin.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
assert!(admin.is_active);
let result = db.update_user(admin.id, None, Some(false));
assert!(result.is_ok(), "DB layer allows deactivation");
let updated_admin = db.get_user_by_id(admin.id).unwrap();
assert!(!updated_admin.is_active, "DB layer allowed deactivation");
}
#[test(tokio::test)]
async fn test_non_superadmin_cannot_reset_superadmin_password() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let roles = db.get_user_roles(admin.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
let result = db.admin_reset_password(admin.id, "NewPassword123!");
assert!(result.is_ok(), "DB layer allows password reset");
let result = db.change_password_with_credentials(
"admin",
"NewPassword123!",
"FinalPassword123!",
);
assert!(result.is_ok(), "Password change should work");
let result = db.verify_credentials("admin", "FinalPassword123!");
assert!(
result.is_ok(),
"Final password should work: {:?}",
result.err()
);
}
#[test(tokio::test)]
async fn test_count_superadmins() {
let (db, _dirs) = common::create_test_db();
let count = db.count_superadmins().unwrap();
assert_eq!(count, 1);
db.create_user("regular", "Password123!", None, None, Some(false))
.unwrap();
let count = db.count_superadmins().unwrap();
assert_eq!(count, 1);
let roles = db.list_roles().unwrap();
let superadmin_role =
roles.iter().find(|r| r.name == "superadmin").unwrap();
let result = db.create_user(
"another_super",
"SuperPass123!",
Some(vec![superadmin_role.id]),
None,
None,
);
assert!(result.is_err());
let count = db.count_superadmins().unwrap();
assert_eq!(count, 1);
}
#[test(tokio::test)]
async fn test_assign_role_cannot_create_second_superadmin() {
let (db, _dirs) = common::create_test_db();
let roles = db.list_roles().unwrap();
let superadmin_role =
roles.iter().find(|r| r.name == "superadmin").unwrap();
let user = db
.create_user("testuser", "Password123!", None, None, Some(false))
.unwrap();
let result = db.assign_role_to_user(user.id, superadmin_role.id, None);
if result.is_ok() {
let count = db.count_superadmins().unwrap();
assert!(count >= 1, "Should have at least the bootstrap superadmin");
}
}
#[test(tokio::test)]
async fn test_remove_superadmin_role_from_only_superadmin_db_level() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let roles = db.list_roles().unwrap();
let superadmin_role =
roles.iter().find(|r| r.name == "superadmin").unwrap();
let count = db.count_superadmins().unwrap();
assert_eq!(count, 1);
let result = db.remove_role_from_user(admin.id, superadmin_role.id);
if result.is_ok() {
let user_roles = db.get_user_roles(admin.id).unwrap();
assert!(
!user_roles.contains(&"superadmin".to_string()),
"DB level allows removal - handler must prevent"
);
let count = db.count_superadmins().unwrap();
assert_eq!(
count, 0,
"DB allows removing last superadmin - handler must prevent"
);
}
}
#[test(tokio::test)]
async fn test_update_user_cannot_remove_superadmin_role() {
let (db, _dirs) = common::create_test_db();
let admin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let roles = db.get_user_roles(admin.id).unwrap();
assert!(roles.contains(&"superadmin".to_string()));
let count = db.count_superadmins().unwrap();
assert_eq!(count, 1, "Should have exactly one superadmin before test");
}
#[test(tokio::test)]
async fn test_cannot_rotate_other_users_api_keys() {
let (db, _dirs) = common::create_test_db();
let user1 = db
.create_user("user1", "Password123!", None, None, Some(false))
.unwrap();
let user2 = db
.create_user("user2", "Password123!", None, None, Some(false))
.unwrap();
let (_, key1) = db
.create_api_key(user1.id, Some("user1_key"), None, None, false)
.unwrap();
let user2_roles = db.get_user_roles(user2.id).unwrap();
assert!(
!user2_roles.contains(&"superadmin".to_string()),
"user2 should not be superadmin for this test"
);
assert_eq!(key1.username, "user1");
}
#[test(tokio::test)]
async fn test_cannot_revoke_other_users_api_keys() {
let (db, _dirs) = common::create_test_db();
let user1 = db
.create_user("user1", "Password123!", None, None, Some(false))
.unwrap();
let user2 = db
.create_user("user2", "Password123!", None, None, Some(false))
.unwrap();
let (_, key1) = db
.create_api_key(user1.id, Some("user1_key"), None, None, false)
.unwrap();
let key_info = db.get_api_key_info(&key1.id).unwrap();
assert!(!key_info.revoked, "Key should not be revoked initially");
let user2_roles = db.get_user_roles(user2.id).unwrap();
assert!(
!user2_roles.contains(&"superadmin".to_string()),
"user2 should not be superadmin for this test"
);
assert_eq!(key_info.username, "user1");
}
#[test(tokio::test)]
async fn test_cannot_create_service_keys_for_other_users() {
let (db, _dirs) = common::create_test_db();
let user1 = db
.create_user("user1", "Password123!", None, None, Some(false))
.unwrap();
let user2 = db
.create_user("user2", "Password123!", None, None, Some(false))
.unwrap();
let user1_roles = db.get_user_roles(user1.id).unwrap();
let user2_roles = db.get_user_roles(user2.id).unwrap();
assert!(
!user1_roles.contains(&"superadmin".to_string()),
"user1 should not be superadmin"
);
assert!(
!user2_roles.contains(&"superadmin".to_string()),
"user2 should not be superadmin"
);
assert_ne!(user1.id, user2.id);
}
#[test(tokio::test)]
async fn test_cannot_escalate_privileges_via_role_permission_modification() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let role_manager_role = db
.create_role("role_manager", Some("Can manage roles"))
.unwrap();
db.set_role_permission(role_manager_role.id, "admin_roles", "all", true)
.unwrap();
let attacker = db
.create_user(
"attacker_user",
"Password123!",
Some(vec![role_manager_role.id]),
None,
None,
)
.unwrap();
let permissions = db.get_effective_permissions(attacker.id).unwrap();
let roles = db.get_user_roles(attacker.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: attacker.id,
username: attacker.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = set_role_permission(
AuthContextExtractor(auth_ctx),
Extension(db.clone()),
Path(role_manager_role.id),
Json(SetPermissionRequest {
resource: "admin_users".to_string(),
action: "all".to_string(),
allowed: true,
}),
)
.await;
assert!(
matches!(result, Err((StatusCode::FORBIDDEN, _))),
"Non-superadmin should not be able to modify role permissions"
);
}
#[test(tokio::test)]
async fn test_cannot_remove_role_permission_denials() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let limited_admin_role = db
.create_role("limited_admin", Some("Admin without system access"))
.unwrap();
db.set_role_permission(limited_admin_role.id, "admin_roles", "all", true)
.unwrap();
db.set_role_permission(limited_admin_role.id, "admin_system", "all", false)
.unwrap();
let attacker = db
.create_user(
"limited_admin_user",
"Password123!",
Some(vec![limited_admin_role.id]),
None,
None,
)
.unwrap();
let permissions = db.get_effective_permissions(attacker.id).unwrap();
let roles = db.get_user_roles(attacker.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: attacker.id,
username: attacker.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = remove_role_permission(
AuthContextExtractor(auth_ctx),
Extension(db.clone()),
Path(limited_admin_role.id),
Query(RemovePermissionQuery {
resource: "admin_system".to_string(),
action: "all".to_string(),
}),
)
.await;
assert!(
matches!(result, Err((StatusCode::FORBIDDEN, _))),
"Non-superadmin should not be able to remove role permissions"
);
}
#[test(tokio::test)]
async fn test_cannot_modify_system_permissions() {
let (db, _dirs) = common::create_test_db();
let superadmin_role = db.get_role_by_name("superadmin").unwrap();
let perms = db.get_role_permissions(superadmin_role.id).unwrap();
assert!(!perms.is_empty(), "Superadmin should have permissions");
let system_perm = perms.iter().find(|p| p.is_system == Some(true));
assert!(
system_perm.is_some(),
"Superadmin should have at least one system permission"
);
let perm = system_perm.unwrap();
let result = db.set_role_permission(
superadmin_role.id,
&perm.resource,
&perm.action,
false,
);
assert!(
matches!(result, Err(DatabaseError::PermissionDenied(_))),
"Should not be able to modify permissions of system role. Got: {:?}",
result
);
let result = db.remove_role_permission(
superadmin_role.id,
&perm.resource,
&perm.action,
);
assert!(
matches!(result, Err(DatabaseError::PermissionDenied(_))),
"Should not be able to delete permissions of system role. Got: {:?}",
result
);
}
#[test(tokio::test)]
async fn test_all_system_roles_are_protected() {
let (db, _dirs) = common::create_test_db();
let system_roles = vec!["superadmin", "admin", "sender", "manager", "data"];
for role_name in system_roles {
let role = db.get_role_by_name(role_name).unwrap();
assert!(role.is_system, "Role {} should be a system role", role_name);
let perms = db.get_role_permissions(role.id).unwrap();
assert!(
!perms.is_empty(),
"System role {} should have permissions",
role_name
);
let first_perm = &perms[0];
let result = db.set_role_permission(
role.id,
&first_perm.resource,
&first_perm.action,
!first_perm.allowed,
);
assert!(
matches!(result, Err(DatabaseError::PermissionDenied(_))),
"Should not be able to modify permissions of system role {}. Got: {:?}",
role_name,
result
);
let result = db.remove_role_permission(
role.id,
&first_perm.resource,
&first_perm.action,
);
assert!(
matches!(result, Err(DatabaseError::PermissionDenied(_))),
"Should not be able to delete permissions of system role {}. Got: {:?}",
role_name,
result
);
}
}
#[test(tokio::test)]
async fn test_can_modify_non_system_permissions() {
let (db, _dirs) = common::create_test_db();
let custom_role = db
.create_role("modifiable_role", Some("Test role"))
.unwrap();
assert!(
!custom_role.is_system,
"Custom role should not be a system role"
);
db.set_role_permission(custom_role.id, "user", "get", true)
.unwrap();
let perms = db.get_role_permissions(custom_role.id).unwrap();
let perm = perms
.iter()
.find(|p| p.resource == "user" && p.action == "get");
assert!(perm.is_some(), "Permission should be added");
assert_eq!(
perm.unwrap().is_system,
Some(true),
"The 'user' resource is a system resource"
);
let result = db.set_role_permission(custom_role.id, "user", "get", false);
assert!(
result.is_ok(),
"Should be able to modify permissions on non-system role"
);
let result = db.remove_role_permission(custom_role.id, "user", "get");
assert!(
result.is_ok(),
"Should be able to remove permissions from non-system role"
);
let perms = db.get_role_permissions(custom_role.id).unwrap();
let perm = perms
.iter()
.find(|p| p.resource == "user" && p.action == "get");
assert!(perm.is_none(), "Permission should be removed");
}
#[test(tokio::test)]
async fn test_api_keys_revoked_on_admin_password_reset() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("victim", "OldPass123!", None, None, Some(false))
.unwrap();
let (api_key, _key_info) = db
.create_api_key(user.id, Some("test_key"), None, None, true)
.unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(auth_result.is_ok(), "API key should work before reset");
db.admin_reset_password(user.id, "NewPass123!").unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(
auth_result.is_err(),
"API key should be revoked after password reset"
);
}
#[test(tokio::test)]
async fn test_api_keys_revoked_on_user_password_change() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user(
"victim2",
"OldPass123!",
None,
None,
Some(false), )
.unwrap();
let (api_key, _key_info) = db
.create_api_key(user.id, Some("test_key2"), None, None, true)
.unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(auth_result.is_ok(), "API key should work before change");
db.update_user(user.id, Some("NewPass123!"), None).unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(
auth_result.is_err(),
"API key should be revoked after password change"
);
}
#[test(tokio::test)]
async fn test_cannot_change_superadmin_password_via_update() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let superadmin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let admin_role = db
.create_role("user_admin", Some("Can manage users"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "put", true)
.unwrap();
let attacker = db
.create_user(
"attacker_admin",
"AttackerPass123!",
Some(vec![admin_role.id]),
None,
None,
)
.unwrap();
let permissions = db.get_effective_permissions(attacker.id).unwrap();
let roles = db.get_user_roles(attacker.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: attacker.id,
username: attacker.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = update_user(
AuthContextExtractor(auth_ctx),
Extension(db.clone()),
Path(superadmin.id),
Json(UpdateUserRequest {
password: Some("HackedPass123!".to_string()),
is_active: None,
role_ids: None,
}),
)
.await;
assert!(
matches!(result, Err((StatusCode::FORBIDDEN, _))),
"Non-superadmin should not be able to change superadmin's password"
);
}
#[test(tokio::test)]
async fn test_api_keys_revoked_on_update_user_password_change() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("victim3", "OldPass123!", None, None, Some(false))
.unwrap();
let (api_key, _key_info) = db
.create_api_key(user.id, Some("test_key3"), None, None, true)
.unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(
auth_result.is_ok(),
"API key should work before password change"
);
db.update_user(user.id, Some("NewPass123!"), None).unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(
auth_result.is_err(),
"API key should be revoked after password change via update_user"
);
}
#[test(tokio::test)]
async fn test_api_keys_blocked_when_must_change_password() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("new_user", "InitialPass123!", None, Some(1), None)
.unwrap();
let (api_key, _key_info) = db
.create_api_key(user.id, Some("bypass_key"), None, None, true)
.unwrap();
let auth_result =
db.authenticate_api_key_request(&api_key, None, "/peer-id");
assert!(
auth_result.is_err(),
"API key should be blocked when must_change_password is set"
);
match auth_result {
Err(DatabaseError::PasswordChangeRequired(_)) => {
}
_ => panic!("Expected PasswordChangeRequired error"),
}
db.change_password_with_credentials(
"new_user",
"InitialPass123!",
"NewPass123!",
)
.unwrap();
let (new_api_key, _new_key_info) = db
.create_api_key(user.id, Some("valid_key"), None, None, true)
.unwrap();
let auth_result =
db.authenticate_api_key_request(&new_api_key, None, "/peer-id");
assert!(
auth_result.is_ok(),
"API key should work after password has been changed"
);
}
#[test(tokio::test)]
async fn test_superadmin_can_change_own_password() {
let (db, _dirs) = common::create_test_db();
let superadmin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let result = db.update_user(superadmin.id, Some("NewAdminPass123!"), None);
assert!(
result.is_ok(),
"Superadmin should be able to change their own password"
);
let old_login = db.verify_credentials("admin", "AdminPass123!");
assert!(old_login.is_err(), "Old password should not work");
let new_login = db.verify_credentials("admin", "NewAdminPass123!");
assert!(new_login.is_ok(), "New password should work");
}
#[test(tokio::test)]
async fn test_superadmin_can_delete_own_api_keys() {
let (db, _dirs) = common::create_test_db();
let superadmin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let (api_key, key_info) = db
.create_api_key(superadmin.id, Some("admin_key"), None, None, true)
.unwrap();
assert!(
db.authenticate_api_key_request(&api_key, None, "/peer-id")
.is_ok()
);
let result = db.revoke_api_key(
&key_info.id,
Some(superadmin.id),
Some("Self-revocation"),
);
assert!(
result.is_ok(),
"Superadmin should be able to revoke their own API key"
);
assert!(
db.authenticate_api_key_request(&api_key, None, "/peer-id")
.is_err()
);
}
#[test(tokio::test)]
async fn test_permission_conflict_user_deny_overrides_role_allow() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
let role = db.create_role("viewer", None).unwrap();
db.set_role_permission(role.id, "admin_users", "get", true)
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
let perms = db.get_effective_permissions(user.id).unwrap();
assert!(perms.iter().any(|p| p.resource == "admin_users"
&& p.action == "get"
&& p.allowed));
db.set_user_permission(user.id, "admin_users", "get", false, None)
.unwrap();
let perms = db.get_effective_permissions(user.id).unwrap();
let events_get = perms
.iter()
.find(|p| p.resource == "admin_users" && p.action == "get");
assert!(events_get.is_some());
assert!(
!events_get.unwrap().allowed,
"User-level deny should override role allow"
);
}
#[test(tokio::test)]
async fn test_removing_role_removes_permissions_immediately() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
let role = db.create_role("editor", None).unwrap();
db.set_role_permission(role.id, "admin_users", "post", true)
.unwrap();
db.set_role_permission(role.id, "admin_users", "delete", true)
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
let perms = db.get_effective_permissions(user.id).unwrap();
assert!(
perms
.iter()
.any(|p| p.resource == "admin_users" && p.action == "post")
);
assert!(
perms
.iter()
.any(|p| p.resource == "admin_users" && p.action == "delete")
);
db.remove_role_from_user(user.id, role.id).unwrap();
let perms = db.get_effective_permissions(user.id).unwrap();
assert!(
!perms
.iter()
.any(|p| p.resource == "admin_users" && p.action == "post")
);
assert!(
!perms
.iter()
.any(|p| p.resource == "admin_users" && p.action == "delete")
);
}
#[test(tokio::test)]
async fn test_deleting_role_removes_from_all_users() {
let (db, _dirs) = common::create_test_db();
let user1 = db
.create_user("user1", "Pass123!", None, None, Some(false))
.unwrap();
let user2 = db
.create_user("user2", "Pass123!", None, None, Some(false))
.unwrap();
let role = db.create_role("shared_role", None).unwrap();
db.set_role_permission(role.id, "admin_users", "get", true)
.unwrap();
db.assign_role_to_user(user1.id, role.id, None).unwrap();
db.assign_role_to_user(user2.id, role.id, None).unwrap();
assert!(db.get_effective_permissions(user1.id).unwrap().len() > 0);
assert!(db.get_effective_permissions(user2.id).unwrap().len() > 0);
db.delete_role(role.id).unwrap();
assert_eq!(db.get_effective_permissions(user1.id).unwrap().len(), 0);
assert_eq!(db.get_effective_permissions(user2.id).unwrap().len(), 0);
}
#[test(tokio::test)]
async fn test_management_key_has_full_permissions() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
let role = db.create_role("viewer", None).unwrap();
db.set_role_permission(role.id, "admin_users", "get", true)
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
let result = db.create_api_key(
user.id,
Some("management_key"),
None,
None,
true, );
assert!(result.is_ok());
let (_api_key, key_info) = result.unwrap();
assert!(
key_info.is_management,
"Key should be marked as management key"
);
}
#[test(tokio::test)]
async fn test_service_key_flag() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
let (_api_key, key_info) = db
.create_api_key(user.id, Some("service_key"), None, None, false)
.unwrap();
assert!(
!key_info.is_management,
"Key should be marked as service key (not management)"
);
}
#[test(tokio::test)]
async fn test_concurrent_password_changes() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let user = db
.create_user("testuser", "OldPass123!", None, None, Some(false))
.unwrap();
let mut handles = vec![];
for i in 0..5 {
let db_clone = Arc::clone(&db);
let user_id = user.id;
let handle = std::thread::spawn(move || {
db_clone.update_user(user_id, Some(&format!("NewPass{}!", i)), None)
});
handles.push(handle);
}
let mut successes = 0;
for handle in handles {
if handle.join().unwrap().is_ok() {
successes += 1;
}
}
assert!(
successes > 0,
"At least one concurrent password change should succeed"
);
}
#[test(tokio::test)]
async fn test_lockout_triggers_after_failed_attempts() {
let (db, _dirs) = common::create_test_db();
db.create_user("victim", "CorrectPass123!", None, None, Some(false))
.unwrap();
for _ in 0..4 {
let _ = db.verify_credentials("victim", "WrongPass123!");
}
let result = db.verify_credentials("victim", "CorrectPass123!");
assert!(
result.is_ok(),
"Should be able to login after 4 failed attempts"
);
for _ in 0..5 {
let _ = db.verify_credentials("victim", "WrongPass123!");
}
let result = db.verify_credentials("victim", "CorrectPass123!");
assert!(
result.is_err(),
"Account should be locked after 5 failed attempts"
);
}
#[test(tokio::test)]
async fn test_password_change_resets_lockout() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("victim", "OldPass123!", None, None, Some(false))
.unwrap();
for _ in 0..5 {
let _ = db.verify_credentials("victim", "WrongPass!");
}
let result = db.verify_credentials("victim", "OldPass123!");
assert!(result.is_err());
db.admin_reset_password(user.id, "NewPass123!").unwrap();
let result = db.change_password_with_credentials(
"victim",
"NewPass123!",
"FinalPass123!",
);
assert!(
result.is_ok(),
"Password change should work after admin reset"
);
let updated_user =
db.verify_credentials("victim", "FinalPass123!").unwrap();
assert_eq!(updated_user.failed_login_attempts, 0);
assert!(updated_user.locked_until.is_none());
}
#[test(tokio::test)]
async fn test_admin_cannot_assign_role_to_other_admin() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let admin_role = db
.create_role("user_admin", Some("Can manage users"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let editor_role = db
.create_role("editor_admin", Some("Editor admin"))
.unwrap();
db.set_role_permission(editor_role.id, "admin_roles", "all", true)
.unwrap();
let admin_actor = db
.create_user(
"admin_actor",
"Password123!",
Some(vec![admin_role.id]),
None,
Some(false),
)
.unwrap();
let admin_target = db
.create_user(
"admin_target",
"Password123!",
Some(vec![editor_role.id]),
None,
Some(false),
)
.unwrap();
let _actor_user = db.get_user_by_id(admin_actor.id).unwrap();
let _target_user = db.get_user_by_id(admin_target.id).unwrap();
let permissions = db.get_effective_permissions(admin_actor.id).unwrap();
let roles = db.get_user_roles(admin_actor.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: admin_actor.id,
username: admin_actor.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = assign_role(
AuthContextExtractor(auth_ctx.clone()),
Extension(db.clone()),
Path((admin_target.id, editor_role.id)),
)
.await;
assert!(
matches!(result, Err((StatusCode::FORBIDDEN, _))),
"Non-superadmin admin should not be able to assign roles to other admins"
);
}
#[test(tokio::test)]
async fn test_admin_cannot_remove_role_from_other_admin() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let admin_role = db
.create_role("user_admin_2", Some("Can manage users"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let system_admin_role = db
.create_role("system_admin", Some("System admin"))
.unwrap();
db.set_role_permission(system_admin_role.id, "admin_system", "all", true)
.unwrap();
let admin_actor = db
.create_user(
"admin_actor_2",
"Password123!",
Some(vec![admin_role.id]),
None,
Some(false),
)
.unwrap();
let admin_target = db
.create_user(
"admin_target_2",
"Password123!",
Some(vec![system_admin_role.id]),
None,
Some(false),
)
.unwrap();
let permissions = db.get_effective_permissions(admin_actor.id).unwrap();
let roles = db.get_user_roles(admin_actor.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: admin_actor.id,
username: admin_actor.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = remove_role(
AuthContextExtractor(auth_ctx.clone()),
Extension(db.clone()),
Path((admin_target.id, system_admin_role.id)),
)
.await;
assert!(
matches!(result, Err((StatusCode::FORBIDDEN, _))),
"Non-superadmin admin should not be able to remove roles from other admins"
);
}
#[test(tokio::test)]
async fn test_admin_can_assign_role_to_regular_user() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let admin_role = db
.create_role("user_admin_3", Some("Can manage users"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let viewer_role = db.create_role("viewer", Some("Viewer role")).unwrap();
let admin_user = db
.create_user(
"admin_user",
"Password123!",
Some(vec![admin_role.id]),
None,
Some(false),
)
.unwrap();
let regular_user = db
.create_user(
"regular_user",
"Password123!",
None, None,
Some(false),
)
.unwrap();
let permissions = db.get_effective_permissions(admin_user.id).unwrap();
let roles = db.get_user_roles(admin_user.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: admin_user.id,
username: admin_user.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = assign_role(
AuthContextExtractor(auth_ctx.clone()),
Extension(db.clone()),
Path((regular_user.id, viewer_role.id)),
)
.await;
assert!(
result.is_ok(),
"Admin should be able to assign roles to regular (non-admin) users"
);
}
#[test(tokio::test)]
async fn test_superadmin_can_assign_role_to_admin() {
let (db, _dirs) = common::create_test_db();
let db = Arc::new(db);
let superadmin = db.verify_credentials("admin", "AdminPass123!").unwrap();
let admin_role = db
.create_role("new_admin_role", Some("New admin role"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let other_admin = db
.create_user(
"other_admin",
"Password123!",
Some(vec![admin_role.id]),
None,
Some(false),
)
.unwrap();
let permissions = db.get_effective_permissions(superadmin.id).unwrap();
let roles = db.get_user_roles(superadmin.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: superadmin.id,
username: superadmin.username.clone(),
roles,
permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = assign_role(
AuthContextExtractor(auth_ctx.clone()),
Extension(db.clone()),
Path((other_admin.id, admin_role.id)),
)
.await;
assert!(
result.is_ok(),
"Superadmin should be able to assign roles to other admins"
);
}
#[test(tokio::test)]
async fn test_permission_source_field_distinguishes_direct_and_role_permissions()
{
use ave_http::auth::admin_handlers::get_user_permissions;
let (db, _dirs) = common::create_test_db();
let superadmin = db
.create_user("superadmin", "SuperPass123!", None, None, Some(false))
.unwrap();
db.assign_role_to_user(superadmin.id, 1, None).unwrap();
let test_role = db
.create_role("test_role", Some("Role for testing permission sources"))
.unwrap();
db.set_role_permission(test_role.id, "admin_users", "get", true)
.unwrap();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
db.assign_role_to_user(user.id, test_role.id, None).unwrap();
db.set_user_permission(user.id, "admin_roles", "post", true, None)
.unwrap();
let superadmin_permissions =
db.get_effective_permissions(superadmin.id).unwrap();
let superadmin_roles = db.get_user_roles(superadmin.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: superadmin.id,
username: superadmin.username.clone(),
roles: superadmin_roles,
permissions: superadmin_permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = get_user_permissions(
AuthContextExtractor(auth_ctx),
Extension(Arc::new(db.clone())),
Path(user.id),
)
.await;
assert!(
result.is_ok(),
"Should successfully retrieve user permissions"
);
let Json(permissions) = result.unwrap();
assert!(!permissions.is_empty(), "User should have permissions");
let role_perm = permissions
.iter()
.find(|p| p.resource == "admin_users" && p.action == "get");
assert!(role_perm.is_some(), "Should have admin_users:get from role");
let role_perm = role_perm.unwrap();
assert_eq!(
role_perm.source.as_deref(),
Some("role"),
"admin_users:get should have source='role'"
);
assert_eq!(
role_perm.role_name.as_deref(),
Some("test_role"),
"admin_users:get should have role_name='test_role'"
);
let direct_perm = permissions
.iter()
.find(|p| p.resource == "admin_roles" && p.action == "post");
assert!(
direct_perm.is_some(),
"Should have admin_roles:post direct permission"
);
let direct_perm = direct_perm.unwrap();
assert_eq!(
direct_perm.source.as_deref(),
Some("direct"),
"admin_roles:post should have source='direct'"
);
assert!(
direct_perm.role_name.is_none(),
"Direct permissions should not have role_name"
);
println!("✓ Permission source tracking works correctly:");
println!(" - Role permission has source='role' and role_name");
println!(" - Direct permission has source='direct' and no role_name");
}
#[test(tokio::test)]
async fn test_direct_permission_overrides_role_permission() {
use ave_http::auth::admin_handlers::get_user_permissions;
let (db, _dirs) = common::create_test_db();
let superadmin = db
.create_user("superadmin", "SuperPass123!", None, None, Some(false))
.unwrap();
db.assign_role_to_user(superadmin.id, 1, None).unwrap();
let role = db
.create_role("system_admin", Some("System administrator"))
.unwrap();
db.set_role_permission(role.id, "admin_system", "all", true)
.unwrap();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
db.set_user_permission(user.id, "admin_system", "all", false, None)
.unwrap();
let superadmin_permissions =
db.get_effective_permissions(superadmin.id).unwrap();
let superadmin_roles = db.get_user_roles(superadmin.id).unwrap();
let auth_ctx = Arc::new(AuthContext {
user_id: superadmin.id,
username: superadmin.username.clone(),
roles: superadmin_roles,
permissions: superadmin_permissions,
api_key_id: "test-key".to_string(),
is_management_key: true,
ip_address: None,
});
let result = get_user_permissions(
AuthContextExtractor(auth_ctx),
Extension(Arc::new(db.clone())),
Path(user.id),
)
.await;
assert!(result.is_ok());
let Json(permissions) = result.unwrap();
let perm = permissions
.iter()
.find(|p| p.resource == "admin_system" && p.action == "all");
assert!(perm.is_some(), "Should have admin_system:all permission");
let perm = perm.unwrap();
assert_eq!(
perm.source.as_deref(),
Some("direct"),
"Direct override should take precedence, showing source='direct'"
);
assert_eq!(
perm.allowed, false,
"Direct deny should override role allow"
);
assert!(
perm.role_name.is_none(),
"Direct permission override should not have role_name"
);
println!(
"✓ Direct permission override correctly takes precedence over role permission"
);
}
#[test(tokio::test)]
async fn test_direct_deny_blocks_role_allow_functionally() {
let (db, _dirs) = common::create_test_db();
let admin_role = db
.create_role("user_admin", Some("User administrator"))
.unwrap();
db.set_role_permission(admin_role.id, "admin_users", "all", true)
.unwrap();
let user = db
.create_user("blocked_user", "TestPass123!", None, None, Some(false))
.unwrap();
db.assign_role_to_user(user.id, admin_role.id, None)
.unwrap();
let role_perms = db.get_role_permissions(admin_role.id).unwrap();
assert!(
role_perms.iter().any(|p| p.resource == "admin_users"
&& p.action == "all"
&& p.allowed),
"Role should have admin_users:all permission"
);
db.set_user_permission(user.id, "admin_users", "get", false, None)
.unwrap();
let effective_perms = db.get_effective_permissions(user.id).unwrap();
let get_perm = effective_perms
.iter()
.find(|p| p.resource == "admin_users" && p.action == "get");
assert!(
get_perm.is_some(),
"Should have admin_users:get in effective permissions"
);
let get_perm = get_perm.unwrap();
assert_eq!(
get_perm.allowed, false,
"Direct deny should override role 'all' permission - user should be BLOCKED"
);
let post_perm = effective_perms
.iter()
.find(|p| p.resource == "admin_users" && p.action == "post");
if let Some(post_perm) = post_perm {
assert!(
post_perm.allowed,
"Other actions not explicitly denied should still be allowed via role"
);
}
let user_roles = db.get_user_roles(user.id).unwrap();
let auth_ctx = AuthContext {
user_id: user.id,
username: user.username.clone(),
roles: user_roles,
permissions: effective_perms,
api_key_id: "test-key".to_string(),
is_management_key: false,
ip_address: None,
};
assert!(
!auth_ctx.has_permission("admin_users", "get"),
"User should be BLOCKED from admin_users:get despite role having 'all' permission"
);
println!(
"✓ Direct deny successfully blocks access despite role granting 'all' permission"
);
println!(" - Role grants admin_users:all (should allow everything)");
println!(
" - Direct deny on admin_users:get (blocks this specific action)"
);
println!(" - Result: User CANNOT access admin_users:get ✓");
}
#[test(tokio::test)]
async fn test_service_key_cannot_manage_api_keys() {
use ave_http::auth::apikey_handlers::{
create_my_api_key, revoke_my_api_key,
};
use ave_http::auth::models::CreateApiKeyRequest;
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
db.set_user_permission(user.id, "user_api_key", "post", true, None)
.unwrap();
db.set_user_permission(user.id, "user_api_key", "delete", true, None)
.unwrap();
let (_management_key, management_info) = db
.create_api_key(user.id, Some("management_session"), None, None, true)
.unwrap();
assert!(management_info.is_management, "Should be a management key");
let user_roles = db.get_user_roles(user.id).unwrap();
let user_perms = db.get_effective_permissions(user.id).unwrap();
let management_ctx = Arc::new(AuthContext {
user_id: user.id,
username: "testuser".to_string(),
roles: user_roles.clone(),
permissions: user_perms.clone(),
api_key_id: management_info.id.clone(),
is_management_key: true,
ip_address: None,
});
let create_req = CreateApiKeyRequest {
name: "service_key".to_string(),
description: Some("Service key for automation".to_string()),
expires_in_seconds: None,
};
let result = create_my_api_key(
AuthContextExtractor(management_ctx.clone()),
Extension(Arc::new(db.clone())),
Json(create_req),
)
.await;
assert!(
result.is_ok(),
"Management key should be able to create service keys"
);
let (status, Json(response)) = result.unwrap();
assert_eq!(status, StatusCode::CREATED);
let _service_key = response.api_key;
let service_info = response.key_info;
assert!(
!service_info.is_management,
"Created key should be a service key"
);
let service_ctx = Arc::new(AuthContext {
user_id: user.id,
username: "testuser".to_string(),
roles: user_roles.clone(),
permissions: user_perms.clone(),
api_key_id: service_info.id.clone(),
is_management_key: false, ip_address: None,
});
let create_req2 = CreateApiKeyRequest {
name: "another_service_key".to_string(),
description: Some("Trying to create another key".to_string()),
expires_in_seconds: None,
};
let result = create_my_api_key(
AuthContextExtractor(service_ctx.clone()),
Extension(Arc::new(db.clone())),
Json(create_req2),
)
.await;
assert!(
result.is_err(),
"Service key should NOT be able to create API keys"
);
let (status, Json(err)) = result.unwrap_err();
assert_eq!(status, StatusCode::FORBIDDEN);
assert!(
err.error.contains("management API key"),
"Error should mention management key requirement"
);
let result = revoke_my_api_key(
AuthContextExtractor(service_ctx),
Extension(Arc::new(db.clone())),
Path(service_info.name.clone()),
None,
)
.await;
assert!(
result.is_err(),
"Service key should NOT be able to revoke API keys"
);
let (status, Json(err)) = result.unwrap_err();
assert_eq!(status, StatusCode::FORBIDDEN);
assert!(
err.error.contains("management API key"),
"Error should mention management key requirement"
);
println!("✓ Service keys correctly blocked from managing API keys");
println!(" - Service key CANNOT create new API keys");
println!(" - Service key CANNOT revoke API keys");
println!(" - Only management keys (from login) can manage API keys");
}
#[test(tokio::test)]
async fn test_all_permission_validation() {
let (db, _dirs) = common::create_test_db();
let user = db
.create_user("testuser", "TestPass123!", None, None, Some(false))
.unwrap();
db.set_user_permission(user.id, "admin_users", "get", true, None)
.unwrap();
db.set_user_permission(user.id, "admin_users", "post", true, None)
.unwrap();
db.set_user_permission(user.id, "admin_users", "put", true, None)
.unwrap();
let all_perms_before = db.get_user_permissions(user.id).unwrap();
let perms_before: Vec<_> = all_perms_before
.iter()
.filter(|p| p.source.as_deref() == Some("direct"))
.collect();
let get_perm = perms_before
.iter()
.find(|p| p.resource == "admin_users" && p.action == "get");
let post_perm = perms_before
.iter()
.find(|p| p.resource == "admin_users" && p.action == "post");
assert!(get_perm.is_some(), "Should have get permission");
assert!(post_perm.is_some(), "Should have post permission");
db.set_user_permission(user.id, "admin_users", "all", true, None)
.unwrap();
let all_perms_after = db.get_user_permissions(user.id).unwrap();
let perms_after: Vec<_> = all_perms_after
.iter()
.filter(|p| p.source.as_deref() == Some("direct"))
.collect();
let get_perm_after = perms_after
.iter()
.find(|p| p.resource == "admin_users" && p.action == "get");
let post_perm_after = perms_after
.iter()
.find(|p| p.resource == "admin_users" && p.action == "post");
let all_perm = perms_after
.iter()
.find(|p| p.resource == "admin_users" && p.action == "all");
assert!(
get_perm_after.is_none(),
"Individual 'get' permission should be removed"
);
assert!(
post_perm_after.is_none(),
"Individual 'post' permission should be removed"
);
assert!(all_perm.is_some(), "Should have 'all' permission");
println!("✓ Test 1 passed: Assigning 'all' removes individual permissions");
let result =
db.set_user_permission(user.id, "admin_users", "delete", true, None);
assert!(
result.is_err(),
"Should not allow individual permission when 'all' exists"
);
if let Err(DatabaseError::Validation(msg)) = result {
assert!(
msg.contains("already has 'all' permission"),
"Error message should mention 'all' permission"
);
} else {
panic!("Expected ValidationError, got: {:?}", result);
}
println!(
"✓ Test 2 passed: Cannot assign individual permission when 'all' exists"
);
let role = db.create_role("test_role", Some("Test role")).unwrap();
db.set_role_permission(role.id, "admin_roles", "get", true)
.unwrap();
db.set_role_permission(role.id, "admin_roles", "post", true)
.unwrap();
db.assign_role_to_user(user.id, role.id, None).unwrap();
let result =
db.set_user_permission(user.id, "admin_roles", "all", false, None);
assert!(
result.is_ok(),
"Should allow direct 'all' permission even when role has individual permissions"
);
let all_perms = db.get_user_permissions(user.id).unwrap();
let role_get = all_perms.iter().find(|p| {
p.resource == "admin_roles"
&& p.action == "get"
&& p.source.as_deref() == Some("role")
});
let role_post = all_perms.iter().find(|p| {
p.resource == "admin_roles"
&& p.action == "post"
&& p.source.as_deref() == Some("role")
});
let direct_all = all_perms.iter().find(|p| {
p.resource == "admin_roles"
&& p.action == "all"
&& p.source.as_deref() == Some("direct")
});
assert!(
role_get.is_some(),
"Role 'get' permission should still exist"
);
assert!(
role_post.is_some(),
"Role 'post' permission should still exist"
);
assert!(direct_all.is_some(), "Direct 'all' override should exist");
assert!(
!direct_all.unwrap().allowed,
"Direct 'all' should be denied (override)"
);
println!(
"✓ Test 3 passed: Role permissions are not affected by direct permission validation"
);
println!("✓ All permission validation works correctly:");
println!(" - Assigning 'all' removes individual direct permissions");
println!(" - Cannot assign individual when 'all' exists");
println!(" - Role permissions remain independent");
}
#[test(tokio::test)]
async fn test_system_config_ttl_validation() {
use ave_bridge::auth::{
ApiKeyConfig, AuthConfig, LockoutConfig, RateLimitConfig, SessionConfig,
};
let tmp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let config = AuthConfig {
durability: false,
enable: true,
database_path: tmp_dir.path().join("auth.db"),
superadmin: "admin".to_string(),
api_key: ApiKeyConfig::default(),
lockout: LockoutConfig::default(),
rate_limit: RateLimitConfig::default(),
session: SessionConfig::default(),
};
let db = AuthDatabase::new(config, "TestPass123!", None)
.expect("Failed to create database");
println!("Testing api_key_default_ttl_seconds validation:");
let result =
db.update_system_config("api_key_default_ttl_seconds", "3600", Some(1));
assert!(result.is_ok(), "Valid positive TTL should be accepted");
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(3600));
let result =
db.update_system_config("api_key_default_ttl_seconds", "0", Some(1));
assert!(
result.is_ok(),
"Zero TTL (no expiration) should be accepted"
);
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(0));
let result =
db.update_system_config("api_key_default_ttl_seconds", "-1", Some(1));
assert!(result.is_err(), "Negative TTL should be rejected");
if let Err(DatabaseError::Validation(msg)) = result {
assert!(msg.contains("must be >= 0"));
} else {
panic!("Expected ValidationError for negative TTL");
}
let result = db.update_system_config(
"api_key_default_ttl_seconds",
"not_a_number",
Some(1),
);
assert!(result.is_err(), "Invalid integer should be rejected");
println!("Testing max_login_attempts validation:");
let result = db.update_system_config("max_login_attempts", "5", Some(1));
assert!(
result.is_ok(),
"Valid max_login_attempts should be accepted"
);
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(5));
let result = db.update_system_config("max_login_attempts", "0", Some(1));
assert!(
result.is_err(),
"Zero max_login_attempts should be rejected"
);
if let Err(DatabaseError::Validation(msg)) = result {
assert!(msg.contains("must be > 0"));
} else {
panic!("Expected ValidationError for zero max_login_attempts");
}
let result =
db.update_system_config("max_login_attempts", "invalid", Some(1));
assert!(
result.is_err(),
"Invalid max_login_attempts should be rejected"
);
println!("Testing lockout_duration_seconds validation:");
let result =
db.update_system_config("lockout_duration_seconds", "300", Some(1));
assert!(result.is_ok(), "Valid lockout_duration should be accepted");
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(300));
let result =
db.update_system_config("lockout_duration_seconds", "0", Some(1));
assert!(result.is_err(), "Zero lockout_duration should be rejected");
if let Err(DatabaseError::Validation(msg)) = result {
assert!(msg.contains("must be > 0"));
} else {
panic!("Expected ValidationError for zero lockout_duration");
}
let result =
db.update_system_config("lockout_duration_seconds", "-100", Some(1));
assert!(
result.is_err(),
"Negative lockout_duration should be rejected"
);
println!("Testing rate_limit_window_seconds validation:");
let result =
db.update_system_config("rate_limit_window_seconds", "60", Some(1));
assert!(result.is_ok(), "Valid rate_limit_window should be accepted");
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(60));
let result =
db.update_system_config("rate_limit_window_seconds", "0", Some(1));
assert!(result.is_err(), "Zero rate_limit_window should be rejected");
if let Err(DatabaseError::Validation(msg)) = result {
assert!(msg.contains("must be > 0"));
} else {
panic!("Expected ValidationError for zero rate_limit_window");
}
let result =
db.update_system_config("rate_limit_window_seconds", "-60", Some(1));
assert!(
result.is_err(),
"Negative rate_limit_window should be rejected"
);
println!("Testing rate_limit_max_requests validation:");
let result =
db.update_system_config("rate_limit_max_requests", "100", Some(1));
assert!(
result.is_ok(),
"Valid rate_limit_max_requests should be accepted"
);
assert_eq!(result.unwrap().value, SystemConfigValue::Integer(100));
let result =
db.update_system_config("rate_limit_max_requests", "0", Some(1));
assert!(
result.is_err(),
"Zero rate_limit_max_requests should be rejected"
);
if let Err(DatabaseError::Validation(msg)) = result {
assert!(msg.contains("must be > 0"));
} else {
panic!("Expected ValidationError for zero rate_limit_max_requests");
}
let result =
db.update_system_config("rate_limit_max_requests", "invalid", Some(1));
assert!(
result.is_err(),
"Invalid rate_limit_max_requests should be rejected"
);
println!("Testing unknown config key:");
let result = db.update_system_config("some_other_config", "-1", Some(1));
assert!(result.is_err(), "Non-existent key should fail");
println!("✓ All system config validations work correctly:");
println!(
" - api_key_default_ttl_seconds: >= 0 (allows 0 for no expiration)"
);
println!(" - max_login_attempts: > 0");
println!(" - lockout_duration_seconds: > 0");
println!(" - rate_limit_window_seconds: > 0");
println!(" - rate_limit_max_requests: > 0");
}