use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Clone)]
pub struct TrustedProxies {
entries: Vec<(std::net::IpAddr, u8)>, }
impl TrustedProxies {
pub fn parse(input: &str) -> Self {
let mut entries = Vec::new();
for item in input.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
if let Some((addr_str, prefix_str)) = item.split_once('/') {
if let (Ok(addr), Ok(prefix)) = (
addr_str.parse::<std::net::IpAddr>(),
prefix_str.parse::<u8>(),
) {
let max_prefix = if addr.is_ipv4() { 32 } else { 128 };
if prefix <= max_prefix {
if prefix == 0 {
tracing::warn!(
value = %item,
"CIDR /0 matches ALL addresses in this family — all peers will be \
trusted proxies (X-Forwarded-For honored, IP-based rate limiting disabled)"
);
}
entries.push((addr, prefix));
} else {
tracing::warn!(value = %item, "Invalid CIDR prefix length, skipping");
}
} else {
tracing::warn!(value = %item, "Cannot parse CIDR, skipping");
}
} else if let Ok(addr) = item.parse::<std::net::IpAddr>() {
let prefix = if addr.is_ipv4() { 32 } else { 128 };
entries.push((addr, prefix));
} else {
tracing::warn!(value = %item, "Cannot parse IP address, skipping");
}
}
Self { entries }
}
pub fn default_loopback() -> Self {
Self {
entries: vec![
(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), 32),
(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), 128),
],
}
}
pub fn contains(&self, ip: std::net::IpAddr) -> bool {
self.entries.iter().any(|(network, prefix)| {
match (network, ip) {
(std::net::IpAddr::V4(net), std::net::IpAddr::V4(addr)) => {
if *prefix == 0 {
return true;
}
if *prefix >= 32 {
return *net == addr;
}
let net_bits = u32::from(*net);
let addr_bits = u32::from(addr);
let mask = u32::MAX << (32 - prefix);
(net_bits & mask) == (addr_bits & mask)
}
(std::net::IpAddr::V6(net), std::net::IpAddr::V6(addr)) => {
if *prefix == 0 {
return true;
}
if *prefix >= 128 {
return *net == addr;
}
let net_bits = u128::from(*net);
let addr_bits = u128::from(addr);
let mask = u128::MAX << (128 - prefix);
(net_bits & mask) == (addr_bits & mask)
}
_ => false, }
})
}
pub fn has_prefix_zero(&self) -> bool {
self.entries.iter().any(|(_, prefix)| *prefix == 0)
}
}
impl Default for TrustedProxies {
fn default() -> Self {
Self::default_loopback()
}
}
impl Serialize for TrustedProxies {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
let parts: Vec<String> = self
.entries
.iter()
.map(|(addr, prefix)| {
let max = if addr.is_ipv4() { 32 } else { 128 };
if *prefix == max {
addr.to_string()
} else {
format!("{}/{}", addr, prefix)
}
})
.collect();
parts.join(",").serialize(serializer)
}
}
impl<'de> Deserialize<'de> for TrustedProxies {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::parse(&s))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub anonymous_read: bool,
#[serde(default)]
pub docker_anon_pull: bool,
#[serde(default)]
pub public_web_ui: bool,
#[serde(default = "default_public_metrics")]
pub public_metrics: bool,
#[serde(default = "default_htpasswd_file")]
pub htpasswd_file: String,
#[serde(default = "default_token_storage")]
pub token_storage: String,
#[serde(default = "default_token_cache_ttl")]
pub token_cache_ttl: u64,
#[serde(default)]
pub trusted_proxies: TrustedProxies,
#[serde(default)]
pub oidc: OidcConfig,
#[serde(default)]
pub admin_users: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_oidc_leeway")]
pub leeway_secs: u64,
#[serde(default = "default_oidc_jwks_cache_secs")]
pub jwks_cache_secs: u64,
#[serde(default)]
pub providers: Vec<OidcProvider>,
}
impl Default for OidcConfig {
fn default() -> Self {
Self {
enabled: false,
leeway_secs: default_oidc_leeway(),
jwks_cache_secs: default_oidc_jwks_cache_secs(),
providers: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcProvider {
pub name: String,
pub issuer: String,
#[serde(default)]
pub jwks_uri: Option<String>,
#[serde(default)]
pub audience: String,
#[serde(default = "default_oidc_algorithms")]
pub algorithms: Vec<String>,
#[serde(default = "default_oidc_max_lifetime")]
pub max_token_lifetime_secs: u64,
#[serde(default = "default_namespace_scope")]
pub namespace_scope: Vec<String>,
#[serde(default)]
pub namespace_scope_enforcement: ScopeEnforcement,
#[serde(default = "super::default_true")]
pub enabled: bool,
#[serde(default)]
pub role_rules: Vec<OidcRoleRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcRoleRule {
pub pattern: String,
pub role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace_scope: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ScopeEnforcement {
#[default]
Enforce,
Audit,
}
pub(super) fn default_oidc_leeway() -> u64 {
60
}
pub(super) fn default_oidc_jwks_cache_secs() -> u64 {
3600
}
pub(super) fn default_oidc_algorithms() -> Vec<String> {
vec!["RS256".to_string(), "ES256".to_string()]
}
pub(super) fn default_oidc_max_lifetime() -> u64 {
900 }
pub(super) fn default_namespace_scope() -> Vec<String> {
vec!["*".to_string()]
}
fn default_public_metrics() -> bool {
true
}
pub(super) fn default_htpasswd_file() -> String {
"users.htpasswd".to_string()
}
pub(super) fn default_token_storage() -> String {
"data/tokens".to_string()
}
pub(super) fn default_token_cache_ttl() -> u64 {
300
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
enabled: false,
anonymous_read: false,
docker_anon_pull: false,
public_web_ui: false,
public_metrics: true,
htpasswd_file: "users.htpasswd".to_string(),
token_storage: "data/tokens".to_string(),
token_cache_ttl: 300,
trusted_proxies: TrustedProxies::default_loopback(),
oidc: OidcConfig::default(),
admin_users: Vec::new(),
}
}
}
impl AuthConfig {
pub(super) fn apply_env_overrides(&mut self) {
if let Ok(val) = env::var("NORA_AUTH_PUBLIC_WEB_UI") {
self.public_web_ui = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_PUBLIC_METRICS") {
self.public_metrics = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_ENABLED") {
self.enabled = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_ANONYMOUS_READ") {
self.anonymous_read = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_DOCKER_ANON_PULL") {
self.docker_anon_pull = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_HTPASSWD_FILE") {
self.htpasswd_file = val;
}
if let Ok(val) = env::var("NORA_AUTH_TOKEN_CACHE_TTL") {
if let Ok(secs) = val.parse() {
self.token_cache_ttl = secs;
}
}
if let Ok(val) = env::var("NORA_AUTH_TRUSTED_PROXIES") {
self.trusted_proxies = TrustedProxies::parse(&val);
}
if let Ok(val) = env::var("NORA_AUTH_ADMIN_USERS") {
self.admin_users = val
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
if let Ok(val) = env::var("NORA_AUTH_OIDC_ENABLED") {
self.oidc.enabled = val.to_lowercase() == "true" || val == "1";
}
if let Ok(val) = env::var("NORA_AUTH_TOKEN_STORAGE") {
self.token_storage = val;
}
}
}