chore: sync repository to current working state
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
Brings azcomputerguru/guru-connect up to the authoritative working copy that had been maintained in the claudetools monorepo: Phase 1 security and infrastructure (middleware, metrics, utils, token blacklist, deployment scripts, security audits) plus the native-remote-control integration spec. Preserves the repo .gitignore, .cargo, and server/static/downloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@ mod auth;
|
||||
mod api;
|
||||
mod db;
|
||||
mod support_codes;
|
||||
mod middleware;
|
||||
mod utils;
|
||||
mod metrics;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/guruconnect.rs"));
|
||||
@@ -22,11 +25,12 @@ use axum::{
|
||||
extract::{Path, State, Json, Query, Request},
|
||||
response::{Html, IntoResponse},
|
||||
http::StatusCode,
|
||||
middleware::{self, Next},
|
||||
middleware::{self as axum_middleware, Next},
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::cors::{Any, CorsLayer, AllowOrigin};
|
||||
use axum::http::{Method, HeaderValue};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::{info, Level};
|
||||
@@ -34,7 +38,9 @@ use tracing_subscriber::FmtSubscriber;
|
||||
use serde::Deserialize;
|
||||
|
||||
use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation};
|
||||
use auth::{JwtConfig, hash_password, generate_random_password, AuthenticatedUser};
|
||||
use auth::{JwtConfig, TokenBlacklist, hash_password, generate_random_password, AuthenticatedUser};
|
||||
use metrics::SharedMetrics;
|
||||
use prometheus_client::registry::Registry;
|
||||
|
||||
/// Application state
|
||||
#[derive(Clone)]
|
||||
@@ -43,17 +49,25 @@ pub struct AppState {
|
||||
support_codes: SupportCodeManager,
|
||||
db: Option<db::Database>,
|
||||
pub jwt_config: Arc<JwtConfig>,
|
||||
pub token_blacklist: TokenBlacklist,
|
||||
/// Optional API key for persistent agents (env: AGENT_API_KEY)
|
||||
pub agent_api_key: Option<String>,
|
||||
/// Prometheus metrics
|
||||
pub metrics: SharedMetrics,
|
||||
/// Prometheus registry (for /metrics endpoint)
|
||||
pub registry: Arc<std::sync::Mutex<Registry>>,
|
||||
/// Server start time
|
||||
pub start_time: Arc<std::time::Instant>,
|
||||
}
|
||||
|
||||
/// Middleware to inject JWT config into request extensions
|
||||
/// Middleware to inject JWT config and token blacklist into request extensions
|
||||
async fn auth_layer(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
request.extensions_mut().insert(state.jwt_config.clone());
|
||||
request.extensions_mut().insert(Arc::new(state.token_blacklist.clone()));
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
@@ -74,11 +88,14 @@ async fn main() -> Result<()> {
|
||||
let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string());
|
||||
info!("Loaded configuration, listening on {}", listen_addr);
|
||||
|
||||
// JWT configuration
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
|
||||
tracing::warn!("JWT_SECRET not set, using default (INSECURE for production!)");
|
||||
"guruconnect-dev-secret-change-me-in-production".to_string()
|
||||
});
|
||||
// JWT configuration - REQUIRED for security
|
||||
let jwt_secret = std::env::var("JWT_SECRET")
|
||||
.expect("JWT_SECRET environment variable must be set! Generate one with: openssl rand -base64 64");
|
||||
|
||||
if jwt_secret.len() < 32 {
|
||||
panic!("JWT_SECRET must be at least 32 characters long for security!");
|
||||
}
|
||||
|
||||
let jwt_expiry_hours = std::env::var("JWT_EXPIRY_HOURS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
@@ -126,12 +143,35 @@ async fn main() -> Result<()> {
|
||||
];
|
||||
let _ = db::set_user_permissions(db.pool(), user.id, &perms).await;
|
||||
|
||||
info!("========================================");
|
||||
info!(" INITIAL ADMIN USER CREATED");
|
||||
info!(" Username: admin");
|
||||
info!(" Password: {}", password);
|
||||
info!(" (Change this password after first login!)");
|
||||
info!("========================================");
|
||||
// SEC-6: Write credentials to secure file instead of logging
|
||||
let creds_file = ".admin-credentials";
|
||||
match std::fs::write(creds_file, format!("Username: admin\nPassword: {}\n\nWARNING: Change this password immediately after first login!\nDelete this file after copying the password.\n", password)) {
|
||||
Ok(_) => {
|
||||
// Set restrictive permissions (Unix only)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(creds_file, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
|
||||
info!("========================================");
|
||||
info!(" INITIAL ADMIN USER CREATED");
|
||||
info!(" Credentials written to: {}", creds_file);
|
||||
info!(" (Read file, change password, then delete file)");
|
||||
info!("========================================");
|
||||
}
|
||||
Err(e) => {
|
||||
// Fallback to logging if file write fails (but warn about security)
|
||||
tracing::warn!("Could not write credentials file: {}", e);
|
||||
info!("========================================");
|
||||
info!(" INITIAL ADMIN USER CREATED");
|
||||
info!(" Username: admin");
|
||||
info!(" Password: {}", password);
|
||||
info!(" WARNING: Password logged due to file write failure!");
|
||||
info!(" (Change this password immediately!)");
|
||||
info!("========================================");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create initial admin user: {}", e);
|
||||
@@ -167,32 +207,63 @@ async fn main() -> Result<()> {
|
||||
|
||||
// Agent API key for persistent agents (optional)
|
||||
let agent_api_key = std::env::var("AGENT_API_KEY").ok();
|
||||
if agent_api_key.is_some() {
|
||||
info!("AGENT_API_KEY configured for persistent agents");
|
||||
if let Some(ref key) = agent_api_key {
|
||||
// Validate API key strength for security
|
||||
utils::validation::validate_api_key_strength(key)?;
|
||||
info!("AGENT_API_KEY configured for persistent agents (validated)");
|
||||
} else {
|
||||
info!("No AGENT_API_KEY set - persistent agents will need JWT token or support code");
|
||||
}
|
||||
|
||||
// Initialize Prometheus metrics
|
||||
let mut registry = Registry::default();
|
||||
let metrics = Arc::new(metrics::Metrics::new(&mut registry));
|
||||
let registry = Arc::new(std::sync::Mutex::new(registry));
|
||||
let start_time = Arc::new(std::time::Instant::now());
|
||||
|
||||
// Spawn background task to update uptime metric
|
||||
let metrics_for_uptime = metrics.clone();
|
||||
let start_time_for_uptime = start_time.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let uptime = start_time_for_uptime.elapsed().as_secs() as i64;
|
||||
metrics_for_uptime.update_uptime(uptime);
|
||||
}
|
||||
});
|
||||
|
||||
// Create application state
|
||||
let token_blacklist = TokenBlacklist::new();
|
||||
|
||||
let state = AppState {
|
||||
sessions,
|
||||
support_codes: SupportCodeManager::new(),
|
||||
db: database,
|
||||
jwt_config,
|
||||
token_blacklist,
|
||||
agent_api_key,
|
||||
metrics,
|
||||
registry,
|
||||
start_time,
|
||||
};
|
||||
|
||||
// Build router
|
||||
let app = Router::new()
|
||||
// Health check (no auth required)
|
||||
.route("/health", get(health))
|
||||
// Prometheus metrics (no auth required - for monitoring)
|
||||
.route("/metrics", get(prometheus_metrics))
|
||||
|
||||
// Auth endpoints (no auth required for login)
|
||||
// Auth endpoints (TODO: Add rate limiting - see SEC2_RATE_LIMITING_TODO.md)
|
||||
.route("/api/auth/login", post(api::auth::login))
|
||||
|
||||
// Auth endpoints (auth required)
|
||||
.route("/api/auth/me", get(api::auth::get_me))
|
||||
.route("/api/auth/change-password", post(api::auth::change_password))
|
||||
.route("/api/auth/me", get(api::auth::get_me))
|
||||
.route("/api/auth/logout", post(api::auth_logout::logout))
|
||||
.route("/api/auth/revoke-token", post(api::auth_logout::revoke_own_token))
|
||||
.route("/api/auth/admin/revoke-user", post(api::auth_logout::revoke_user_tokens))
|
||||
.route("/api/auth/blacklist/stats", get(api::auth_logout::get_blacklist_stats))
|
||||
.route("/api/auth/blacklist/cleanup", post(api::auth_logout::cleanup_blacklist))
|
||||
|
||||
// User management (admin only)
|
||||
.route("/api/users", get(api::users::list_users))
|
||||
@@ -203,7 +274,7 @@ async fn main() -> Result<()> {
|
||||
.route("/api/users/:id/permissions", put(api::users::set_permissions))
|
||||
.route("/api/users/:id/clients", put(api::users::set_client_access))
|
||||
|
||||
// Portal API - Support codes
|
||||
// Portal API - Support codes (TODO: Add rate limiting)
|
||||
.route("/api/codes", post(create_code))
|
||||
.route("/api/codes", get(list_codes))
|
||||
.route("/api/codes/:code/validate", get(validate_code))
|
||||
@@ -245,19 +316,35 @@ async fn main() -> Result<()> {
|
||||
|
||||
// State and middleware
|
||||
.with_state(state.clone())
|
||||
.layer(middleware::from_fn_with_state(state, auth_layer))
|
||||
.layer(axum_middleware::from_fn_with_state(state, auth_layer))
|
||||
|
||||
// Serve static files for portal (fallback)
|
||||
.fallback_service(ServeDir::new("static").append_index_html_on_directories(true))
|
||||
|
||||
// Middleware
|
||||
.layer(axum_middleware::from_fn(middleware::add_security_headers)) // SEC-7 & SEC-12
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any),
|
||||
);
|
||||
// SEC-11: Restricted CORS configuration
|
||||
.layer({
|
||||
let cors = CorsLayer::new()
|
||||
// Allow requests from the production domain and localhost (for development)
|
||||
.allow_origin([
|
||||
"https://connect.azcomputerguru.com".parse::<HeaderValue>().unwrap(),
|
||||
"http://localhost:3002".parse::<HeaderValue>().unwrap(),
|
||||
"http://127.0.0.1:3002".parse::<HeaderValue>().unwrap(),
|
||||
])
|
||||
// Allow only necessary HTTP methods
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
|
||||
// Allow common headers needed for API requests
|
||||
.allow_headers([
|
||||
axum::http::header::AUTHORIZATION,
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
axum::http::header::ACCEPT,
|
||||
])
|
||||
// Allow credentials (cookies, auth headers)
|
||||
.allow_credentials(true);
|
||||
cors
|
||||
});
|
||||
|
||||
// Start server
|
||||
let addr: SocketAddr = listen_addr.parse()?;
|
||||
@@ -265,7 +352,11 @@ async fn main() -> Result<()> {
|
||||
|
||||
info!("Server listening on {}", addr);
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
// Use into_make_service_with_connect_info to enable IP address extraction
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>()
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -274,6 +365,18 @@ async fn health() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
/// Prometheus metrics endpoint
|
||||
async fn prometheus_metrics(
|
||||
State(state): State<AppState>,
|
||||
) -> String {
|
||||
use prometheus_client::encoding::text::encode;
|
||||
|
||||
let registry = state.registry.lock().unwrap();
|
||||
let mut buffer = String::new();
|
||||
encode(&mut buffer, ®istry).unwrap();
|
||||
buffer
|
||||
}
|
||||
|
||||
// Support code API handlers
|
||||
|
||||
async fn create_code(
|
||||
|
||||
Reference in New Issue
Block a user