Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package himmelblau for openSUSE:Factory 
checked in at 2025-06-18 15:57:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/himmelblau (Old)
 and      /work/SRC/openSUSE:Factory/.himmelblau.new.19631 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "himmelblau"

Wed Jun 18 15:57:44 2025 rev:31 rq:1286464 version:0.9.16+git.0.aac2205

Changes:
--------
--- /work/SRC/openSUSE:Factory/himmelblau/himmelblau.changes    2025-05-20 
12:20:53.268093587 +0200
+++ /work/SRC/openSUSE:Factory/.himmelblau.new.19631/himmelblau.changes 
2025-06-18 16:03:02.837293350 +0200
@@ -1,0 +2,20 @@
+Tue Jun 17 21:16:41 UTC 2025 - david.mul...@suse.com
+
+- Update to version 0.9.16+git.0.aac2205:
+  * Disable passwordless Fido by default
+  * Stop using deprecated `users` crate
+  * Version 0.9.16
+  * When group membership lookup fails, use cached groups
+  * Just report whether some passwordless type is available
+  * Version 0.9.15
+  * Name-Based Group Matching in `pam_allow_groups` Leads to Potential 
Security Bypass
+  * Version 0.9.14
+  * Support Fido auth in pam passwd
+  * Add TAP support to himmelblaud and pam passwd
+  * Mixed case names should properly identify Hello Key
+  * Remove write locks where unecessary
+  * Fix group lookup for Entra Id group name
+  * Version 0.9.13
+  * Fix mixed case name lookup from PRT cache
+
+-------------------------------------------------------------------

Old:
----
  himmelblau-0.9.12+git.0.99b5ca6.tar.bz2

New:
----
  himmelblau-0.9.16+git.0.aac2205.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ himmelblau.spec ++++++
--- /var/tmp/diff_new_pack.QlktKZ/_old  2025-06-18 16:03:03.753331268 +0200
+++ /var/tmp/diff_new_pack.QlktKZ/_new  2025-06-18 16:03:03.753331268 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           himmelblau
-Version:        0.9.12+git.0.99b5ca6
+Version:        0.9.16+git.0.aac2205
 Release:        0
 Summary:        Interoperability suite for Microsoft Azure Entra Id
 License:        GPL-3.0-or-later

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.QlktKZ/_old  2025-06-18 16:03:03.793332925 +0200
+++ /var/tmp/diff_new_pack.QlktKZ/_new  2025-06-18 16:03:03.797333090 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/openSUSE/himmelblau.git</param>
               <param 
name="changesrevision">6d2f6450ff3c0c945a884d4b35307e03a035a581</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/himmelblau-idm/himmelblau.git</param>
-              <param 
name="changesrevision">99b5ca619330a68ed79ae887c109b232d5e7ae3d</param></service></servicedata>
+              <param 
name="changesrevision">aac2205d925fcb5bf050842592d360fbe46132cb</param></service></servicedata>
 (No newline at EOF)
 

++++++ himmelblau-0.9.12+git.0.99b5ca6.tar.bz2 -> 
himmelblau-0.9.16+git.0.aac2205.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/himmelblau-0.9.12+git.0.99b5ca6/Cargo.lock 
new/himmelblau-0.9.16+git.0.aac2205/Cargo.lock
--- old/himmelblau-0.9.12+git.0.99b5ca6/Cargo.lock      2025-05-20 
05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/Cargo.lock      2025-06-17 
20:44:46.000000000 +0200
@@ -4,7 +4,7 @@
 
 [[package]]
 name = "aad-tool"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "anyhow",
  "clap 4.5.31",
@@ -16,7 +16,7 @@
  "tokio-util",
  "tracing",
  "tracing-subscriber",
- "users",
+ "uzers",
 ]
 
 [[package]]
@@ -599,7 +599,7 @@
 
 [[package]]
 name = "broker"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "dbus",
  "himmelblau_unix_common",
@@ -1951,7 +1951,7 @@
 
 [[package]]
 name = "himmelblau_unix_common"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1987,7 +1987,7 @@
 
 [[package]]
 name = "himmelblaud"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "async-trait",
  "base64 0.22.1",
