diff --git a/man/man5/himmelblau.conf.5 b/man/man5/himmelblau.conf.5 index ee07efa..f38db41 100644 --- a/man/man5/himmelblau.conf.5 +++ b/man/man5/himmelblau.conf.5 @@ -47,10 +47,11 @@ This section contains settings that apply globally to all operations of Himmelbl .TP .B domains .RE -A comma-separated list of configured domains. This parameter is +A comma-separated list of primary domains for your Azure Entra ID tenants. This parameter is .B REQUIRED for successful authentication. If this option is not specified, no users will be permitted to authenticate. The first user to authenticate to each domain will become the owner of the device object in the directory. +Specify ONLY the primary domain for each tenant. Specifying multiple custom domains which belong to a single tenant will cause an idmap range overlap and the himmelblaud daemon will NOT start. If multiple domains are specified, you .B MUST diff --git a/platform/debian/himmelblau.conf.example b/platform/debian/himmelblau.conf.example index 41a2a42..ad34a1e 100644 --- a/platform/debian/himmelblau.conf.example +++ b/platform/debian/himmelblau.conf.example @@ -2,7 +2,8 @@ # REQUIRED: The list of configured domains. This must be specified, or no users # will be permitted to authenticate. The first user to authenticate to each # domain will be the owner of the device object in the directory. Typically -# this would be the primary user of the device. +# this would be the primary user of the device. Specify ONLY the primary domain +# for your tenant. # domains = # ### Optional global values diff --git a/src/common/src/config.rs b/src/common/src/config.rs index a8eb543..79d1b2d 100644 --- a/src/common/src/config.rs +++ b/src/common/src/config.rs @@ -30,7 +30,10 @@ use crate::constants::{ DEFAULT_TASK_SOCK_PATH, DEFAULT_USE_ETC_SKEL, SERVER_CONFIG_PATH, }; use crate::unix_config::{HomeAttr, HsmType}; +use himmelblau::error::MsalError; use idmap::DEFAULT_IDMAP_RANGE; +use reqwest::Url; +use serde::Deserialize; use std::env; #[derive(Debug, Copy, Clone, PartialEq)] @@ -47,6 +50,54 @@ pub fn split_username(username: &str) -> Option<(&str, &str)> { None } +#[derive(Debug, Deserialize)] +struct FederationProvider { + #[serde(rename = "tenantId")] + tenant_id: String, + authority_host: String, + graph: String, +} + +async fn request_federation_provider( + odc_provider: &str, + domain: &str, +) -> Result<(String, String, String), MsalError> { + let client = reqwest::Client::builder() + .build() + .map_err(|e| MsalError::RequestFailed(format!("{:?}", e)))?; + + let url = Url::parse_with_params( + &format!("https://{}/odc/v2.1/federationProvider", odc_provider), + &[("domain", domain)], + ) + .map_err(|e| MsalError::RequestFailed(format!("{:?}", e)))?; + + let resp = client + .get(url) + .send() + .await + .map_err(|e| MsalError::RequestFailed(format!("{:?}", e)))?; + if resp.status().is_success() { + let json_resp: FederationProvider = resp + .json() + .await + .map_err(|e| MsalError::InvalidJson(format!("{:?}", e)))?; + debug!("Discovered tenant_id: {}", json_resp.tenant_id); + debug!("Discovered authority_host: {}", json_resp.authority_host); + debug!("Discovered graph: {}", json_resp.graph); + Ok(( + json_resp.authority_host, + json_resp.tenant_id, + json_resp.graph, + )) + } else { + Err(MsalError::RequestFailed(format!( + "Federation Provider request failed: {}", + resp.status(), + ))) + } +} + #[derive(Clone)] pub struct HimmelblauConfig { config: Ini, @@ -482,6 +533,76 @@ impl HimmelblauConfig { pub fn get_enable_experimental_mfa(&self) -> bool { match_bool(self.config.get("global", "enable_experimental_mfa"), true) } + + pub async fn get_primary_domain_from_alias(&mut self, alias: &str) -> Option { + let domains = self.get_configured_domains(); + + // Attempt to short-circut the request by checking if the alias is + // already configured. + for domain in &domains { + let domain_aliases = match self.config.get(domain, "domain_aliases") { + Some(aliases) => aliases.split(",").map(|s| s.to_string()).collect(), + None => vec![], + }; + if domain_aliases.contains(&alias.to_string()) { + return Some(domain.to_string()); + } + } + + let mut modified_config = false; + + // We don't recognize this alias, so now we need to search for it the + // hard way by checking for matching tenant id's. + let (_, alias_tenant_id, _) = + match request_federation_provider(DEFAULT_ODC_PROVIDER, alias).await { + Ok(resp) => resp, + Err(e) => { + error!( + "Failed matching alias '{}' to a configured tenant: {:?}", + alias, e + ); + return None; + } + }; + for domain in domains { + let tenant_id = match self.get_tenant_id(&domain) { + Some(tenant_id) => tenant_id, + None => { + let (authority_host, tenant_id, graph_url) = + match request_federation_provider(&self.get_odc_provider(&domain), &domain) + .await + { + Ok(resp) => resp, + Err(e) => { + error!("Failed sending federation provider request: {:?}", e); + continue; + } + }; + self.set(&domain, "authority_host", &authority_host); + self.set(&domain, "tenant_id", &tenant_id); + self.set(&domain, "graph_url", &graph_url); + modified_config = true; + tenant_id + } + }; + if tenant_id == alias_tenant_id { + let mut domain_aliases = match self.config.get(&domain, "domain_aliases") { + Some(aliases) => aliases.split(",").map(|s| s.to_string()).collect(), + None => vec![], + }; + domain_aliases.push(alias.to_string()); + self.set(&domain, "domain_aliases", &domain_aliases.join(",")); + let _ = self.write_server_config(); + return Some(domain); + } + } + + error!("Failed matching alias '{}' to a configured tenant", alias); + if modified_config { + let _ = self.write_server_config(); + } + None + } } impl fmt::Debug for HimmelblauConfig { diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index 090c967..eb071be 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -48,6 +48,7 @@ use uuid::Uuid; struct Token(Option, String); pub struct HimmelblauMultiProvider { + config: Arc>, providers: RwLock>, } @@ -161,11 +162,28 @@ impl HimmelblauMultiProvider { } Ok(HimmelblauMultiProvider { + config: config.clone(), providers: RwLock::new(providers), }) } } +macro_rules! find_provider { + ($hmp:ident, $providers:ident, $domain:ident) => {{ + match $providers.get($domain) { + Some(provider) => Some(provider), + None => { + // Attempt to match a provider alias + let mut cfg = $hmp.config.write().await; + match cfg.get_primary_domain_from_alias($domain).await { + Some(domain) => $providers.get(&domain), + None => None, + } + } + } + }}; +} + #[async_trait] impl IdProvider for HimmelblauMultiProvider { /* TODO: Kanidm should be modified to provide the account_id to @@ -197,7 +215,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(&account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_access(id, scopes, old_token, tpm, machine_key) @@ -224,7 +242,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(&account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_ccaches(id, old_token, tpm, machine_key) @@ -251,7 +269,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(&account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_prt_cookie(id, old_token, tpm, machine_key) @@ -276,7 +294,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .change_auth_token( @@ -311,7 +329,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(&account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_get(id, old_token, tpm, machine_key) @@ -336,7 +354,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_online_auth_init( @@ -373,7 +391,7 @@ impl IdProvider for HimmelblauMultiProvider { match split_username(account_id) { Some((_sam, domain)) => { let providers = self.providers.read().await; - match providers.get(domain) { + match find_provider!(self, providers, domain) { Some(provider) => { provider .unix_user_online_auth_step( diff --git a/src/config/himmelblau.conf.example b/src/config/himmelblau.conf.example index d9cb1d3..06eb6c0 100644 --- a/src/config/himmelblau.conf.example +++ b/src/config/himmelblau.conf.example @@ -2,7 +2,8 @@ # REQUIRED: The list of configured domains. This must be specified, or no users # will be permitted to authenticate. The first user to authenticate to each # domain will be the owner of the device object in the directory. Typically -# this would be the primary user of the device. +# this would be the primary user of the device. Specify ONLY the primary domain +# for your tenant. # domains = # ### Optional global values