Skip to content

Commit

Permalink
Custom domains matching
Browse files Browse the repository at this point in the history
Fixes #314 #205

Himmelblau needs to be able to recognize multiple
custom domains as configured in an Azure tenant.

Signed-off-by: David Mulder <[email protected]>
  • Loading branch information
dmulder committed Dec 17, 2024
1 parent 762c756 commit a6164e8
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 10 deletions.
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
32 changes: 25 additions & 7 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 @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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(
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 a6164e8

Please sign in to comment.