@@ -2012,7 +2012,7 @@
  "tokio-util",
  "tracing",
  "tracing-subscriber",
- "users",
+ "uzers",
  "walkdir",
 ]
 
@@ -2410,7 +2410,7 @@
 
 [[package]]
 name = "idmap"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "bindgen 0.71.1",
  "cc",
@@ -2594,7 +2594,7 @@
 
 [[package]]
 name = "kanidm_lib_crypto"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "argon2",
  "base64 0.22.1",
@@ -2613,7 +2613,7 @@
 
 [[package]]
 name = "kanidm_lib_file_permissions"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "kanidm_utils_users",
  "whoami",
@@ -2621,7 +2621,7 @@
 
 [[package]]
 name = "kanidm_proto"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "base32",
  "base64urlsafedata",
@@ -2641,7 +2641,7 @@
 
 [[package]]
 name = "kanidm_utils_users"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "libc",
 ]
@@ -2698,9 +2698,9 @@
 
 [[package]]
 name = "libhimmelblau"
-version = "0.6.17"
+version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3d138aee16400ce129e6cbe10ad59969e3d8053e817febb2ee4a7793857b7545"
+checksum = "c66d34144f1c56cd5cba4c3dbccc6ae875199d86e42e4a1212a9d109582f03ec"
 dependencies = [
  "base64 0.22.1",
  "browser-window",
@@ -2722,6 +2722,7 @@
  "reqwest",
  "scraper",
  "serde",
+ "serde_bytes",
  "serde_json",
  "tokio",
  "tracing",
@@ -3060,7 +3061,7 @@
 
 [[package]]
 name = "nss_himmelblau"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "himmelblau_unix_common",
  "lazy_static",
@@ -3454,7 +3455,7 @@
 
 [[package]]
 name = "pam_himmelblau"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "authenticator",
  "base64 0.22.1",
@@ -3956,7 +3957,7 @@
 
 [[package]]
 name = "qr-greeter"
-version = "0.9.12"
+version = "0.9.16"
 
 [[package]]
 name = "quote"
@@ -4427,9 +4428,9 @@
 
 [[package]]
 name = "serde_bytes"
-version = "0.11.15"
+version = "0.11.17"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a"
+checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
 dependencies = [
  "serde",
 ]
@@ -4609,7 +4610,7 @@
 
 [[package]]
 name = "sketching"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "gethostname",
  "num_enum",
@@ -4684,11 +4685,11 @@
 
 [[package]]
 name = "sshd-config"
-version = "0.9.12"
+version = "0.9.16"
 
 [[package]]
 name = "sso"
-version = "0.9.12"
+version = "0.9.16"
 dependencies = [
  "clap 4.5.31",
  "serde",
@@ -5426,16 +5427,6 @@
 checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
 
 [[package]]
-name = "users"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
-dependencies = [
- "libc",
- "log",
-]
-
-[[package]]
 name = "utf-8"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index";
@@ -5496,6 +5487,16 @@
 ]
 
 [[package]]
+name = "uzers"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63"
+dependencies = [
+ "libc",
+ "log",
+]
+
+[[package]]
 name = "valuable"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/himmelblau-0.9.12+git.0.99b5ca6/Cargo.toml 
new/himmelblau-0.9.16+git.0.aac2205/Cargo.toml
--- old/himmelblau-0.9.12+git.0.99b5ca6/Cargo.toml      2025-05-20 
05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/Cargo.toml      2025-06-17 
20:44:46.000000000 +0200
@@ -19,7 +19,7 @@
 resolver = "2"
 
 [workspace.package]
-version = "0.9.12"
+version = "0.9.16"
 authors = [
     "David Mulder <dmul...@suse.com>"
 ]
@@ -39,7 +39,7 @@
 tracing-subscriber = "^0.3.17"
 tracing = "^0.1.37"
 himmelblau_unix_common = { path = "src/common" }
-libhimmelblau = { version = "0.6.17", features = ["broker", "changepassword", 
"on_behalf_of"] }
+libhimmelblau = { version = "0.6.22", features = ["broker", "changepassword", 
"on_behalf_of"] }
 clap = { version = "^4.5", features = ["derive", "env"] }
 clap_complete = "^4.5.46"
 reqwest = { version = "^0.12.2", features = ["json"] }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/man/man5/himmelblau.conf.5 
new/himmelblau-0.9.16+git.0.aac2205/man/man5/himmelblau.conf.5
--- old/himmelblau-0.9.12+git.0.99b5ca6/man/man5/himmelblau.conf.5      
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/man/man5/himmelblau.conf.5      
2025-06-17 20:44:46.000000000 +0200
@@ -81,10 +81,10 @@
 .TP
 .B pam_allow_groups
 .RE
-A comma-separated list of Entra Id Users and Groups permitted to access the 
system. Users should be specified by UPN. Groups may be specified using either 
their Object ID or their Name.
+A comma-separated list of Entra Id Users and Groups permitted to access the 
system. Users should be specified by UPN. Groups MUST be specified using their 
Object ID GUID. Group names may not be used because these names are not 
guaranteed to be unique in Entra Id.
 
 .EXAMPLES
-pam_allow_groups = 
f3c9a7e4-7d5a-47e8-832f-3d2d92abcd12,EntraSudoersGroup,ad...@himmelblau-idm.org
+pam_allow_groups = 
f3c9a7e4-7d5a-47e8-832f-3d2d92abcd12,5ba4ef1d-e454-4f43-ba7c-6fe6f1601915,ad...@himmelblau-idm.org
 
 .TP
 .B id_attr_map
@@ -199,6 +199,14 @@
 enable_experimental_mfa = true
 
 .TP
+.B enable_experimental_passwordless_fido
+.RE
+A boolean option that enables the experimental passwordless FIDO flow for 
Azure Entra ID authentication. When enabled, Himmelblau will attempt to 
authenticate with Entra ID using a FIDO2 security key without requiring a 
password. By default, this option is disabled.
+
+.EXAMPLES
+enable_experimental_passwordless_fido = true
+
+.TP
 .B name_mapping_script
 .RE
 Specifies the path to an executable script used for mapping custom names to 
UPN names. The script MUST accept a single argument, which will always be a 
mapped name. The script MUST print the corresponding UPN (User Principal Name) 
to stdout. If the script does not recognize the input name, it MUST simply 
return the input name unchanged. This option is particularly useful in 
environments where direct UPN-to-CN mappings are impractical or where custom 
transformations are required.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/himmelblau-0.9.12+git.0.99b5ca6/src/cli/Cargo.toml 
new/himmelblau-0.9.16+git.0.aac2205/src/cli/Cargo.toml
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/cli/Cargo.toml      2025-05-20 
05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/cli/Cargo.toml      2025-06-17 
20:44:46.000000000 +0200
@@ -23,6 +23,6 @@
 clap = { workspace = true, features = ["derive"] }
 clap_complete = { workspace = true }
 anyhow = { workspace = true }
-users = "^0.11.0"
+uzers = "^0.11.0"
 sketching = { workspace = true }
 rpassword = "^7.2.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/config.rs 
new/himmelblau-0.9.16+git.0.aac2205/src/common/src/config.rs
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/config.rs        
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/common/src/config.rs        
2025-06-17 20:44:46.000000000 +0200
@@ -550,6 +550,14 @@
         match_bool(self.config.get("global", "enable_experimental_mfa"), true)
     }
 
