@@ -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