Skip to content

Commit 1654b42

Browse files
authored
Merge pull request #4 from Steake/copilot/sub-pr-3
Address security, performance, and code quality issues from PR #3 review and fix admin console UI
2 parents 409943f + 5859ca5 commit 1654b42

14 files changed

Lines changed: 306 additions & 108 deletions

File tree

crates/bitcell-admin/README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,23 @@ bitcell-admin/
162162

163163
## Security Considerations
164164

165-
⚠️ **IMPORTANT**: The admin console provides powerful administrative capabilities. In production:
166-
167-
1. **Enable authentication** before exposing to network
168-
2. **Use HTTPS/TLS** for encrypted communication
169-
3. **Restrict access** via firewall rules or VPN
170-
4. **Use strong passwords** and rotate regularly
171-
5. **Enable audit logging** for all administrative actions
172-
6. **Limit API rate limits** to prevent abuse
165+
⚠️ **CRITICAL SECURITY WARNING** ⚠️
166+
167+
**NO AUTHENTICATION IS CURRENTLY IMPLEMENTED**
168+
169+
The admin console currently allows **unrestricted access** to all endpoints. This is a **critical security vulnerability**.
170+
171+
**DO NOT expose this admin console to any network (including localhost) in production without implementing authentication first.**
172+
173+
For production deployments, you MUST:
174+
175+
1. **Implement authentication** before exposing to any network
176+
2. **Use HTTPS/TLS** for all communication (never HTTP in production)
177+
3. **Restrict network access** via firewall rules, VPN, or IP allowlisting
178+
4. **Use strong passwords** and rotate them regularly
179+
5. **Enable comprehensive audit logging** for all administrative actions
180+
6. **Implement API rate limiting** to prevent abuse
181+
7. **Run with least-privilege** user accounts (never as root)
173182

174183
## Development
175184