+    pub fn get_enable_experimental_passwordless_fido(&self) -> bool {
+        match_bool(
+            self.config
+                .get("global", "enable_experimental_passwordless_fido"),
+            false,
+        )
+    }
+
     pub async fn get_primary_domain_from_alias(&mut self, alias: &str) -> 
Option<String> {
         let domains = self.get_configured_domains();
 
@@ -1166,6 +1174,27 @@
     }
 
     #[test]
+    fn test_get_enable_experimental_passwordless_fido() {
+        let config_data = r#"
+        [global]
+        enable_experimental_passwordless_fido = true
+        "#;
+
+        let temp_file = create_temp_config(config_data);
+        let config = HimmelblauConfig::new(Some(&temp_file)).unwrap();
+
+        // Test when explicitly set to true
+        assert_eq!(config.get_enable_experimental_passwordless_fido(), true);
+
+        // Test fallback default (false) when config is missing
+        let config_empty = HimmelblauConfig::new(None).unwrap();
+        assert_eq!(
+            config_empty.get_enable_experimental_passwordless_fido(),
+            false
+        );
+    }
+
+    #[test]
     fn test_get_debug() {
         let config_data = r#"
         [global]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/idprovider/himmelblau.rs 
new/himmelblau-0.9.16+git.0.aac2205/src/common/src/idprovider/himmelblau.rs
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/idprovider/himmelblau.rs 
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/common/src/idprovider/himmelblau.rs 
2025-06-17 20:44:46.000000000 +0200
@@ -89,7 +89,7 @@
     async fn refresh_token(&self, account_id: &str) -> Result<SealedData, 
IdpError> {
         self.purge().await;
         let refresh_cache = self.refresh_cache.read().await;
-        match refresh_cache.get(account_id) {
+        match refresh_cache.get(account_id.to_lowercase().as_str()) {
             Some((refresh_token, _)) => Ok(refresh_token.clone()),
             None => Err(IdpError::NotFound),
         }
@@ -110,7 +110,10 @@
 
     async fn add(&self, account_id: &str, prt: &SealedData) {
         let mut refresh_cache = self.refresh_cache.write().await;
-        refresh_cache.insert(account_id.to_string(), (prt.clone(), 
SystemTime::now()));
+        refresh_cache.insert(
+            account_id.to_string().to_lowercase(),
+            (prt.clone(), SystemTime::now()),
+        );
     }
 }
 
@@ -156,6 +159,7 @@
             let provider = HimmelblauProvider::new(app, &config, &domain, 
graph, &idmap)
                 .map_err(|_| anyhow!("Failed to initialize the provider"))?;
             {
+                // A client write lock is required here.
                 let mut client = provider.client.write().await;
                 if let Ok(transport_key) =
                     
provider.fetch_loadable_transport_key_from_keystore(keystore)
@@ -607,7 +611,7 @@
         };
         let prt = self.refresh_cache.refresh_token(&account_id).await?;
         self.client
-            .write()
+            .read()
             .await
             .exchange_prt_for_access_token(
                 &prt,
@@ -657,13 +661,13 @@
         };
         let cloud_ccache = self
             .client
-            .write()
+            .read()
             .await
             .fetch_cloud_ccache(&prt, tpm, machine_key)
             .unwrap_or(vec![]);
         let ad_ccache = self
             .client
-            .write()
+            .read()
             .await
             .fetch_ad_ccache(&prt, tpm, machine_key)
             .unwrap_or(vec![]);
@@ -692,7 +696,7 @@
         };
         let prt = self.refresh_cache.refresh_token(&account_id).await?;
         self.client
-            .write()
+            .read()
             .await
             .acquire_prt_sso_cookie(&prt, tpm, machine_key)
             .await
@@ -743,7 +747,7 @@
         // Set the hello pin
         let hello_key = match self
             .client
-            .write()
+            .read()
             .await
             .provision_hello_for_business_key(token, tpm, machine_key, new_tok)
             .await
@@ -816,7 +820,7 @@
                         // Check if the user exists
                         let auth_init = net_down_check!(
                             self.client
-                                .write()
+                                .read()
                                 .await
                                 .check_user_exists(&account_id, &[])
                                 .await,
@@ -899,7 +903,7 @@
         };
         let mtoken = self
             .client
-            .write()
+            .read()
             .await
             .exchange_prt_for_access_token(&prt, scopes, None, client_id, tpm, 
machine_key)
             .await;
@@ -912,7 +916,7 @@
                 fake_user!()
             }
         );
