security(dashboard): enforce admin authentication on all sensitive endpoints
This commit adds the missing auth::require_admin check to all analytics, system info, and configuration list endpoints. It also improves error logging in the usage summary handler to aid in troubleshooting 'Failed to load statistics' errors.
This commit is contained in:
@@ -33,7 +33,15 @@ pub(super) struct UpdateClientPayload {
|
|||||||
pub(super) rate_limit_per_minute: Option<i64>,
|
pub(super) rate_limit_per_minute: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_get_clients(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_get_clients(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
@@ -321,8 +329,14 @@ pub(super) async fn handle_delete_client(
|
|||||||
|
|
||||||
pub(super) async fn handle_client_usage(
|
pub(super) async fn handle_client_usage(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
// Get per-model breakdown for this client
|
// Get per-model breakdown for this client
|
||||||
@@ -381,8 +395,14 @@ pub(super) async fn handle_client_usage(
|
|||||||
|
|
||||||
pub(super) async fn handle_get_client_tokens(
|
pub(super) async fn handle_get_client_tokens(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
|
|||||||
@@ -43,8 +43,14 @@ pub(super) struct ModelListParams {
|
|||||||
|
|
||||||
pub(super) async fn handle_get_models(
|
pub(super) async fn handle_get_models(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(params): Query<ModelListParams>,
|
Query(params): Query<ModelListParams>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let registry = &state.app_state.model_registry;
|
let registry = &state.app_state.model_registry;
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,15 @@ pub(super) struct UpdateProviderRequest {
|
|||||||
pub(super) billing_mode: Option<String>,
|
pub(super) billing_mode: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_get_providers(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_get_providers(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let registry = &state.app_state.model_registry;
|
let registry = &state.app_state.model_registry;
|
||||||
let config = &state.app_state.config;
|
let config = &state.app_state.config;
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
@@ -154,8 +162,14 @@ pub(super) async fn handle_get_providers(State(state): State<DashboardState>) ->
|
|||||||
|
|
||||||
pub(super) async fn handle_get_provider(
|
pub(super) async fn handle_get_provider(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let registry = &state.app_state.model_registry;
|
let registry = &state.app_state.model_registry;
|
||||||
let config = &state.app_state.config;
|
let config = &state.app_state.config;
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
@@ -351,8 +365,14 @@ pub(super) async fn handle_update_provider(
|
|||||||
|
|
||||||
pub(super) async fn handle_test_provider(
|
pub(super) async fn handle_test_provider(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
let provider = match state.app_state.provider_manager.get_provider(&name).await {
|
let provider = match state.app_state.provider_manager.get_provider(&name).await {
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ fn read_proc_file(path: &str) -> Option<String> {
|
|||||||
std::fs::read_to_string(path).ok()
|
std::fs::read_to_string(path).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_system_health(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_system_health(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let mut components = HashMap::new();
|
let mut components = HashMap::new();
|
||||||
components.insert("api_server".to_string(), "online".to_string());
|
components.insert("api_server".to_string(), "online".to_string());
|
||||||
components.insert("database".to_string(), "online".to_string());
|
components.insert("database".to_string(), "online".to_string());
|
||||||
@@ -67,7 +75,13 @@ pub(super) async fn handle_system_health(State(state): State<DashboardState>) ->
|
|||||||
/// Real system metrics from /proc (Linux only).
|
/// Real system metrics from /proc (Linux only).
|
||||||
pub(super) async fn handle_system_metrics(
|
pub(super) async fn handle_system_metrics(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
// --- CPU usage (aggregate across all cores) ---
|
// --- CPU usage (aggregate across all cores) ---
|
||||||
// /proc/stat first line: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
// /proc/stat first line: cpu user nice system idle iowait irq softirq steal guest guest_nice
|
||||||
let cpu_percent = read_proc_file("/proc/stat")
|
let cpu_percent = read_proc_file("/proc/stat")
|
||||||
@@ -220,7 +234,15 @@ pub(super) async fn handle_system_metrics(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_system_logs(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_system_logs(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
@@ -318,7 +340,15 @@ pub(super) async fn handle_system_backup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_get_settings(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
pub(super) async fn handle_get_settings(
|
||||||
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let registry = &state.app_state.model_registry;
|
let registry = &state.app_state.model_registry;
|
||||||
let provider_count = registry.providers.len();
|
let provider_count = registry.providers.len();
|
||||||
let model_count: usize = registry.providers.values().map(|p| p.models.len()).sum();
|
let model_count: usize = registry.providers.values().map(|p| p.models.len()).sum();
|
||||||
|
|||||||
@@ -71,8 +71,14 @@ impl UsagePeriodFilter {
|
|||||||
|
|
||||||
pub(super) async fn handle_usage_summary(
|
pub(super) async fn handle_usage_summary(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
|
|
||||||
@@ -133,7 +139,9 @@ pub(super) async fn handle_usage_summary(
|
|||||||
)
|
)
|
||||||
.fetch_one(pool);
|
.fetch_one(pool);
|
||||||
|
|
||||||
match tokio::join!(total_stats, today_stats, error_stats, avg_response) {
|
let results = tokio::join!(total_stats, today_stats, error_stats, avg_response);
|
||||||
|
|
||||||
|
match results {
|
||||||
(Ok(t), Ok(d), Ok(e), Ok(a)) => {
|
(Ok(t), Ok(d), Ok(e), Ok(a)) => {
|
||||||
let total_requests: i64 = t.get("total_requests");
|
let total_requests: i64 = t.get("total_requests");
|
||||||
let total_tokens: i64 = t.get("total_tokens");
|
let total_tokens: i64 = t.get("total_tokens");
|
||||||
@@ -168,14 +176,26 @@ pub(super) async fn handle_usage_summary(
|
|||||||
"total_cache_write_tokens": total_cache_write,
|
"total_cache_write_tokens": total_cache_write,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
_ => Json(ApiResponse::error("Failed to fetch usage statistics".to_string())),
|
(t_res, d_res, e_res, a_res) => {
|
||||||
|
if let Err(e) = t_res { warn!("Total stats query failed: {}", e); }
|
||||||
|
if let Err(e) = d_res { warn!("Today stats query failed: {}", e); }
|
||||||
|
if let Err(e) = e_res { warn!("Error stats query failed: {}", e); }
|
||||||
|
if let Err(e) = a_res { warn!("Avg response query failed: {}", e); }
|
||||||
|
Json(ApiResponse::error("Failed to fetch usage statistics".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn handle_time_series(
|
pub(super) async fn handle_time_series(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
let granularity = filter.granularity();
|
let granularity = filter.granularity();
|
||||||
@@ -248,8 +268,14 @@ pub(super) async fn handle_time_series(
|
|||||||
|
|
||||||
pub(super) async fn handle_clients_usage(
|
pub(super) async fn handle_clients_usage(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
|
|
||||||
@@ -308,8 +334,14 @@ pub(super) async fn handle_clients_usage(
|
|||||||
|
|
||||||
pub(super) async fn handle_providers_usage(
|
pub(super) async fn handle_providers_usage(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
|
|
||||||
@@ -370,8 +402,14 @@ pub(super) async fn handle_providers_usage(
|
|||||||
|
|
||||||
pub(super) async fn handle_detailed_usage(
|
pub(super) async fn handle_detailed_usage(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
|
|
||||||
@@ -433,8 +471,14 @@ pub(super) async fn handle_detailed_usage(
|
|||||||
|
|
||||||
pub(super) async fn handle_analytics_breakdown(
|
pub(super) async fn handle_analytics_breakdown(
|
||||||
State(state): State<DashboardState>,
|
State(state): State<DashboardState>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
Query(filter): Query<UsagePeriodFilter>,
|
Query(filter): Query<UsagePeriodFilter>,
|
||||||
) -> Json<ApiResponse<serde_json::Value>> {
|
) -> Json<ApiResponse<serde_json::Value>> {
|
||||||
|
let (_session, _) = match super::auth::require_admin(&state, &headers).await {
|
||||||
|
Ok((session, new_token)) => (session, new_token),
|
||||||
|
Err(e) => return e,
|
||||||
|
};
|
||||||
|
|
||||||
let pool = &state.app_state.db_pool;
|
let pool = &state.app_state.db_pool;
|
||||||
let (period_clause, period_binds) = filter.to_sql();
|
let (period_clause, period_binds) = filter.to_sql();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user