mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 16:10:31 +00:00
refactor: extract basic_auth_header helper, add plaintext credential warnings
- basic_auth_header() in config.rs replaces 6 inline STANDARD.encode calls - warn_plaintext_credentials() logs warning at startup if auth is in config.toml - All protocol handlers use shared helper instead of duplicating base64 logic
This commit is contained in:
@@ -1,12 +1,19 @@
|
|||||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
pub use crate::secrets::SecretsConfig;
|
pub use crate::secrets::SecretsConfig;
|
||||||
|
|
||||||
|
/// Encode "user:pass" into a Basic Auth header value, e.g. "Basic dXNlcjpwYXNz".
|
||||||
|
/// Returns None if input is None.
|
||||||
|
pub fn basic_auth_header(credentials: &str) -> String {
|
||||||
|
format!("Basic {}", STANDARD.encode(credentials))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub server: ServerConfig,
|
pub server: ServerConfig,
|
||||||
@@ -348,6 +355,37 @@ impl Default for RateLimitConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// Warn if credentials are configured via config.toml (not env vars)
|
||||||
|
pub fn warn_plaintext_credentials(&self) {
|
||||||
|
// Docker upstreams
|
||||||
|
for (i, upstream) in self.docker.upstreams.iter().enumerate() {
|
||||||
|
if upstream.auth.is_some() && std::env::var("NORA_DOCKER_UPSTREAMS").is_err() {
|
||||||
|
tracing::warn!(
|
||||||
|
upstream_index = i,
|
||||||
|
url = %upstream.url,
|
||||||
|
"Docker upstream credentials in config.toml are plaintext — consider NORA_DOCKER_UPSTREAMS env var"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Maven proxies
|
||||||
|
for proxy in &self.maven.proxies {
|
||||||
|
if proxy.auth().is_some() && std::env::var("NORA_MAVEN_PROXIES").is_err() {
|
||||||
|
tracing::warn!(
|
||||||
|
url = %proxy.url(),
|
||||||
|
"Maven proxy credentials in config.toml are plaintext — consider NORA_MAVEN_PROXIES env var"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// npm
|
||||||
|
if self.npm.proxy_auth.is_some() && std::env::var("NORA_NPM_PROXY_AUTH").is_err() {
|
||||||
|
tracing::warn!("npm proxy credentials in config.toml are plaintext — consider NORA_NPM_PROXY_AUTH env var");
|
||||||
|
}
|
||||||
|
// PyPI
|
||||||
|
if self.pypi.proxy_auth.is_some() && std::env::var("NORA_PYPI_PROXY_AUTH").is_err() {
|
||||||
|
tracing::warn!("PyPI proxy credentials in config.toml are plaintext — consider NORA_PYPI_PROXY_AUTH env var");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Load configuration with priority: ENV > config.toml > defaults
|
/// Load configuration with priority: ENV > config.toml > defaults
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
// 1. Start with defaults
|
// 1. Start with defaults
|
||||||
|
|||||||
@@ -289,6 +289,9 @@ async fn run_server(config: Config, storage: Storage) {
|
|||||||
let storage_path = config.storage.path.clone();
|
let storage_path = config.storage.path.clone();
|
||||||
let rate_limit_enabled = config.rate_limit.enabled;
|
let rate_limit_enabled = config.rate_limit.enabled;
|
||||||
|
|
||||||
|
// Warn about plaintext credentials in config.toml
|
||||||
|
config.warn_plaintext_credentials();
|
||||||
|
|
||||||
// Initialize Docker auth with proxy timeout
|
// Initialize Docker auth with proxy timeout
|
||||||
let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout);
|
let docker_auth = registry::DockerAuth::new(config.docker.proxy_timeout);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::activity_log::{ActionType, ActivityEntry};
|
use crate::activity_log::{ActionType, ActivityEntry};
|
||||||
use crate::audit::AuditEntry;
|
use crate::audit::AuditEntry;
|
||||||
|
use crate::config::basic_auth_header;
|
||||||
use crate::registry::docker_auth::DockerAuth;
|
use crate::registry::docker_auth::DockerAuth;
|
||||||
use crate::storage::Storage;
|
use crate::storage::Storage;
|
||||||
use crate::validation::{validate_digest, validate_docker_name, validate_docker_reference};
|
use crate::validation::{validate_digest, validate_docker_name, validate_docker_reference};
|
||||||
@@ -15,7 +16,6 @@ use axum::{
|
|||||||
routing::{delete, get, head, patch, put},
|
routing::{delete, get, head, patch, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -748,8 +748,7 @@ async fn fetch_blob_from_upstream(
|
|||||||
// First try — with basic auth if configured
|
// First try — with basic auth if configured
|
||||||
let mut request = client.get(&url).timeout(Duration::from_secs(timeout));
|
let mut request = client.get(&url).timeout(Duration::from_secs(timeout));
|
||||||
if let Some(credentials) = basic_auth {
|
if let Some(credentials) = basic_auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
@@ -817,8 +816,7 @@ async fn fetch_manifest_from_upstream(
|
|||||||
.timeout(Duration::from_secs(timeout))
|
.timeout(Duration::from_secs(timeout))
|
||||||
.header("Accept", accept_header);
|
.header("Accept", accept_header);
|
||||||
if let Some(credentials) = basic_auth {
|
if let Some(credentials) = basic_auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|e| {
|
let response = request.send().await.map_err(|e| {
|
||||||
tracing::error!(error = %e, url = %url, "Failed to send request to upstream");
|
tracing::error!(error = %e, url = %url, "Failed to send request to upstream");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
use crate::config::basic_auth_header;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -91,8 +91,7 @@ impl DockerAuth {
|
|||||||
|
|
||||||
let mut request = self.client.get(&url);
|
let mut request = self.client.get(&url);
|
||||||
if let Some(credentials) = basic_auth {
|
if let Some(credentials) = basic_auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
tracing::debug!("Using basic auth for token request");
|
tracing::debug!("Using basic auth for token request");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +122,7 @@ impl DockerAuth {
|
|||||||
// First try — with basic auth if configured, otherwise anonymous
|
// First try — with basic auth if configured, otherwise anonymous
|
||||||
let mut request = self.client.get(url);
|
let mut request = self.client.get(url);
|
||||||
if let Some(credentials) = basic_auth {
|
if let Some(credentials) = basic_auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::activity_log::{ActionType, ActivityEntry};
|
use crate::activity_log::{ActionType, ActivityEntry};
|
||||||
use crate::audit::AuditEntry;
|
use crate::audit::AuditEntry;
|
||||||
|
use crate::config::basic_auth_header;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
@@ -12,7 +13,6 @@ use axum::{
|
|||||||
routing::{get, put},
|
routing::{get, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -136,8 +136,7 @@ async fn fetch_from_proxy(
|
|||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
if let Some(credentials) = auth {
|
if let Some(credentials) = auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::activity_log::{ActionType, ActivityEntry};
|
use crate::activity_log::{ActionType, ActivityEntry};
|
||||||
use crate::audit::AuditEntry;
|
use crate::audit::AuditEntry;
|
||||||
|
use crate::config::basic_auth_header;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
@@ -12,7 +13,6 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -108,8 +108,7 @@ async fn fetch_from_proxy(
|
|||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
if let Some(credentials) = auth {
|
if let Some(credentials) = auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::activity_log::{ActionType, ActivityEntry};
|
use crate::activity_log::{ActionType, ActivityEntry};
|
||||||
use crate::audit::AuditEntry;
|
use crate::audit::AuditEntry;
|
||||||
|
use crate::config::basic_auth_header;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
@@ -11,7 +12,6 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -217,8 +217,7 @@ async fn fetch_package_page(
|
|||||||
.timeout(Duration::from_secs(timeout_secs))
|
.timeout(Duration::from_secs(timeout_secs))
|
||||||
.header("Accept", "text/html");
|
.header("Accept", "text/html");
|
||||||
if let Some(credentials) = auth {
|
if let Some(credentials) = auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
@@ -238,8 +237,7 @@ async fn fetch_file(
|
|||||||
) -> Result<Vec<u8>, ()> {
|
) -> Result<Vec<u8>, ()> {
|
||||||
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
let mut request = client.get(url).timeout(Duration::from_secs(timeout_secs));
|
||||||
if let Some(credentials) = auth {
|
if let Some(credentials) = auth {
|
||||||
let encoded = STANDARD.encode(credentials);
|
request = request.header("Authorization", basic_auth_header(credentials));
|
||||||
request = request.header("Authorization", format!("Basic {}", encoded));
|
|
||||||
}
|
}
|
||||||
let response = request.send().await.map_err(|_| ())?;
|
let response = request.send().await.map_err(|_| ())?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user