-        match self.token_validate(&account_id, &token).await {
+        match self.token_validate(&account_id, &token, old_token).await {
             Ok(AuthResult::Success { mut token }) => {
                 /* Set the GECOS from the old_token, since MS doesn't
                  * provide this during a silent acquire
@@ -979,10 +983,13 @@
                 return Ok((AuthRequest::Password, AuthCredHandler::None));
             }
             if self.config.read().await.get_enable_experimental_mfa() {
-                let auth_options = vec![AuthOption::Fido, 
AuthOption::Passwordless];
+                let mut auth_options = vec![AuthOption::Fido, 
AuthOption::Passwordless];
+                if 
self.config.read().await.get_enable_experimental_passwordless_fido() {
+                    auth_options.push(AuthOption::PasswordlessFido);
+                }
                 let auth_init = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .check_user_exists(account_id, &auth_options)
                         .await,
@@ -996,7 +1003,7 @@
                 } else {
                     let flow = net_down_check!(
                         self.client
-                            .write()
+                            .read()
                             .await
                             
.initiate_acquire_token_by_mfa_flow_for_device_enrollment(
                                 account_id,
@@ -1031,7 +1038,7 @@
             } else {
                 let resp = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .initiate_device_flow_for_device_enrollment()
                         .await,
@@ -1146,7 +1153,7 @@
                 };
                 let mtoken2 = self
                     .client
-                    .write()
+                    .read()
                     .await
                     .acquire_token_by_refresh_token(
                         &$token.refresh_token,
@@ -1172,7 +1179,7 @@
                                     sleep(Duration::from_secs(5));
                                     net_down_check!(
                                         self.client
-                                            .write()
+                                            .read()
                                             .await
                                             .acquire_token_by_refresh_token(
                                                 &$token.refresh_token,
@@ -1214,7 +1221,7 @@
                 };
                 let token = match self
                     .client
-                    .write()
+                    .read()
                     .await
                     .acquire_token_by_hello_for_business_key(
                         account_id,
@@ -1255,7 +1262,7 @@
                     }
                 };
 
-                match self.token_validate(account_id, &token).await {
+                match self.token_validate(account_id, &token, None).await {
                     Ok(AuthResult::Success { token }) => {
                         debug!("Returning user token from successful Hello PIN 
authentication.");
                         Ok((AuthResult::Success { token }, 
AuthCacheAction::None))
@@ -1283,7 +1290,7 @@
 
                 let hello_key = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .provision_hello_for_business_key(token, tpm, 
machine_key, &pin)
                         .await,
@@ -1330,7 +1337,7 @@
                     // we'll make another run at it in a moment.
                     let _ = net_down_check!(
                         self.client
-                            .write()
+                            .read()
                             .await
                             .handle_password_change(account_id, old_cred, 
&cred)
                             .await,
@@ -1350,6 +1357,9 @@
                 // Prohibit Fido over ssh (since it can't work)
                 if service != "ssh" {
                     opts.push(AuthOption::Fido);
+                    if 
self.config.read().await.get_enable_experimental_passwordless_fido() {
+                        opts.push(AuthOption::PasswordlessFido);
+                    }
                 }
                 // If SFA is enabled, disable the DAG fallback, otherwise SFA 
users
                 // will always be prompted for DAG.
@@ -1359,7 +1369,7 @@
                 }
                 let mresp = self
                     .client
-                    .write()
+                    .read()
                     .await
                     .initiate_acquire_token_by_mfa_flow_for_device_enrollment(
                         account_id,
@@ -1400,7 +1410,7 @@
                                 // will deadlock.
                                 let res = self
                                     .client
-                                    .write()
+                                    .read()
                                     .await
                                     .acquire_token_by_username_password(
                                         account_id,
@@ -1449,7 +1459,7 @@
                             }
                         };
                         let token2 = enroll_and_obtain_enrolled_token!(token);
-                        return match self.token_validate(account_id, 
&token2).await {
+                        return match self.token_validate(account_id, &token2, 
None).await {
                             Ok(AuthResult::Success { token }) => {
                                 // STOP! If we just enrolled with an SFA 
token, then we
                                 // need to bail out here and refuse Hello 
enrollment
@@ -1487,7 +1497,7 @@
                             AuthCacheAction::None,
                         ));
                     }
-                    "PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
+                    "AccessPass" | "PhoneAppOTP" | "OneWaySMS" | 
"ConsolidatedTelephony" => {
                         let msg = resp.msg.clone();
                         *cred_handler = AuthCredHandler::MFA {
                             flow: resp,
@@ -1534,7 +1544,7 @@
             ) => {
                 let token = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .acquire_token_by_mfa_flow(account_id, Some(&cred), 
None, flow)
                         .await,
@@ -1567,7 +1577,7 @@
                     }
                 );
                 let token2 = enroll_and_obtain_enrolled_token!(token);
-                match self.token_validate(account_id, &token2).await {
+                match self.token_validate(account_id, &token2, None).await {
                     Ok(AuthResult::Success { token: token3 }) => {
                         // Skip Hello enrollment if it is disabled by config
                         let hello_enabled = 
self.config.read().await.get_enable_hello();
@@ -1615,7 +1625,7 @@
                 }
                 let token = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .acquire_token_by_mfa_flow(account_id, None, 
Some(poll_attempt), flow)
                         .await,
@@ -1655,7 +1665,7 @@
                     }
                 );
                 let token2 = enroll_and_obtain_enrolled_token!(token);
-                match self.token_validate(account_id, &token2).await {
+                match self.token_validate(account_id, &token2, None).await {
                     Ok(AuthResult::Success { token: token3 }) => {
                         // Skip Hello enrollment if it is disabled by config
                         let hello_enabled = 
self.config.read().await.get_enable_hello();
@@ -1698,7 +1708,7 @@
             ) => {
                 let token = net_down_check!(
                     self.client
-                        .write()
+                        .read()
                         .await
                         .acquire_token_by_mfa_flow(account_id, 
Some(&assertion), None, flow)
                         .await,
@@ -1731,7 +1741,7 @@
                     }
                 );
                 let token2 = enroll_and_obtain_enrolled_token!(token);
-                match self.token_validate(account_id, &token2).await {
+                match self.token_validate(account_id, &token2, None).await {
                     Ok(AuthResult::Success { token: token3 }) => {
                         // Skip Hello enrollment if it is disabled by config
                         let hello_enabled = 
self.config.read().await.get_enable_hello();
@@ -1908,6 +1918,7 @@
 
             // Set the authority on the app
             let authority_url = format!("https://{}/{}";, authority_host, 
tenant_id);
+            // A client write lock is required here.
             self.client
                 .write()
                 .await
@@ -1976,7 +1987,7 @@
     }
 
     fn fetch_hello_key_tag(&self, account_id: &str) -> String {
-        format!("{}/hello", account_id)
+        format!("{}/hello", account_id.to_lowercase())
     }
 
     fn fetch_cert_key_tag(&self) -> String {
@@ -2020,6 +2031,7 @@
         &self,
         account_id: &str,
         token: &UnixUserToken,
+        old_token: Option<&UserToken>,
     ) -> Result<AuthResult, IdpError> {
         match &token.access_token {
             Some(_) => {
@@ -2044,7 +2056,9 @@
                     self.refresh_cache.add(account_id, prt).await;
                 }
                 Ok(AuthResult::Success {
-                    token: self.user_token_from_unix_user_token(token).await?,
+                    token: self
+                        .user_token_from_unix_user_token(token, old_token)
+                        .await?,
                 })
             }
             None => {
@@ -2057,6 +2071,7 @@
     async fn user_token_from_unix_user_token(
         &self,
         value: &UnixUserToken,
+        old_token: Option<&UserToken>,
     ) -> Result<UserToken, IdpError> {
         let config = self.config.read().await;
         let mut groups: Vec<GroupToken>;
@@ -2092,7 +2107,13 @@
                     }
                     Err(_e) => {
                         debug!("Failed fetching user groups for {}", &spn);
-                        vec![]
+                        /* If we failed to fetch the groups, and we have an old
+                         * token, preserve the existing cached group 
memberships.
+                         */
+                        match old_token {
+                            Some(old_token) => old_token.groups.clone(),
+                            None => vec![],
+                        }
                     }
                 };
                 posix_attrs = if config.get_id_attr_map() == IdAttr::Rfc2307 {
@@ -2122,7 +2143,13 @@
             }
             None => {
                 debug!("Failed fetching user groups for {}", &spn);
-                groups = vec![];
+                /* If we failed to fetch the groups, and we have an old
+                 * token, preserve the existing cached group memberships.
+                 */
+                groups = match old_token {
+                    Some(old_token) => old_token.groups.clone(),
+                    None => vec![],
+                };
                 posix_attrs = HashMap::new();
             }
         };
