use std::collections::HashMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdpKind {
Oidc,
Saml,
}
#[derive(Debug, Clone)]
pub struct IdpConfig {
pub id: String,
pub tenant: String,
pub kind: IdpKind,
pub issuer: String,
pub audience: String,
pub domains: Vec<String>,
pub sso_url: String,
}
struct IdpTable {
by_id: HashMap<String, Arc<IdpConfig>>,
by_domain: HashMap<String, Arc<IdpConfig>>,
}
impl IdpTable {
fn build(configs: Vec<IdpConfig>) -> Self {
let mut by_id = HashMap::new();
let mut by_domain = HashMap::new();
for cfg in configs {
let arc = Arc::new(cfg);
for d in &arc.domains {
by_domain.insert(d.to_ascii_lowercase(), arc.clone());
}
by_id.insert(arc.id.clone(), arc.clone());
}
Self { by_id, by_domain }
}
}
pub struct IdpRegistry {
table: ArcSwap<IdpTable>,
}
impl IdpRegistry {
pub fn new(configs: Vec<IdpConfig>) -> Self {
Self {
table: ArcSwap::from_pointee(IdpTable::build(configs)),
}
}
pub fn resolve_by_email(&self, email: &str) -> Option<Arc<IdpConfig>> {
let domain = email.rsplit('@').next()?.to_ascii_lowercase();
self.table.load().by_domain.get(&domain).cloned()
}
pub fn get(&self, id: &str) -> Option<Arc<IdpConfig>> {
self.table.load().by_id.get(id).cloned()
}
pub fn reload(&self, configs: Vec<IdpConfig>) {
self.table.store(Arc::new(IdpTable::build(configs)));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reg() -> IdpRegistry {
IdpRegistry::new(vec![IdpConfig {
id: "acme-azuread".into(),
tenant: "acme".into(),
kind: IdpKind::Oidc,
issuer: "https://login.microsoftonline.com/acme/v2.0".into(),
audience: "client-acme".into(),
domains: vec!["acme.com".into(), "acme.co.uk".into()],
sso_url: "https://login.microsoftonline.com/acme/authorize".into(),
}])
}
#[test]
fn resolves_home_realm_by_domain() {
let r = reg();
assert_eq!(r.resolve_by_email("alice@ACME.com").unwrap().tenant, "acme");
assert_eq!(
r.resolve_by_email("bob@acme.co.uk").unwrap().id,
"acme-azuread"
);
assert!(r.resolve_by_email("carol@gmail.com").is_none());
}
}