use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize)]
struct Root {
identity: IdentityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_callback_host")]
pub callback_host: String,
#[serde(default)]
pub callback_port: u16,
#[serde(default = "default_hold")]
pub hold_seconds: u64,
#[serde(default)]
pub providers: Vec<ProviderConfig>,
}
impl Default for IdentityConfig {
fn default() -> Self {
Self {
enabled: true,
callback_host: default_callback_host(),
callback_port: 0,
hold_seconds: default_hold(),
providers: vec![ProviderConfig::default_mock()],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub id: String,
pub kind: ProviderKind,
#[serde(default)]
pub sandbox: bool,
#[serde(default)]
pub client_id_env: Option<String>,
#[serde(default)]
pub client_secret_env: Option<String>,
#[serde(default = "default_scopes")]
pub scopes: Vec<String>,
#[serde(default)]
pub authorize_url: Option<String>,
#[serde(default)]
pub token_url: Option<String>,
#[serde(default)]
pub userinfo_url: Option<String>,
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub loa: u8,
}
impl ProviderConfig {
pub fn default_mock() -> Self {
Self {
id: "mock".to_string(),
kind: ProviderKind::Mock,
sandbox: false,
client_id_env: None,
client_secret_env: None,
scopes: default_scopes(),
authorize_url: None,
token_url: None,
userinfo_url: None,
subject: Some("mock-subject-0001".to_string()),
email: Some("[email protected]".to_string()),
loa: 2,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderKind {
IdMe,
Mock,
}
fn default_true() -> bool { true }
fn default_callback_host() -> String { "127.0.0.1".to_string() }
fn default_hold() -> u64 { 120 }
fn default_scopes() -> Vec<String> { vec!["openid".to_string()] }
impl IdentityConfig {
pub fn from_yaml(raw: &str) -> anyhow::Result<Self> {
let root: Root = serde_yaml::from_str(raw)?;
Ok(root.identity)
}
pub fn load(explicit: Option<&Path>) -> anyhow::Result<Self> {
if let Some(p) = explicit {
let raw = std::fs::read_to_string(p)?;
return Self::from_yaml(&raw);
}
if let Ok(p) = std::env::var("APERION_SHIELD_IDENTITY_CONFIG") {
if !p.is_empty() {
let raw = std::fs::read_to_string(&p)?;
return Self::from_yaml(&raw);
}
}
if let Some(home) = dirs::home_dir() {
let p = home.join(".aperion-shield").join("identity.yaml");
if p.exists() {
let raw = std::fs::read_to_string(&p)?;
return Self::from_yaml(&raw);
}
}
Ok(Self::default())
}
pub fn state_dir() -> PathBuf {
if let Ok(d) = std::env::var("APERION_SHIELD_STATE_DIR") {
if !d.is_empty() {
return PathBuf::from(d);
}
}
dirs::home_dir()
.map(|h| h.join(".aperion-shield"))
.unwrap_or_else(|| PathBuf::from(".aperion-shield"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_have_mock_provider() {
let c = IdentityConfig::default();
assert!(c.enabled);
assert_eq!(c.callback_host, "127.0.0.1");
assert_eq!(c.hold_seconds, 120);
assert_eq!(c.providers.len(), 1);
assert_eq!(c.providers[0].id, "mock");
assert_eq!(c.providers[0].kind, ProviderKind::Mock);
}
#[test]
fn parses_full_yaml() {
let yaml = r#"
identity:
enabled: true
callback_host: 127.0.0.1
callback_port: 0
hold_seconds: 90
providers:
- id: id_me
kind: id_me
sandbox: true
client_id_env: IDME_CLIENT_ID
client_secret_env: IDME_CLIENT_SECRET
scopes: ["openid", "ial2"]
- id: mock
kind: mock
subject: "[email protected]"
loa: 2
"#;
let c = IdentityConfig::from_yaml(yaml).unwrap();
assert_eq!(c.hold_seconds, 90);
assert_eq!(c.providers.len(), 2);
assert_eq!(c.providers[0].kind, ProviderKind::IdMe);
assert!(c.providers[0].sandbox);
assert_eq!(c.providers[0].scopes, vec!["openid".to_string(), "ial2".into()]);
assert_eq!(c.providers[1].kind, ProviderKind::Mock);
}
}