Skip to content

Commit

Permalink
Merge pull request #330 from himmelblau-idm/stable-0.7.x_bug314
Browse files Browse the repository at this point in the history
Stable 0.7.x Fix Multi Domain support not working
  • Loading branch information
dmulder authored Dec 18, 2024
2 parents d790d31 + b6e5bd7 commit a54af23
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 14 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.7.13"
version = "0.7.14"
authors = [
"David Mulder <[email protected]>"
]
Expand All @@ -40,7 +40,7 @@ tracing-subscriber = "^0.3.17"
tracing = "^0.1.37"
himmelblau_unix_common = { path = "src/common" }
kanidm_unix_common = { path = "src/glue" }
libhimmelblau = { version = "0.4.4" }
libhimmelblau = { version = "0.4.6" }
clap = { version = "^4.5", features = ["derive", "env"] }
clap_complete = "^4.4.1"
reqwest = { version = "^0.12.2", features = ["json"] }
Expand Down
3 changes: 2 additions & 1 deletion man/man5/himmelblau.conf.5
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion platform/debian/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 121 additions & 0 deletions src/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -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<String> {
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 {
Expand Down
39 changes: 30 additions & 9 deletions src/common/src/idprovider/himmelblau.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use uuid::Uuid;
struct Token(Option<String>, String);

pub struct HimmelblauMultiProvider {
config: Arc<RwLock<HimmelblauConfig>>,
providers: RwLock<HashMap<String, HimmelblauProvider>>,
}

Expand Down Expand Up @@ -128,8 +129,11 @@ impl HimmelblauMultiProvider {
continue;
}
};
let authority_host = graph.authority_host();
let tenant_id = graph.tenant_id();
let authority_host = graph
.authority_host()
.await
.map_err(|e| anyhow!("{:?}", e))?;
let tenant_id = graph.tenant_id().await.map_err(|e| anyhow!("{:?}", e))?;
idmap_lk
.add_gen_domain(&domain, &tenant_id, range)
.map_err(|e| anyhow!("{:?}", e))?;
Expand Down Expand Up @@ -161,11 +165,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
Expand Down Expand Up @@ -197,7 +218,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)
Expand All @@ -224,7 +245,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)
Expand All @@ -249,7 +270,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(
Expand Down Expand Up @@ -284,7 +305,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)
Expand All @@ -309,7 +330,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(
Expand Down Expand Up @@ -345,7 +366,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(
Expand Down Expand Up @@ -1473,7 +1494,7 @@ impl HimmelblauProvider {
"Setting domain {} config authority_host to {}",
self.domain, &self.authority_host
);
let graph_url = self.graph.graph_url();
let graph_url = self.graph.graph_url().await?;
config.set(&self.domain, "graph_url", &graph_url);
debug!(
"Setting domain {} config graph_url to {}",
Expand Down
3 changes: 2 additions & 1 deletion src/config/himmelblau.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a54af23

Please sign in to comment.