diff --git a/ak-sysd/src/cfg/domain.rs b/ak-sysd/src/cfg/domain.rs index 7077362e..1ac5c4be 100644 --- a/ak-sysd/src/cfg/domain.rs +++ b/ak-sysd/src/cfg/domain.rs @@ -212,17 +212,41 @@ impl DomainManager { refresh_interval } - /// First enabled domain — mirrors Go's `dom[0]` shortcut for - /// single-tenant components (ping, auth, directory, device). Do not + /// First enabled domain that has a token — mirrors Go's `dom[0]` shortcut + /// for single-tenant components (ping, auth, directory, device). Do not /// invent smarter "current domain" selection here. + /// + /// An MDM-managed domain wins when there is one: it carries the connector + /// and device group the organisation assigned, whereas a hand-run + /// `domains join` may point somewhere else entirely. Policy beats ad hoc. + /// + /// The token check matters because `load_managed` adds an MDM-managed + /// domain alongside any user-enrolled ones. If its enrollment produced no + /// token, it is still `enabled`, and returning it means every request goes + /// out as `Bearer+agent ` and comes back 403 "Authentication credentials + /// were not provided" — while a perfectly good domain sits further down the + /// list. + /// + /// Note this still only ever returns one domain, while the storage layer + /// happily holds several. Every auth path (`ping`, `auth/*`, `session`, + /// `directory`) calls this and silently ignores the rest; only `device` + /// check-in and `healthcheck_all` fan out. Making multi-domain genuinely + /// work means binding a domain to each of those call sites. pub async fn active(&self) -> Result> { - self.domains - .read() - .await + let domains = self.domains.read().await; + let usable = |d: &&Arc| d.cfg.enabled && !d.cfg.token.is_empty(); + let selected = domains .iter() - .find(|d| d.cfg.enabled) + .find(|d| usable(d) && d.cfg.managed) + .or_else(|| domains.iter().find(usable)) .cloned() - .ok_or_else(|| eyre!("no enabled domain configured")) + .ok_or_else(|| eyre!("no enabled domain with a token configured"))?; + tracing::debug!( + domain = %selected.cfg.domain, + managed = selected.cfg.managed, + "selected active domain" + ); + Ok(selected) } pub async fn save_domain(&self, cfg: DomainConfig) -> Result<()> { @@ -370,11 +394,25 @@ impl DomainManager { /// Loads (or re-enrolls, or removes) the MDM-managed domain. See /// `cfg::managed` for the platform-specific config source. pub async fn load_managed(&self) -> Result<()> { + const MANAGED_DOMAIN_NAME: &str = "ak-mdm-managed"; + let Some(managed) = crate::cfg::managed::load_managed_config()? else { + // No managed config means the profile is gone, so a managed domain + // left behind is stale policy. `load_all` reloads every `*.json` + // regardless of whether the config that created it still exists, so + // without this the domain lingers forever — never refreshed, never + // revoked, and still competing to be the active one. + if self + .domains() + .await + .iter() + .any(|d| d.cfg.domain == MANAGED_DOMAIN_NAME) + { + tracing::info!("managed config absent, removing managed domain"); + self.delete_domain(MANAGED_DOMAIN_NAME).await?; + } return Ok(()); }; - - const MANAGED_DOMAIN_NAME: &str = "ak-mdm-managed"; let existing = self .domains() .await