#[cfg(server)]
mod inner {
use reinhardt_conf::settings::fragment::{HasSettings, SettingsFragment, SettingsValidation};
use reinhardt_conf::settings::profile::Profile;
use reinhardt_conf::settings::validation::ValidationResult;
use serde::{Deserialize, Serialize};
fn default_site_title() -> String {
"Reinhardt Admin".to_string()
}
fn default_site_header() -> String {
"Administration".to_string()
}
fn default_list_per_page() -> usize {
100
}
fn default_login_url() -> String {
"/admin/login".to_string()
}
fn default_logout_url() -> String {
"/admin/logout".to_string()
}
fn default_self_only() -> Vec<String> {
vec!["'self'".to_string()]
}
fn default_script_src() -> Vec<String> {
vec!["'self'".to_string(), "'wasm-unsafe-eval'".to_string()]
}
fn default_style_src() -> Vec<String> {
vec!["'self'".to_string(), "'unsafe-inline'".to_string()]
}
fn default_img_src() -> Vec<String> {
vec!["'self'".to_string(), "data:".to_string()]
}
fn default_frame_ancestors() -> Vec<String> {
vec!["'none'".to_string()]
}
fn default_frame_options() -> String {
"deny".to_string()
}
fn default_referrer_policy() -> String {
"strict-origin-when-cross-origin".to_string()
}
fn default_permissions_policy() -> String {
"camera=(), microphone=(), geolocation=(), payment=()".to_string()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdminCspSettings {
#[serde(default = "default_self_only")]
pub default_src: Vec<String>,
#[serde(default = "default_script_src")]
pub script_src: Vec<String>,
#[serde(default = "default_style_src")]
pub style_src: Vec<String>,
#[serde(default = "default_img_src")]
pub img_src: Vec<String>,
#[serde(default = "default_self_only")]
pub font_src: Vec<String>,
#[serde(default = "default_self_only")]
pub connect_src: Vec<String>,
#[serde(default = "default_frame_ancestors")]
pub frame_ancestors: Vec<String>,
#[serde(default = "default_self_only")]
pub base_uri: Vec<String>,
#[serde(default = "default_self_only")]
pub form_action: Vec<String>,
}
impl Default for AdminCspSettings {
fn default() -> Self {
Self {
default_src: default_self_only(),
script_src: default_script_src(),
style_src: default_style_src(),
img_src: default_img_src(),
font_src: default_self_only(),
connect_src: default_self_only(),
frame_ancestors: default_frame_ancestors(),
base_uri: default_self_only(),
form_action: default_self_only(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdminSecuritySettings {
#[serde(default = "default_frame_options")]
pub frame_options: String,
#[serde(default = "default_referrer_policy")]
pub referrer_policy: String,
#[serde(default = "default_permissions_policy")]
pub permissions_policy: String,
}
impl Default for AdminSecuritySettings {
fn default() -> Self {
Self {
frame_options: default_frame_options(),
referrer_policy: default_referrer_policy(),
permissions_policy: default_permissions_policy(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AdminSettings {
#[serde(default = "default_site_title")]
pub site_title: String,
#[serde(default = "default_site_header")]
pub site_header: String,
#[serde(default = "default_list_per_page")]
pub list_per_page: usize,
#[serde(default = "default_login_url")]
pub login_url: String,
#[serde(default = "default_logout_url")]
pub logout_url: String,
#[serde(default)]
pub csp: AdminCspSettings,
#[serde(default)]
pub security: AdminSecuritySettings,
}
impl Default for AdminSettings {
fn default() -> Self {
Self {
site_title: default_site_title(),
site_header: default_site_header(),
list_per_page: default_list_per_page(),
login_url: default_login_url(),
logout_url: default_logout_url(),
csp: AdminCspSettings::default(),
security: AdminSecuritySettings::default(),
}
}
}
impl SettingsValidation for AdminSettings {
fn validate(&self, _profile: &Profile) -> ValidationResult {
self.warn_csp_misconfigurations();
self.warn_security_misconfigurations();
Ok(())
}
}
impl SettingsFragment for AdminSettings {
type Accessor = dyn HasAdminSettings;
fn section() -> &'static str {
"admin"
}
fn validate(&self, profile: &Profile) -> ValidationResult {
<Self as SettingsValidation>::validate(self, profile)
}
}
pub trait HasAdminSettings {
fn admin(&self) -> &AdminSettings;
}
impl<T: HasSettings<AdminSettings>> HasAdminSettings for T {
fn admin(&self) -> &AdminSettings {
self.get_settings()
}
}
use crate::server::security::{ContentSecurityPolicy, SecurityHeaders};
impl AdminSettings {
pub fn to_security_headers(&self) -> SecurityHeaders {
SecurityHeaders {
csp: ContentSecurityPolicy {
default_src: self.csp.default_src.clone(),
script_src: self.csp.script_src.clone(),
style_src: self.csp.style_src.clone(),
img_src: self.csp.img_src.clone(),
font_src: self.csp.font_src.clone(),
connect_src: self.csp.connect_src.clone(),
frame_ancestors: self.csp.frame_ancestors.clone(),
base_uri: self.csp.base_uri.clone(),
form_action: self.csp.form_action.clone(),
},
frame_options: self.security.frame_options.parse().unwrap(),
referrer_policy: self.security.referrer_policy.parse().unwrap(),
permissions_policy: self.security.permissions_policy.clone(),
}
}
fn warn_csp_misconfigurations(&self) {
if !self.csp.default_src.iter().any(|s| s == "'self'") {
tracing::warn!(
"Admin CSP: default_src is missing `'self'`, this may block admin resources"
);
}
if !self.csp.script_src.iter().any(|s| s == "'self'") {
tracing::warn!(
"Admin CSP: script_src is missing `'self'`, admin panel assets may not load"
);
}
if !self
.csp
.script_src
.iter()
.any(|s| s == "'wasm-unsafe-eval'")
{
tracing::warn!(
"Admin CSP: script_src is missing `'wasm-unsafe-eval'`, WASM SPA will not function"
);
}
if !self.csp.style_src.iter().any(|s| s == "'self'") {
tracing::warn!(
"Admin CSP: style_src is missing `'self'`, admin styles may not load"
);
}
if !self.csp.connect_src.iter().any(|s| s == "'self'") {
tracing::warn!(
"Admin CSP: connect_src is missing `'self'`, API calls will be blocked"
);
}
}
fn warn_security_misconfigurations(&self) {
let fo = self.security.frame_options.to_lowercase();
if fo != "deny" && fo != "sameorigin" {
tracing::warn!(
"Admin security: unrecognized frame_options value '{}', expected 'deny' or 'sameorigin'",
self.security.frame_options
);
}
}
}
use std::sync::OnceLock;
static ADMIN_SETTINGS: OnceLock<AdminSettings> = OnceLock::new();
pub fn configure(settings: AdminSettings) {
ADMIN_SETTINGS
.set(settings)
.expect("AdminSettings can only be configured once");
}
pub fn get_admin_settings() -> &'static AdminSettings {
ADMIN_SETTINGS.get_or_init(AdminSettings::default)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
fn test_admin_settings_default_values() {
let settings = AdminSettings::default();
assert_eq!(settings.site_title, "Reinhardt Admin");
assert_eq!(settings.site_header, "Administration");
assert_eq!(settings.list_per_page, 100);
assert_eq!(settings.login_url, "/admin/login");
assert_eq!(settings.logout_url, "/admin/logout");
}
#[rstest]
fn test_admin_csp_settings_default_matches_admin_default() {
let csp = AdminCspSettings::default();
assert_eq!(csp.default_src, vec!["'self'"]);
assert_eq!(csp.script_src, vec!["'self'", "'wasm-unsafe-eval'"]);
assert_eq!(csp.style_src, vec!["'self'", "'unsafe-inline'"]);
assert_eq!(csp.img_src, vec!["'self'", "data:"]);
assert_eq!(csp.font_src, vec!["'self'"]);
assert_eq!(csp.connect_src, vec!["'self'"]);
assert_eq!(csp.frame_ancestors, vec!["'none'"]);
assert_eq!(csp.base_uri, vec!["'self'"]);
assert_eq!(csp.form_action, vec!["'self'"]);
}
#[rstest]
fn test_admin_security_settings_default_values() {
let security = AdminSecuritySettings::default();
assert_eq!(security.frame_options, "deny");
assert_eq!(security.referrer_policy, "strict-origin-when-cross-origin");
assert_eq!(
security.permissions_policy,
"camera=(), microphone=(), geolocation=(), payment=()"
);
}
#[rstest]
fn test_toml_partial_deserialization() {
let toml_str = r#"
site_title = "My Admin"
list_per_page = 50
"#;
let settings: AdminSettings = toml::from_str(toml_str).unwrap();
assert_eq!(settings.site_title, "My Admin");
assert_eq!(settings.list_per_page, 50);
assert_eq!(settings.site_header, "Administration");
assert_eq!(settings.login_url, "/admin/login");
assert_eq!(settings.csp, AdminCspSettings::default());
assert_eq!(settings.security, AdminSecuritySettings::default());
}
#[rstest]
fn test_toml_csp_override() {
let toml_str = r#"
[csp]
script_src = ["'self'", "'wasm-unsafe-eval'", "https://cdn.example.com"]
img_src = ["'self'", "data:", "https://images.example.com"]
"#;
let settings: AdminSettings = toml::from_str(toml_str).unwrap();
assert_eq!(
settings.csp.script_src,
vec!["'self'", "'wasm-unsafe-eval'", "https://cdn.example.com"]
);
assert_eq!(
settings.csp.img_src,
vec!["'self'", "data:", "https://images.example.com"]
);
assert_eq!(settings.csp.default_src, vec!["'self'"]);
assert_eq!(settings.csp.font_src, vec!["'self'"]);
assert_eq!(settings.csp.frame_ancestors, vec!["'none'"]);
}
#[rstest]
fn test_settings_fragment_section_is_admin() {
use reinhardt_conf::SettingsFragment;
let section = AdminSettings::section();
assert_eq!(section, "admin");
}
#[rstest]
fn test_validate_warns_on_missing_self_in_script_src() {
let mut settings = AdminSettings::default();
settings.csp.script_src = vec!["'wasm-unsafe-eval'".to_string()];
use reinhardt_conf::settings::fragment::SettingsValidation;
let result = SettingsValidation::validate(
&settings,
&reinhardt_conf::settings::profile::Profile::Development,
);
assert!(result.is_ok());
}
#[rstest]
fn test_validate_warns_on_missing_wasm_unsafe_eval() {
let mut settings = AdminSettings::default();
settings.csp.script_src = vec!["'self'".to_string()];
use reinhardt_conf::settings::fragment::SettingsValidation;
let result = SettingsValidation::validate(
&settings,
&reinhardt_conf::settings::profile::Profile::Development,
);
assert!(result.is_ok());
}
#[rstest]
fn test_validate_warns_on_unrecognized_frame_options() {
let mut settings = AdminSettings::default();
settings.security.frame_options = "invalid-value".to_string();
use reinhardt_conf::settings::fragment::SettingsValidation;
let result = SettingsValidation::validate(
&settings,
&reinhardt_conf::settings::profile::Profile::Production,
);
assert!(result.is_ok());
}
#[rstest]
fn test_validate_ok_with_defaults() {
let settings = AdminSettings::default();
use reinhardt_conf::settings::fragment::SettingsValidation;
let result = SettingsValidation::validate(
&settings,
&reinhardt_conf::settings::profile::Profile::Production,
);
assert!(result.is_ok());
}
#[rstest]
fn test_toml_empty_deserialization() {
let toml_str = "";
let settings: AdminSettings = toml::from_str(toml_str).unwrap();
assert_eq!(settings, AdminSettings::default());
}
#[rstest]
fn test_to_security_headers_default_matches_security_headers_default() {
let admin_settings = AdminSettings::default();
let direct_headers = crate::server::security::SecurityHeaders::default();
let converted_headers = admin_settings.to_security_headers();
let direct_map = direct_headers.to_header_map();
let converted_map = converted_headers.to_header_map();
assert_eq!(
direct_map.get("Content-Security-Policy"),
converted_map.get("Content-Security-Policy")
);
assert_eq!(
direct_map.get("X-Frame-Options"),
converted_map.get("X-Frame-Options")
);
assert_eq!(
direct_map.get("Referrer-Policy"),
converted_map.get("Referrer-Policy")
);
assert_eq!(
direct_map.get("Permissions-Policy"),
converted_map.get("Permissions-Policy")
);
}
#[rstest]
fn test_to_security_headers_custom_csp() {
let mut settings = AdminSettings::default();
settings.csp.script_src = vec![
"'self'".to_string(),
"'wasm-unsafe-eval'".to_string(),
"https://cdn.example.com".to_string(),
];
let headers = settings.to_security_headers();
let map = headers.to_header_map();
let csp = map.get("Content-Security-Policy").unwrap();
assert!(csp.contains("https://cdn.example.com"));
assert!(csp.contains("'self'"));
assert!(csp.contains("'wasm-unsafe-eval'"));
}
#[rstest]
fn test_to_security_headers_custom_frame_options() {
let mut settings = AdminSettings::default();
settings.security.frame_options = "sameorigin".to_string();
let headers = settings.to_security_headers();
let map = headers.to_header_map();
assert_eq!(map.get("X-Frame-Options").unwrap(), "SAMEORIGIN");
}
#[rstest]
fn test_to_security_headers_custom_permissions_policy() {
let mut settings = AdminSettings::default();
settings.security.permissions_policy = "camera=(), microphone=()".to_string();
let headers = settings.to_security_headers();
let map = headers.to_header_map();
assert_eq!(
map.get("Permissions-Policy").unwrap(),
"camera=(), microphone=()"
);
}
}
}
#[cfg(server)]
pub use inner::*;