@@ -2315,6 +2342,7 @@
     ) -> Result<(), MsalError> {
         /* If not already joined, join the domain now. */
         let attrs = EnrollAttrs::new(self.domain.clone(), None, None, None, 
None)?;
+        // A client write lock is required here.
         match self
             .client
             .write()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/resolver.rs 
new/himmelblau-0.9.16+git.0.aac2205/src/common/src/resolver.rs
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/common/src/resolver.rs      
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/common/src/resolver.rs      
2025-06-17 20:44:46.000000000 +0200
@@ -933,10 +933,15 @@
             Ok(Some(true))
         } else {
             Ok(token.map(|tok| {
+                // Never ever EVER list group names in the allowed groups set.
+                // This is a SECURITY RISK! See CVE-2025-49012. Group names ARE
+                // NOT unique in Entra Id. Only group Object Ids may be listed
+                // here.
                 let user_set: BTreeSet<_> = tok
                     .groups
                     .iter()
-                    .flat_map(|g| [g.name.clone(), 
g.uuid.hyphenated().to_string()])
+                    .flat_map(|g| [g.uuid.hyphenated().to_string()])
+                    .chain(std::iter::once(account_id.to_string()))
                     .collect();
 
                 debug!(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/config/himmelblau.conf.example 
new/himmelblau-0.9.16+git.0.aac2205/src/config/himmelblau.conf.example
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/config/himmelblau.conf.example      
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/config/himmelblau.conf.example      
2025-06-17 20:44:46.000000000 +0200
@@ -84,6 +84,11 @@
 # Grant) flow for MFA, and Hello authentication will be disabled.
 # enable_experimental_mfa = true
 #
+# This option enables the experimental passwordless FIDO flow for Azure Entra 
ID
+# authentication. When enabled, Himmelblau will attempt to initiate Entra ID
+# login using a FIDO2 security key without requiring a password.
+# enable_experimental_passwordless_fido = true
+#
 # In some cases, mapping the UPN to the CN may be impractical. The following
 # option executes the specified filename (any executable), which MUST accept
 # a single argument representing a mapped name. The executable MUST print
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/daemon/Cargo.toml 
new/himmelblau-0.9.16+git.0.aac2205/src/daemon/Cargo.toml
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/daemon/Cargo.toml   2025-05-20 
05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/daemon/Cargo.toml   2025-06-17 
20:44:46.000000000 +0200
@@ -30,7 +30,7 @@
 serde_json.workspace = true
 futures = "^0.3.28"
 systemd-journal-logger = "^2.1.1"
-users = "^0.11.0"
+uzers = "^0.11.0"
 sketching = { workspace = true }
 walkdir = { workspace = true }
 libc = { workspace = true }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/nss/src/implementation.rs 
new/himmelblau-0.9.16+git.0.aac2205/src/nss/src/implementation.rs
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/nss/src/implementation.rs   
2025-05-20 05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/nss/src/implementation.rs   
2025-06-17 20:44:46.000000000 +0200
@@ -282,7 +282,7 @@
             }
         };
 
-        daemon_client
+        let resp = match daemon_client
             .call_and_wait(&req, cfg.get_unix_sock_timeout())
             .map(|r| match r {
                 ClientResponse::NssGroup(opt) => opt
@@ -300,6 +300,52 @@
                 _ => Response::NotFound,
             })
             .unwrap_or_else(|_| Response::NotFound)
+        {
+            Response::NotFound => {
+                // If the mapped UPN name isn't found, then this is probably a
+                // real Entra Id group, instead of a fake primary group.
+                let req = ClientRequest::NssGroupByName(name.clone());
+                daemon_client
+                    .call_and_wait(&req, cfg.get_unix_sock_timeout())
+                    .map(|r| match r {
+                        ClientResponse::NssGroup(opt) => opt
+                            .map(|ng| {
+                                let mut group = group_from_nssgroup(ng);
+                                group.members = group
+                                    .members
+                                    .into_iter()
+                                    .map(|member| cfg.map_upn_to_name(&member))
+                                    .collect();
+                                Response::Success(group)
+                            })
+                            .unwrap_or_else(|| Response::NotFound),
+                        _ => Response::NotFound,
+                    })
+                    .unwrap_or_else(|_| Response::NotFound)
+            }
+            other => other,
+        };
+        match resp {
+            Response::Success(group) => {
+                // Never ever EVER respond to a group request by Entra Id group
+                // name. This is a SECURITY RISK! See CVE-2025-49012. Group
+                // names ARE NOT unique in Entra Id. Responding to this name
+                // request could expose SUDO and other privileged commands to
+                // an attacker. Admins should only ever specify group names in
+                // configuration via the Object Id GUID or the GID. Ignoring
+                // this request will still permit commands such as `ls`, etc
+                // to display the group name, while prohibiting dangerous
+                // behavior.
+                if group.name.to_lowercase() == name.to_lowercase() {
+                    return Response::NotFound;
+                } else {
+                    return Response::Success(group);
+                }
+            }
+            _ => {
+                return resp;
+            }
+        }
     }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/himmelblau-0.9.12+git.0.99b5ca6/src/pam/src/pam/mod.rs 
new/himmelblau-0.9.16+git.0.aac2205/src/pam/src/pam/mod.rs
--- old/himmelblau-0.9.12+git.0.99b5ca6/src/pam/src/pam/mod.rs  2025-05-20 
05:21:01.000000000 +0200
+++ new/himmelblau-0.9.16+git.0.aac2205/src/pam/src/pam/mod.rs  2025-06-17 
20:44:46.000000000 +0200
@@ -1274,7 +1274,46 @@
             };
 
             match mfa_req.mfa_method.as_str() {
-                "PhoneAppOTP" | "OneWaySMS" | "ConsolidatedTelephony" => {
+                "FidoKey" => {
+                    let conv = Arc::new(Mutex::new(conv.clone()));
+                    let fido_challenge = match mfa_req.fido_challenge {
+                        Some(ref fido_challenge) => fido_challenge.clone(),
+                        None => {
+                            debug!("no Fido challenge");
+                            return PamResultCode::PAM_CRED_INSUFFICIENT;
+                        }
+                    };
+
+                    let fido_allow_list = match mfa_req.fido_allow_list {
+                        Some(ref fido_allow_list) => fido_allow_list.clone(),
+                        None => {
+                            debug!("no Fido allow list");
+                            return PamResultCode::PAM_CRED_INSUFFICIENT;
+                        }
+                    };
+
+                    let assertion = match fido_auth(conv.clone(), 
fido_challenge, fido_allow_list) {
+                        Ok(assertion) => assertion,
+                        Err(e) => {
+                            pam_fail!(
+                                conv.lock().unwrap(),
+                                "Entra Id Fido authentication failed.",
+                                e
+                            );
+                        },
+                    };
+                    match rt.block_on(async {
+                        app.acquire_token_by_mfa_flow(&account_id, 
Some(&assertion), None, &mut mfa_req)
+                            .await
+                    }) {
+                        Ok(token) => token,
+                        Err(e) => {
+                            error!("MFA FAIL: {:?}", e);
+                            return PamResultCode::PAM_AUTH_ERR;
+                        }
+                    }
+                }
+                "AccessPass" | "PhoneAppOTP" | "OneWaySMS" | 
"ConsolidatedTelephony" => {
                     let input = match conv.send(PAM_PROMPT_ECHO_OFF, 
&mfa_req.msg) {
                         Ok(password) => match password {
                             Some(cred) => cred,

++++++ vendor.tar.zst ++++++
/work/SRC/openSUSE:Factory/himmelblau/vendor.tar.zst 
/work/SRC/openSUSE:Factory/.himmelblau.new.19631/vendor.tar.zst differ: char 7, 
line 1

Reply via email to