@@ -6,7 +6,7 @@ use std::{
66
77use byteorder::{LittleEndian, WriteBytesExt};
88use krb5::{Keyblock, Keytab, KrbContext, Principal, PrincipalUnparseOptions};
9- use ldap3::{Ldap, LdapConnAsync, LdapConnSettings};
9+ use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry };
1010use rand::{seq::IndexedRandom, CryptoRng};
1111use snafu::{OptionExt, ResultExt, Snafu};
1212use stackable_krb5_provision_keytab::ActiveDirectorySamAccountNameRules;
@@ -70,6 +70,15 @@ pub enum Error {
7070
7171 #[snafu(display("configured samAccountName prefix is longer than the requested length"))]
7272 SamAccountNamePrefixLongerThanRequestedLength,
73+
74+ #[snafu(display("failed to execute LDAP search"))]
75+ SearchLdap { source: ldap3::LdapError },
76+
77+ #[snafu(display("failed to extract successful LDAP search"))]
78+ SearchLdapSuccess { source: ldap3::LdapError },
79+
80+ #[snafu(display("the user did not have an associated kvno"))]
81+ KvnoNotFound,
7382}
7483pub type Result<T, E = Error> = std::result::Result<T, E>;
7584
@@ -136,9 +145,7 @@ impl<'a> AdAdmin<'a> {
136145 principal: &Principal<'_>,
137146 kt: &mut Keytab<'_>,
138147 ) -> Result<()> {
139- let princ_name = principal
140- .unparse(PrincipalUnparseOptions::default())
141- .context(UnparsePrincipalSnafu)?;
148+ let princ_name = get_principal_data(principal)?.princ_name;
142149 let password_cache_key = princ_name.replace(['/', '@'], "__");
143150 let password = self
144151 .password_cache
@@ -162,18 +169,27 @@ impl<'a> AdAdmin<'a> {
162169 // FIXME: What about cases where ldap.add() succeeds but not the cache write?
163170 .context(PasswordCacheSnafu)??;
164171 let password_c = CString::new(password).context(DecodePasswordSnafu)?;
165- principal
166- .default_salt()
167- .and_then(|salt| {
168- Keyblock::from_password(
169- self.krb,
170- krb5::enctype::AES256_CTS_HMAC_SHA1_96,
171- &password_c,
172- &salt,
173- )
174- })
175- .and_then(|key| kt.add(principal, 0, &key.as_ref()))
176- .context(AddToKeytabSnafu)?;
172+
173+ let kvno = get_user_kvno(&mut self.ldap, principal, &self.user_distinguished_name).await?;
174+ if let Some(kvno) = kvno {
175+ principal
176+ .default_salt()
177+ .and_then(|salt| {
178+ Keyblock::from_password(
179+ self.krb,
180+ krb5::enctype::AES256_CTS_HMAC_SHA1_96,
181+ &password_c,
182+ &salt,
183+ )
184+ })
185+ .and_then(|key| kt.add(principal, kvno, &key.as_ref()))
186+ .context(AddToKeytabSnafu)?;
187+ } else {
188+ // If we can't detect the kvno then some applications may not
189+ // authenticate if the keytab/kvno does not match the kvno of the
190+ // ticket from the KDC. So always throw an exception.
191+ return Err(Error::KvnoNotFound);
192+ }
177193 Ok(())
178194 }
179195}
@@ -262,18 +278,12 @@ async fn create_ad_user(
262278 const AD_ENCTYPE_AES256_HMAC_SHA1: u32 = 0x10;
263279
264280 tracing::info!("creating principal");
265- let princ_name = principal
266- .unparse(PrincipalUnparseOptions::default())
267- .context(UnparsePrincipalSnafu)?;
268- let princ_name_realmless = principal
269- .unparse(PrincipalUnparseOptions {
270- realm: krb5::PrincipalRealmDisplayMode::Never,
271- ..Default::default()
272- })
273- .context(UnparsePrincipalSnafu)?;
274- let principal_cn = ldap3::dn_escape(&princ_name);
275- // FIXME: AD restricts RDNs to 64 characters
276- let principal_cn = principal_cn.get(..64).unwrap_or(&*principal_cn);
281+
282+ let principal_data = get_principal_data(principal)?;
283+ let princ_name = principal_data.princ_name;
284+ let principal_cn = principal_data.principal_cn;
285+ let princ_name_realmless = principal_data.princ_name_realmless;
286+
277287 let sam_account_name = generate_sam_account_name
278288 .map(|sam_rules| {
279289 let mut name = sam_rules.prefix.clone();
@@ -350,3 +360,71 @@ async fn create_ad_user(
350360 };
351361 Ok(())
352362}
363+
364+ pub struct PrincipalData {
365+ princ_name: String,
366+ principal_cn: String,
367+ princ_name_realmless: String,
368+ }
369+
370+ fn get_principal_data(principal: &Principal<'_>) -> Result<PrincipalData> {
371+ let princ_name = principal
372+ .unparse(PrincipalUnparseOptions::default())
373+ .context(UnparsePrincipalSnafu)?;
374+ let principal_cn = ldap3::dn_escape(&princ_name);
375+ // AD restricts RDNs to 64 characters
376+ let principal_cn = principal_cn.get(..64).unwrap_or(&*principal_cn).to_string();
377+ let princ_name_realmless = principal
378+ .unparse(PrincipalUnparseOptions {
379+ realm: krb5::PrincipalRealmDisplayMode::Never,
380+ ..Default::default()
381+ })
382+ .context(UnparsePrincipalSnafu)?;
383+ Ok(PrincipalData {
384+ princ_name,
385+ principal_cn,
386+ princ_name_realmless,
387+ })
388+ }
389+
390+ #[tracing::instrument(skip(ldap), fields(%principal, %user_dn_base))]
391+ async fn get_user_kvno(
392+ ldap: &mut Ldap,
393+ principal: &Principal<'_>,
394+ user_dn_base: &str,
395+ ) -> Result<Option<u32>> {
396+ let principal_cn = get_principal_data(principal)?.principal_cn;
397+ let distinguished_name = &format!("CN={principal_cn},{user_dn_base}");
398+ tracing::info!("searching for kvno using DN {distinguished_name}");
399+
400+ // Perform search with KVNO attribute
401+ let (search_results, _) = ldap
402+ .search(
403+ distinguished_name,
404+ Scope::Base,
405+ "(objectClass=user)",
406+ vec!["msDS-KeyVersionNumber"],
407+ )
408+ .await
409+ .context(SearchLdapSnafu)?
410+ .success()
411+ .context(SearchLdapSuccessSnafu)?;
412+
413+ let mut kvno = None;
414+
415+ // Extract KVNO from first result
416+ if let Some(entry) = search_results.into_iter().next() {
417+ let entry = SearchEntry::construct(entry);
418+ tracing::debug!("detected search result entry {:?}", entry);
419+ kvno = entry
420+ .attrs
421+ .get("msDS-KeyVersionNumber")
422+ .and_then(|v| v.first())
423+ .and_then(|s| s.parse::<u32>().ok());
424+ tracing::debug!("detected kvno {:?} for DN {distinguished_name}", kvno);
425+ } else {
426+ tracing::info!("no kvno detected for DN {distinguished_name}");
427+ }
428+
429+ Ok(kvno)
430+ }
0 commit comments