@@ -6,7 +6,7 @@ use std::{
6
6
7
7
use byteorder:: { LittleEndian , WriteBytesExt } ;
8
8
use krb5:: { Keyblock , Keytab , KrbContext , Principal , PrincipalUnparseOptions } ;
9
- use ldap3:: { Ldap , LdapConnAsync , LdapConnSettings } ;
9
+ use ldap3:: { Ldap , LdapConnAsync , LdapConnSettings , Scope , SearchEntry } ;
10
10
use rand:: { seq:: IndexedRandom , CryptoRng } ;
11
11
use snafu:: { OptionExt , ResultExt , Snafu } ;
12
12
use stackable_krb5_provision_keytab:: ActiveDirectorySamAccountNameRules ;
@@ -70,6 +70,15 @@ pub enum Error {
70
70
71
71
#[ snafu( display( "configured samAccountName prefix is longer than the requested length" ) ) ]
72
72
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 ,
73
82
}
74
83
pub type Result < T , E = Error > = std:: result:: Result < T , E > ;
75
84
@@ -136,9 +145,7 @@ impl<'a> AdAdmin<'a> {
136
145
principal : & Principal < ' _ > ,
137
146
kt : & mut Keytab < ' _ > ,
138
147
) -> Result < ( ) > {
139
- let princ_name = principal
140
- . unparse ( PrincipalUnparseOptions :: default ( ) )
141
- . context ( UnparsePrincipalSnafu ) ?;
148
+ let princ_name = get_principal_data ( principal) ?. princ_name ;
142
149
let password_cache_key = princ_name. replace ( [ '/' , '@' ] , "__" ) ;
143
150
let password = self
144
151
. password_cache
@@ -162,18 +169,27 @@ impl<'a> AdAdmin<'a> {
162
169
// FIXME: What about cases where ldap.add() succeeds but not the cache write?
163
170
. context ( PasswordCacheSnafu ) ??;
164
171
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
+ }
177
193
Ok ( ( ) )
178
194
}
179
195
}
@@ -262,18 +278,12 @@ async fn create_ad_user(
262
278
const AD_ENCTYPE_AES256_HMAC_SHA1 : u32 = 0x10 ;
263
279
264
280
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
+
277
287
let sam_account_name = generate_sam_account_name
278
288
. map ( |sam_rules| {
279
289
let mut name = sam_rules. prefix . clone ( ) ;
@@ -350,3 +360,71 @@ async fn create_ad_user(
350
360
} ;
351
361
Ok ( ( ) )
352
362
}
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