crates/bitcell-admin/src/api/metrics.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,9 @@ pub async fn get_metrics(
7878
.await
7979
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
8080

81-
// Calculate system metrics (placeholder - would normally come from node metrics or system stats)
82-
let uptime_seconds = if let Some(first_node) = aggregated.node_metrics.first() {
83-
let duration = chrono::Utc::now().signed_duration_since(first_node.last_updated);
84-
duration.num_seconds().max(0) as u64
85-
} else {
86-
0
87-
};
81+
// Calculate system metrics
82+
// TODO: Track actual node start times to compute real uptime
83+
let uptime_seconds = 0u64; // Placeholder - requires node start time tracking
8884

8985
let response = MetricsResponse {
9086
chain: ChainMetrics {
@@ -100,20 +96,20 @@ pub async fn get_metrics(
10096
total_peers: aggregated.total_nodes * 10, // Estimate
10197
bytes_sent: aggregated.bytes_sent,
10298
bytes_received: aggregated.bytes_received,
103-
messages_sent: 0, // TODO: Add to node metrics
104-
messages_received: 0, // TODO: Add to node metrics
99+
messages_sent: 0, // TODO: Requires adding message_sent to node metrics
100+
messages_received: 0, // TODO: Requires adding message_received to node metrics
105101
},
106102
ebsl: EbslMetrics {
107103
active_miners: aggregated.active_miners,
108104
banned_miners: aggregated.banned_miners,
109-
average_trust_score: 0.85, // TODO: Calculate from actual data
110-
total_slashing_events: 0, // TODO: Add to node metrics
105+
average_trust_score: 0.85, // TODO: Requires adding trust scores to node metrics
106+
total_slashing_events: 0, // TODO: Requires adding slashing events to node metrics
111107
},
112108
system: SystemMetrics {
113109
uptime_seconds,
114-
cpu_usage: 0.0, // TODO: Add system metrics collection
115-
memory_usage_mb: 0, // TODO: Add system metrics collection
116-
disk_usage_mb: 0, // TODO: Add system metrics collection
110+
cpu_usage: 0.0, // TODO: Requires system metrics collection (e.g., sysinfo crate)
111+
memory_usage_mb: 0, // TODO: Requires system metrics collection
112+
disk_usage_mb: 0, // TODO: Requires system metrics collection
117113
},
118114
};
119115

crates/bitcell-admin/src/api/nodes.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ pub struct StartNodeRequest {
3232
pub config: Option<serde_json::Value>,
3333
}
3434

35+
/// Validate node ID format (alphanumeric, hyphens, and underscores only)
36+
fn validate_node_id(id: &str) -> Result<(), (StatusCode, Json<ErrorResponse>)> {
37+
if id.is_empty() || !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
38+
return Err((
39+
StatusCode::BAD_REQUEST,
40+
Json(ErrorResponse {
41+
error: "Invalid node ID format".to_string(),
42+
}),
43+
));
44+
}
45+
Ok(())
46+
}
47+
3548
/// List all registered nodes
3649
pub async fn list_nodes(
3750
State(state): State<Arc<AppState>>,
@@ -47,6 +60,8 @@ pub async fn get_node(
4760
State(state): State<Arc<AppState>>,
4861
Path(id): Path<String>,
4962
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
63+
validate_node_id(&id)?;
64+
5065
match state.process.get_node(&id) {
5166
Some(node) => Ok(Json(NodeResponse { node })),
5267
None => Err((
@@ -62,8 +77,21 @@ pub async fn get_node(
6277
pub async fn start_node(
6378
State(state): State<Arc<AppState>>,
6479
Path(id): Path<String>,
65-
Json(_req): Json<StartNodeRequest>,
80+
Json(req): Json<StartNodeRequest>,
6681
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
82+
validate_node_id(&id)?;
83+
84+
// Config is not supported yet
85+
if req.config.is_some() {
86+
tracing::warn!("Node '{}': Rejected start request with unsupported config", id);
87+
return Err((
88+
StatusCode::BAD_REQUEST,
89+
Json(ErrorResponse {
90+
error: "Custom config is not supported yet".to_string(),
91+
}),
92+
));
93+
}
94+
6795
match state.process.start_node(&id) {
6896
Ok(node) => {
6997
tracing::info!("Started node '{}' successfully", id);
@@ -83,6 +111,8 @@ pub async fn stop_node(
83111
State(state): State<Arc<AppState>>,
84112
Path(id): Path<String>,
85113
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
114+
validate_node_id(&id)?;
115+
86116
match state.process.stop_node(&id) {
87117
Ok(node) => {
88118
tracing::info!("Stopped node '{}' successfully", id);

crates/bitcell-admin/src/api/setup.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
99
use std::sync::Arc;
1010

1111
use crate::AppState;
12-
use crate::setup::NodeEndpoint;
12+
use crate::setup::{NodeEndpoint, SETUP_FILE_PATH};
1313

1414
#[derive(Debug, Serialize)]
1515
pub struct SetupStatusResponse {
@@ -68,7 +68,7 @@ pub async fn add_node(
6868
state.setup.add_node(node.clone());
6969

7070
// Save setup state
71-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
71+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
7272
state.setup.save_to_file(&setup_path)
7373
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
7474

@@ -87,7 +87,7 @@ pub async fn set_config_path(
8787
state.setup.set_config_path(path.clone());
8888

8989
// Save setup state
90-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
90+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
9191
state.setup.save_to_file(&setup_path)
9292
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
9393

@@ -101,17 +101,30 @@ pub async fn set_data_dir(
101101
) -> Result<Json<String>, (StatusCode, Json<String>)> {
102102
let path = std::path::PathBuf::from(&req.path);
103103

104-
// Create directory if it doesn't exist
104+
// Create directory if it doesn't exist with restrictive permissions
105105
std::fs::create_dir_all(&path)
106106
.map_err(|e| (
107107
StatusCode::INTERNAL_SERVER_ERROR,
108108
Json(format!("Failed to create data directory: {}", e))
109109
))?;
110110

111+
// Set restrictive permissions on Unix systems (0700 - owner only)
112+
#[cfg(unix)]
113+
{
114+
use std::os::unix::fs::PermissionsExt;
115+
let permissions = std::fs::Permissions::from_mode(0o700);
116+
std::fs::set_permissions(&path, permissions)
117+
.map_err(|e| (
118+
StatusCode::INTERNAL_SERVER_ERROR,
119+
Json(format!("Failed to set directory permissions: {}", e))
120+
))?;
121+
tracing::info!("Set data directory permissions to 0700 (owner only)");
122+
}
123+
111124
state.setup.set_data_dir(path);
112125

113126
// Save setup state
114-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
127+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
115128
state.setup.save_to_file(&setup_path)
116129
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
117130

@@ -125,7 +138,7 @@ pub async fn complete_setup(
125138
state.setup.mark_initialized();
126139

127140
// Save setup state
128-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
141+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
129142
state.setup.save_to_file(&setup_path)
130143
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
131144

crates/bitcell-admin/src/api/test.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,16 @@ pub async fn run_battle_test(
114114

115115
let (outcome, energy_a, energy_b) = tokio::task::spawn_blocking(move || {
116116
// Simulate the battle
117-
let outcome = battle.simulate()
118-
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
117+
let outcome = battle.simulate();
119118

120119
// Get final grid to measure energies
121120
let final_grid = battle.final_grid();
122121
let (energy_a, energy_b) = battle.measure_regional_energy(&final_grid);
123122

124-
Ok::<_, String>((outcome, energy_a, energy_b))
123+
(outcome, energy_a, energy_b)
125124
})
126125
.await
127-
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
128-
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
126+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?;
129127

130128
let duration = start.elapsed();
131129

@@ -222,8 +220,7 @@ pub async fn run_battle_visualization(
222220
// Run simulation and capture frames
223221
let (outcome, frames) = tokio::task::spawn_blocking(move || {
224222
// Get outcome
225-
let outcome = battle.simulate()
226-
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
223+
let outcome = battle.simulate();
227224

228225
// Get grid states at sample steps
229226
let grids = battle.grid_states(&sample_steps);
@@ -243,11 +240,10 @@ pub async fn run_battle_visualization(
243240
});
244241
}
245242

246-
Ok::<_, String>((outcome, frames))
243+
(outcome, frames)
247244
})
248245
.await
249-
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
250-
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
246+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?;
251247

252248
let winner = match outcome {
253249
BattleOutcome::AWins => "glider_a".to_string(),

crates/bitcell-admin/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub use api::AdminApi;
3030
pub use deployment::DeploymentManager;
3131
pub use config::ConfigManager;
3232
pub use process::ProcessManager;
33+
pub use setup::SETUP_FILE_PATH;
3334

3435
/// Administrative console server
3536
pub struct AdminConsole {
@@ -50,7 +51,7 @@ impl AdminConsole {
5051
let setup = Arc::new(setup::SetupManager::new());
5152

5253
// Try to load setup state from default location
53-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
54+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
5455
if let Err(e) = setup.load_from_file(&setup_path) {
5556
tracing::warn!("Failed to load setup state: {}", e);
5657
}
@@ -112,7 +113,9 @@ impl AdminConsole {
112113
// Static files
113114
.nest_service("/static", ServeDir::new("static"))
114115

115-
// CORS
116+
// CORS - WARNING: Permissive CORS allows requests from any origin.
117+
// This is only suitable for local development. For production,
118+
// configure specific allowed origins to prevent CSRF attacks.
116119
.layer(CorsLayer::permissive())
117120

118121
// State

crates/bitcell-admin/src/main.rs

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! BitCell Admin Console - Main Entry Point
22
3-
use bitcell_admin::{AdminConsole, process::{ProcessManager, NodeConfig}, api::NodeType};
3+
use bitcell_admin::AdminConsole;
44
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
55

66
#[tokio::main]
@@ -23,47 +23,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
2323
.parse()?;
2424

2525
let console = AdminConsole::new(addr);
26-
let process_mgr = console.process_manager();
2726

28-
// Register some sample nodes for demonstration
29-
register_sample_nodes(&process_mgr);
30-
31-
tracing::info!("Admin console ready - registered {} nodes", process_mgr.list_nodes().len());
27+
tracing::info!("Admin console ready");
3228
tracing::info!("Dashboard available at http://{}", addr);
3329

3430
console.serve().await?;
3531

3632
Ok(())
3733
}
38-
39-
fn register_sample_nodes(process: &ProcessManager) {
40-
// Register sample validator nodes
41-
for i in 1..=3 {
42-
let config = NodeConfig {
43-
node_type: NodeType::Validator,
44-
data_dir: format!("/tmp/bitcell/validator-{}", i),
45-
port: 9000 + i as u16,
46-
rpc_port: 10000 + i as u16,
47-
log_level: "info".to_string(),
48-
network: "testnet".to_string(),
49-
};
50-
51-
process.register_node(format!("validator-{}", i), config);
52-
tracing::info!("Registered validator-{}", i);
53-
}
54-
55-
// Register sample miner nodes
56-
for i in 1..=2 {
57-
let config = NodeConfig {
58-
node_type: NodeType::Miner,
59-
data_dir: format!("/tmp/bitcell/miner-{}", i),
60-
port: 9100 + i as u16,
61-
rpc_port: 10100 + i as u16,
62-
log_level: "info".to_string(),
63-
network: "testnet".to_string(),
64-
};
65-
66-
process.register_node(format!("miner-{}", i), config);
67-
tracing::info!("Registered miner-{}", i);
68-
}
69-
}

crates/bitcell-admin/src/metrics_client.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ impl MetricsClient {
3131
pub fn new() -> Self {
3232
Self {
3333
client: reqwest::Client::builder()
34-
.timeout(Duration::from_secs(5))
34+
.timeout(Duration::from_secs(10))
3535
.build()
36-
.unwrap(),
36+
.expect("Failed to build HTTP client for metrics"),
3737
}
3838
}
3939

@@ -58,6 +58,9 @@ impl MetricsClient {
5858
}
5959

6060
/// Parse Prometheus metrics format
61+
/// NOTE: This is a basic parser that only handles simple "metric_name value" format.
62+
/// It does NOT support metric labels (e.g., metric{label="value"}).
63+
/// For production use, consider using a proper Prometheus parsing library.
6164
fn parse_prometheus_metrics(&self, node_id: &str, endpoint: &str, text: &str) -> Result<NodeMetrics, String> {
6265
let mut metrics = HashMap::new();
6366

0 commit comments

Comments
 (0)