683 lines
22 KiB
Rust
683 lines
22 KiB
Rust
use axum::extract::{ConnectInfo, Form, State};
|
|
use axum::http::HeaderMap;
|
|
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
|
use chrono::Utc;
|
|
use rusqlite::Connection;
|
|
use sha2::{Digest, Sha256};
|
|
use std::collections::HashMap;
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
use std::path::PathBuf;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Instant;
|
|
|
|
use bzod::analytics::AnalyticsQueue;
|
|
use bzod::auth::session::authenticate_api_key;
|
|
use bzod::config::Config;
|
|
use bzod::db::admin::{create_api_key, create_session};
|
|
use bzod::db::migrations::{run_migrations, ADMIN_MIGRATIONS, SYSTEM_MIGRATIONS, USERS_MIGRATIONS};
|
|
use bzod::db::sqlite::get_user_version;
|
|
use bzod::db::users::{create_admin_user, create_user, get_user_by_username};
|
|
use bzod::db::Db;
|
|
use bzod::web::admin::{dashboard_get, login_post, LoginForm};
|
|
|
|
fn create_temp_config(temp_dir: PathBuf) -> Config {
|
|
let mut config = Config::load();
|
|
config.data_dir = temp_dir.clone();
|
|
config.backup_dir = temp_dir.clone();
|
|
config.admin_username = "admin".to_string();
|
|
config.base_url = Some("http://localhost:8080".to_string());
|
|
config
|
|
}
|
|
|
|
fn compute_sha256(value: &str) -> String {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(value.as_bytes());
|
|
hex::encode(hasher.finalize())
|
|
}
|
|
|
|
fn build_state(config: Config) -> (Db, bzod::state::AppState) {
|
|
let db = Db::init(&config).expect("Failed to init Db");
|
|
let (tx, rx) = tokio::sync::watch::channel(false);
|
|
Box::leak(Box::new(tx));
|
|
let (queue, _) = AnalyticsQueue::new(db.clone(), 1000, rx);
|
|
let state = bzod::state::AppState {
|
|
admin_db: db.admin.clone(),
|
|
system_db: db.system.clone(),
|
|
users_db: db.users.clone(),
|
|
user_dbs: Arc::new(Mutex::new(HashMap::new())),
|
|
db: db.clone(),
|
|
config,
|
|
analytics_queue: queue,
|
|
start_time: Instant::now(),
|
|
};
|
|
(db, state)
|
|
}
|
|
|
|
#[test]
|
|
fn test_migration_idempotent() {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
|
|
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
|
|
assert_eq!(
|
|
get_user_version(&conn).unwrap(),
|
|
USERS_MIGRATIONS.last().unwrap().version
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_users_db_schema_version_2() {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
|
|
assert_eq!(
|
|
get_user_version(&conn).unwrap(),
|
|
USERS_MIGRATIONS.last().unwrap().version
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_admin_db_schema_version_2() {
|
|
let mut conn = Connection::open_in_memory().unwrap();
|
|
run_migrations(&mut conn, "admin", ADMIN_MIGRATIONS, None).unwrap();
|
|
assert_eq!(get_user_version(&conn).unwrap(), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_account_migration() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_admin_migration_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
|
|
let users_path = temp_dir.join("admin").join("users.db");
|
|
std::fs::create_dir_all(users_path.parent().unwrap()).unwrap();
|
|
let mut conn = Connection::open(&users_path).unwrap();
|
|
run_migrations(&mut conn, "users", USERS_MIGRATIONS, None).unwrap();
|
|
conn.pragma_update(None, "user_version", 1).unwrap();
|
|
|
|
let password_hash = bzod::auth::hash_password("securepass").unwrap();
|
|
conn.execute(
|
|
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES (?1, ?2, ?3, ?4, ?5);",
|
|
rusqlite::params!["admin", password_hash, "active", Utc::now().to_rfc3339(), "standard"],
|
|
)
|
|
.unwrap();
|
|
|
|
let mut system_conn = Connection::open(temp_dir.join("admin/system.db")).unwrap();
|
|
run_migrations(&mut system_conn, "system", SYSTEM_MIGRATIONS, None).unwrap();
|
|
drop(system_conn);
|
|
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let db = Db::init(&config).expect("Db::init should repair an existing admin account");
|
|
|
|
let conn = db.users.lock().unwrap();
|
|
let user = get_user_by_username(&conn, "admin").unwrap().unwrap();
|
|
println!("admin account_type = {}", user.account_type);
|
|
println!("admin status = {}", user.status);
|
|
let schema_version: i64 = conn
|
|
.query_row("PRAGMA user_version;", [], |row| row.get(0))
|
|
.unwrap();
|
|
println!("users db version = {}", schema_version);
|
|
assert_eq!(user.account_type, "admin");
|
|
assert_eq!(user.status, "active");
|
|
|
|
let system_conn = db.system.lock().unwrap();
|
|
let count: i64 = system_conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM audit_events WHERE action = 'migration_repair' AND actor = 'admin' AND object_type = 'users' AND object_id = 'admin';",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(count, 1);
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_login() {
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bzod_test_admin_login_{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "adminpassword";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_admin_user(&conn, "admin", &hash).unwrap();
|
|
}
|
|
|
|
let csrf_token = compute_sha256("csrf-token");
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_temp_csrf", csrf_token.clone()));
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert("user-agent", "test-agent".parse().unwrap());
|
|
let connect_info = Some(ConnectInfo(SocketAddr::new(
|
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
8080,
|
|
)));
|
|
let form = LoginForm {
|
|
username: "admin".to_string(),
|
|
password: password.to_string(),
|
|
csrf_token,
|
|
};
|
|
|
|
let response = login_post(
|
|
State(state.clone()),
|
|
jar.clone(),
|
|
headers,
|
|
connect_info,
|
|
Form(form),
|
|
)
|
|
.await;
|
|
assert!(response.status().is_redirection());
|
|
assert_eq!(
|
|
response.headers().get("location").unwrap(),
|
|
"/admin/dashboard"
|
|
);
|
|
|
|
let conn = state.users_db.lock().unwrap();
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM sessions;", [], |row| row.get(0))
|
|
.unwrap();
|
|
assert_eq!(count, 1);
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_disabled_user_login_rejected() {
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bzod_test_disabled_login_{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "disabledpass";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
conn.execute(
|
|
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES (?1, ?2, ?3, ?4, ?5);",
|
|
rusqlite::params!["disabled_user", hash, "disabled", Utc::now().to_rfc3339(), "system"],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let csrf_token = compute_sha256("csrf-token");
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_temp_csrf", csrf_token.clone()));
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert("user-agent", "test-agent".parse().unwrap());
|
|
let connect_info = Some(ConnectInfo(SocketAddr::new(
|
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
8080,
|
|
)));
|
|
let form = LoginForm {
|
|
username: "disabled_user".to_string(),
|
|
password: password.to_string(),
|
|
csrf_token,
|
|
};
|
|
|
|
let response = login_post(
|
|
State(state.clone()),
|
|
jar.clone(),
|
|
headers,
|
|
connect_info,
|
|
Form(form),
|
|
)
|
|
.await;
|
|
assert!(response.status().is_redirection());
|
|
assert!(response
|
|
.headers()
|
|
.get("location")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("/admin/login"));
|
|
|
|
let conn = state.users_db.lock().unwrap();
|
|
let count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM sessions;", [], |row| row.get(0))
|
|
.unwrap();
|
|
assert_eq!(count, 0);
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_session_created_after_login() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_session_created_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "adminpass2";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_admin_user(&conn, "admin", &hash).unwrap();
|
|
}
|
|
|
|
let csrf_token = compute_sha256("csrf-token");
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_temp_csrf", csrf_token.clone()));
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert("user-agent", "test-agent".parse().unwrap());
|
|
let connect_info = Some(ConnectInfo(SocketAddr::new(
|
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
8080,
|
|
)));
|
|
let form = LoginForm {
|
|
username: "admin".to_string(),
|
|
password: password.to_string(),
|
|
csrf_token,
|
|
};
|
|
|
|
let response = login_post(
|
|
State(state.clone()),
|
|
jar.clone(),
|
|
headers,
|
|
connect_info,
|
|
Form(form),
|
|
)
|
|
.await;
|
|
assert!(response.status().is_redirection());
|
|
|
|
let conn = state.users_db.lock().unwrap();
|
|
let row_count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM sessions;", [], |row| row.get(0))
|
|
.unwrap();
|
|
assert_eq!(row_count, 1);
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_dashboard_access() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_dashboard_access_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "adminpass3";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
let user_id = {
|
|
let conn = state.users_db.lock().unwrap();
|
|
let user = create_admin_user(&conn, "admin", &hash).unwrap();
|
|
user.id
|
|
};
|
|
|
|
let session_token = "session_access_token";
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_session(
|
|
&conn,
|
|
session_token,
|
|
&user_id.to_string(),
|
|
&(Utc::now() + chrono::Duration::hours(1)).to_rfc3339(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
|
|
let response = dashboard_get(State(state.clone()), jar).await;
|
|
assert!(response.status().is_success());
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_standard_user_cannot_access_admin_panel() {
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bzod_test_standard_block_{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "userpass";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
let user_id = {
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_user(&conn, "regular_user", &hash, "standard", None)
|
|
.unwrap()
|
|
.id
|
|
};
|
|
|
|
let session_token = "standard_session_token";
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_session(
|
|
&conn,
|
|
session_token,
|
|
&user_id.to_string(),
|
|
&Utc::now().to_rfc3339(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
|
|
let response = dashboard_get(State(state.clone()), jar).await;
|
|
assert!(response.status().is_redirection());
|
|
assert!(response
|
|
.headers()
|
|
.get("location")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("/admin/login"));
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bootstrap_only_when_no_admin_exists() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_bootstrap_block_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let mut config = create_temp_config(temp_dir.clone());
|
|
config.bootstrap_password_sha256 = compute_sha256("bootstrap-secret");
|
|
let (_db, state) = build_state(config.clone());
|
|
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert("user-agent", "test-agent".parse().unwrap());
|
|
let connect_info = Some(ConnectInfo(SocketAddr::new(
|
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
8080,
|
|
)));
|
|
|
|
// Create an existing active admin, which should block bootstrap mode because the user count and admin count are already populated.
|
|
let _existing_admin = {
|
|
let conn = state.users_db.lock().unwrap();
|
|
let user = create_admin_user(
|
|
&conn,
|
|
"admin",
|
|
&bzod::auth::hash_password("admin-password").unwrap(),
|
|
)
|
|
.unwrap();
|
|
create_session(
|
|
&conn,
|
|
"existing_admin_session",
|
|
&user.id.to_string(),
|
|
&(Utc::now() + chrono::Duration::hours(1)).to_rfc3339(),
|
|
)
|
|
.unwrap();
|
|
user.id
|
|
};
|
|
|
|
let csrf_token = compute_sha256("csrf-token");
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_temp_csrf", csrf_token.clone()));
|
|
let form = LoginForm {
|
|
username: "admin".to_string(),
|
|
password: "bootstrap-secret".to_string(),
|
|
csrf_token,
|
|
};
|
|
let response = login_post(
|
|
State(state.clone()),
|
|
jar.clone(),
|
|
headers,
|
|
connect_info,
|
|
Form(form),
|
|
)
|
|
.await;
|
|
assert!(response.status().is_redirection());
|
|
assert!(response
|
|
.headers()
|
|
.get("location")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("/admin/login"));
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_system_account_cannot_access_admin_panel() {
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bzod_test_system_block_{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "systempass";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
let user_id = {
|
|
let conn = state.users_db.lock().unwrap();
|
|
conn.execute(
|
|
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES (?1, ?2, ?3, ?4, ?5);",
|
|
rusqlite::params!["system_user", hash, "active", Utc::now().to_rfc3339(), "system"],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
};
|
|
|
|
let session_token = "system_session_token";
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_session(
|
|
&conn,
|
|
session_token,
|
|
&user_id.to_string(),
|
|
&(Utc::now() + chrono::Duration::hours(1)).to_rfc3339(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
|
|
let response = dashboard_get(State(state.clone()), jar).await;
|
|
assert!(response.status().is_redirection());
|
|
assert!(response
|
|
.headers()
|
|
.get("location")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("/admin/login"));
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_reserved_username_rejected() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_reserved_username_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password_hash = bzod::auth::hash_password("test").unwrap();
|
|
let create_result = create_user(
|
|
&state.users_db.lock().unwrap(),
|
|
"admin",
|
|
&password_hash,
|
|
"standard",
|
|
None,
|
|
);
|
|
assert!(create_result.is_err());
|
|
|
|
let admin_result = create_admin_user(&state.users_db.lock().unwrap(), "admin", &password_hash);
|
|
assert!(admin_result.is_ok());
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_existing_installation_can_login_after_upgrade() {
|
|
let temp_dir =
|
|
std::env::temp_dir().join(format!("bzod_test_upgrade_login_{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
|
|
let admin_dir = temp_dir.join("admin");
|
|
std::fs::create_dir_all(&admin_dir).unwrap();
|
|
let users_db_path = admin_dir.join("users.db");
|
|
let mut users_conn = Connection::open(&users_db_path).unwrap();
|
|
run_migrations(&mut users_conn, "users", USERS_MIGRATIONS, None).unwrap();
|
|
users_conn.pragma_update(None, "user_version", 1).unwrap();
|
|
let password = "upgradetest";
|
|
let password_hash = bzod::auth::hash_password(password).unwrap();
|
|
users_conn
|
|
.execute(
|
|
"INSERT INTO users (username, password_hash, status, created_at, account_type) VALUES (?1, ?2, ?3, ?4, ?5);",
|
|
rusqlite::params!["admin", password_hash, "active", Utc::now().to_rfc3339(), "standard"],
|
|
)
|
|
.unwrap();
|
|
|
|
let _ = Db::init(&config).expect("Db::init should repair legacy install");
|
|
let (_db, state) = build_state(config.clone());
|
|
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
let user_count: i64 = conn
|
|
.query_row("SELECT COUNT(*) FROM users;", [], |row| row.get(0))
|
|
.unwrap();
|
|
let admin_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM users WHERE account_type = 'admin';",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
let active_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM users WHERE status = 'active';",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
println!(
|
|
"upgrade login counts users={}, admin={}, active={}",
|
|
user_count, admin_count, active_count
|
|
);
|
|
let mut stmt = conn
|
|
.prepare("SELECT username, status, account_type FROM users;")
|
|
.unwrap();
|
|
let mut rows = stmt.query([]).unwrap();
|
|
while let Some(row) = rows.next().unwrap() {
|
|
let username: String = row.get(0).unwrap();
|
|
let status: String = row.get(1).unwrap();
|
|
let account_type: String = row.get(2).unwrap();
|
|
println!("user row: {} {} {}", username, status, account_type);
|
|
}
|
|
}
|
|
|
|
let csrf_token = compute_sha256("csrf-token");
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_temp_csrf", csrf_token.clone()));
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert("user-agent", "test-agent".parse().unwrap());
|
|
let connect_info = Some(ConnectInfo(SocketAddr::new(
|
|
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
|
8080,
|
|
)));
|
|
let form = LoginForm {
|
|
username: "admin".to_string(),
|
|
password: password.to_string(),
|
|
csrf_token,
|
|
};
|
|
|
|
let response = login_post(
|
|
State(state.clone()),
|
|
jar.clone(),
|
|
headers,
|
|
connect_info,
|
|
Form(form),
|
|
)
|
|
.await;
|
|
assert!(response.status().is_redirection());
|
|
assert_eq!(
|
|
response.headers().get("location").unwrap(),
|
|
"/admin/dashboard"
|
|
);
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_expired_session_cannot_access_dashboard() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_expired_session_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (_db, state) = build_state(config);
|
|
|
|
let password = "adminpass4";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
let user_id = {
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_admin_user(&conn, "admin", &hash).unwrap().id
|
|
};
|
|
|
|
let session_token = "expired_session_token";
|
|
{
|
|
let conn = state.users_db.lock().unwrap();
|
|
create_session(
|
|
&conn,
|
|
session_token,
|
|
&user_id.to_string(),
|
|
&(Utc::now() - chrono::Duration::hours(1)).to_rfc3339(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
let jar = CookieJar::new().add(Cookie::new("bzod_session", session_token));
|
|
let response = dashboard_get(State(state.clone()), jar).await;
|
|
assert!(response.status().is_redirection());
|
|
assert!(response
|
|
.headers()
|
|
.get("location")
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("/admin/login"));
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_api_key_rejected_for_non_admin_user() {
|
|
let temp_dir = std::env::temp_dir().join(format!(
|
|
"bzod_test_api_key_non_admin_{}",
|
|
uuid::Uuid::new_v4()
|
|
));
|
|
std::fs::create_dir_all(&temp_dir).unwrap();
|
|
let config = create_temp_config(temp_dir.clone());
|
|
let (db, _state) = build_state(config);
|
|
|
|
let password = "standardpass";
|
|
let hash = bzod::auth::hash_password(password).unwrap();
|
|
let user_id = {
|
|
let conn = db.users.lock().unwrap();
|
|
create_user(&conn, "standard_user", &hash, "standard", None)
|
|
.unwrap()
|
|
.id
|
|
};
|
|
|
|
let key_secret = "nonadmin-api-key";
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(key_secret.as_bytes());
|
|
let hashed_key = hex::encode(hasher.finalize());
|
|
{
|
|
let conn = db.admin.lock().unwrap();
|
|
create_api_key(&conn, &user_id.to_string(), "test-key", &hashed_key).unwrap();
|
|
}
|
|
|
|
let auth_header = format!("Bearer {}", key_secret);
|
|
let admin_conn = db.admin.lock().unwrap();
|
|
let users_conn = db.users.lock().unwrap();
|
|
let auth_res = authenticate_api_key(&admin_conn, &users_conn, &auth_header).unwrap();
|
|
assert!(auth_res.is_none());
|
|
|
|
let _ = std::fs::remove_dir_all(&temp_dir);
|
|
}
|