use crate::{DevConfig, Result, PepError};
#[cfg(any(feature = "oidc", feature = "oidc-client"))]
use crate::OidcClientConfig;
use crate::oidc::types::{ResourceServerConfig, JwtValidationOptions};
#[cfg(feature = "cedar")]
use crate::cedar::config::CedarConfig;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PepConfig {
#[serde(default)]
pub oidc: Option<OidcConfig>,
#[serde(default)]
pub dev: Option<OidcDevConfig>,
#[cfg(feature = "cedar")]
#[serde(default)]
pub cedar: Option<CedarConfig>,
}
impl PepConfig {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
load_config(path)
}
pub fn oidc_config(&self) -> Result<OidcConfig> {
self.oidc.clone().ok_or_else(|| {
PepError::Config("OIDC configuration not found in config file".to_string())
})
}
pub fn dev_config(&self) -> Option<OidcDevConfig> {
self.dev.clone()
}
#[cfg(feature = "cedar")]
pub fn cedar_config(&self) -> Result<CedarConfig> {
self.cedar.clone().ok_or_else(|| {
PepError::Config("Cedar configuration not found in config file".to_string())
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OidcConfig {
#[serde(default)]
pub provider: Option<String>,
pub issuer_url: String,
pub client_id: String,
#[serde(default)]
pub client_secret: Option<String>,
#[serde(default)]
pub redirect_url: Option<String>,
#[serde(default)]
pub code_challenge_method: Option<String>,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub skip_issuer_validation: Option<bool>,
#[serde(default)]
pub skip_audience_validation: Option<bool>,
#[serde(default)]
pub expected_audience: Option<String>,
}
impl OidcConfig {
#[cfg(any(feature = "oidc", feature = "oidc-client"))]
pub fn to_oidc_client_config(&self) -> OidcClientConfig {
OidcClientConfig {
issuer_url: self.issuer_url.clone(),
client_id: self.client_id.clone(),
client_secret: self.client_secret.clone(),
redirect_uri: self.redirect_url.clone().unwrap_or_default(),
scope: self.scope.clone().unwrap_or_else(|| "openid profile email".to_string()),
code_challenge_method: self.code_challenge_method.clone().unwrap_or_else(|| "S256".to_string()),
}
}
pub fn to_resource_server_config(&self) -> ResourceServerConfig {
let validation_options = JwtValidationOptions {
skip_issuer_validation: self.skip_issuer_validation.unwrap_or(false),
skip_audience_validation: self.skip_audience_validation.unwrap_or(false),
expected_audience: self.expected_audience.clone(),
};
ResourceServerConfig {
issuer_url: self.issuer_url.clone(),
client_id: self.client_id.clone(),
validation_options,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OidcDevConfig {
#[serde(default)]
pub local_dev_mode: bool,
#[serde(default)]
pub local_dev_email: Option<String>,
#[serde(default)]
pub local_dev_name: Option<String>,
#[serde(default)]
pub local_dev_username: Option<String>,
#[serde(default)]
pub local_dev_role: Option<String>,
}
impl OidcDevConfig {
pub fn to_pep_dev_config(&self) -> DevConfig {
DevConfig {
local_dev_mode: self.local_dev_mode,
local_dev_email: self.local_dev_email.clone(),
local_dev_name: self.local_dev_name.clone(),
local_dev_username: self.local_dev_username.clone(),
}
}
}
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<PepConfig> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.map_err(|e| PepError::Config(format!("Failed to read config file: {}", e)))?;
toml::from_str(&content)
.map_err(|e| PepError::Config(format!("Failed to parse TOML config: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_oidc_config() {
let toml_str = r#"
[oidc]
issuer_url = "https://idm.example.com"
client_id = "test-client"
"#;
let config: PepConfig = toml::from_str(toml_str).unwrap();
assert!(config.oidc.is_some());
let oidc = config.oidc.unwrap();
assert_eq!(oidc.issuer_url, "https://idm.example.com");
assert_eq!(oidc.client_id, "test-client");
assert!(oidc.client_secret.is_none());
}
#[test]
fn test_parse_full_oidc_config() {
let toml_str = r#"
[oidc]
provider = "kanidm"
issuer_url = "https://idm.example.com"
client_id = "test-client"
client_secret = "test-secret"
redirect_url = "https://app.example.com/callback"
code_challenge_method = "S256"
scope = "openid email profile offline_access"
skip_issuer_validation = false
skip_audience_validation = false
expected_audience = "test-audience"
"#;
let config: PepConfig = toml::from_str(toml_str).unwrap();
assert!(config.oidc.is_some());
let oidc = config.oidc.unwrap();
assert_eq!(oidc.provider.unwrap(), "kanidm");
assert_eq!(oidc.client_secret.unwrap(), "test-secret");
assert_eq!(oidc.scope.unwrap(), "openid email profile offline_access");
}
#[test]
fn test_parse_dev_config() {
let toml_str = r#"
[dev]
local_dev_mode = true
local_dev_email = "dev@example.com"
local_dev_name = "Dev User"
local_dev_username = "devuser"
local_dev_role = "admin"
"#;
let config: PepConfig = toml::from_str(toml_str).unwrap();
assert!(config.dev.is_some());
let dev = config.dev.unwrap();
assert!(dev.local_dev_mode);
assert_eq!(dev.local_dev_email.unwrap(), "dev@example.com");
}
#[test]
fn test_parse_full_config() {
let toml_str = r#"
[oidc]
issuer_url = "https://idm.example.com"
client_id = "test-client"
client_secret = "test-secret"
scope = "openid email profile"
[dev]
local_dev_mode = true
local_dev_email = "dev@example.com"
"#;
let config: PepConfig = toml::from_str(toml_str).unwrap();
assert!(config.oidc.is_some());
assert!(config.dev.is_some());
}
#[test]
fn test_oidc_config_conversion() {
let oidc_config = OidcConfig {
provider: Some("kanidm".to_string()),
issuer_url: "https://idm.example.com".to_string(),
client_id: "test-client".to_string(),
client_secret: Some("test-secret".to_string()),
redirect_url: Some("https://app.example.com/callback".to_string()),
code_challenge_method: Some("S256".to_string()),
scope: Some("openid email profile".to_string()),
skip_issuer_validation: None,
skip_audience_validation: None,
expected_audience: None,
};
let client_config = oidc_config.to_oidc_client_config();
assert_eq!(client_config.issuer_url, "https://idm.example.com");
assert_eq!(client_config.client_id, "test-client");
assert_eq!(client_config.scope, "openid email profile");
let resource_config = oidc_config.to_resource_server_config();
assert_eq!(resource_config.issuer_url, "https://idm.example.com");
assert_eq!(resource_config.client_id, "test-client");
}
#[test]
fn test_dev_config_conversion() {
let dev_config = OidcDevConfig {
local_dev_mode: true,
local_dev_email: Some("dev@example.com".to_string()),
local_dev_name: Some("Dev User".to_string()),
local_dev_username: Some("devuser".to_string()),
local_dev_role: Some("admin".to_string()),
};
let pep_dev = dev_config.to_pep_dev_config();
assert!(pep_dev.local_dev_mode);
assert_eq!(pep_dev.local_dev_email.unwrap(), "dev@example.com");
}
#[test]
fn test_pep_config_oidc_config_method() {
let toml_str = r#"
[oidc]
issuer_url = "https://idm.example.com"
client_id = "test-client"
"#;
let config: PepConfig = toml::from_str(toml_str).unwrap();
let oidc = config.oidc_config().unwrap();
assert_eq!(oidc.client_id, "test-client");
}
}