Skip to content

Commit f8507dc

Browse files
authored
Merge pull request xapi-project#4478 from xapi-project/private/linl/winbind
CA-356901: Perform ldap query if winbind failed to resovle subject
2 parents f9af7d4 + f1a520d commit f8507dc

File tree

3 files changed

+186
-60
lines changed

3 files changed

+186
-60
lines changed

ocaml/tests/test_extauth_plugin_ADwinbind.ml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,28 @@ let test_domainify_uname =
112112
|> List.map @@ fun (inp, exp) ->
113113
(Printf.sprintf "%s -> %s" inp exp, `Quick, check inp exp)
114114

115+
let test_ldap_escape =
116+
let open Extauth_plugin_ADwinbind.Ldap in
117+
let check str exp () =
118+
let msg = Printf.sprintf "%s -> %s" str exp in
119+
let escaped = escape str in
120+
Alcotest.(check string) msg exp escaped
121+
in
122+
let matrix =
123+
[
124+
({|user|}, {|user|})
125+
; ({|(user)|}, {|\28user\29|})
126+
; ({|(user|}, {|\28user|})
127+
; ({|user)|}, {|user\29|})
128+
; ({|us\er)|}, {|us\5der\29|})
129+
; ({|user)1|}, {|user\291|})
130+
; ({|user*|}, {|user\2a|})
131+
]
132+
in
133+
matrix
134+
|> List.map @@ fun (inp, exp) ->
135+
(Printf.sprintf "%s -> %s" inp exp, `Quick, check inp exp)
136+
115137
let test_parse_wbinfo_uid_info =
116138
let open Extauth_plugin_ADwinbind.Wbinfo in
117139
let string_of_result x =
@@ -424,6 +446,7 @@ let tests =
424446
; ("ADwinbind:test_range", Range.tests)
425447
; ("ADwinbind:test_parse_value_from_pbis", ParseValueFromPbis.tests)
426448
; ("ADwinbind:test_domainify_uname", test_domainify_uname)
449+
; ("ADwinbind:test_ldap_escape", test_ldap_escape)
427450
; ("ADwinbind:test_parse_wbinfo_uid_info", test_parse_wbinfo_uid_info)
428451
; ("ADwinbind:test_parse_ldap_stdout", test_parse_ldap_stdout)
429452
; ( "ADwinbind:test_wbinfo_exception_of_stderr"

ocaml/xapi/extauth_plugin_ADwinbind.ml

Lines changed: 161 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ let domain_krb5_dir = Filename.concat Xapi_globs.samba_dir "lock/smb_krb5"
6262

6363
let debug_level () = !Xapi_globs.winbind_debug_level |> string_of_int
6464

65+
type domain_info = {
66+
service_name: string
67+
; workgroup: string option
68+
(* For upgrade case, the legacy db does not contain workgroup *)
69+
; netbios_name: string option
70+
(* Persist netbios_name to support hostname change *)
71+
}
72+
6573
let hd msg = function
6674
| [] ->
6775
error "%s" msg ;
@@ -81,7 +89,62 @@ let ntlm_auth uname passwd : (unit, exn) result =
8189
Ok ()
8290
with _ -> Error (auth_ex uname)
8391

92+
let get_domain_info_from_db () =
93+
Server_helpers.exec_with_new_task "retrieving external auth domain workgroup"
94+
@@ fun __context ->
95+
let host = Helpers.get_localhost ~__context in
96+
let service_name =
97+
Db.Host.get_external_auth_service_name ~__context ~self:host
98+
in
99+
let workgroup, netbios_name =
100+
Db.Host.get_external_auth_configuration ~__context ~self:host
101+
|> fun config ->
102+
(List.assoc_opt "workgroup" config, List.assoc_opt "netbios_name" config)
103+
in
104+
{service_name; workgroup; netbios_name}
105+
84106
module Ldap = struct
107+
module Escape = struct
108+
(*
109+
* Escape characters according to
110+
* https://docs.microsoft.com/en-gb/windows/win32/adsi/search-filter-syntax?redirectedfrom=MSDN#special-characters
111+
* *)
112+
113+
let reg_star = {|*|} |> Re.str |> Re.compile
114+
115+
let reg_left_bracket = {|(|} |> Re.str |> Re.compile
116+
117+
let reg_right_bracket = {|)|} |> Re.str |> Re.compile
118+
119+
let reg_backward_slash = {|\|} |> Re.str |> Re.compile
120+
121+
let reg_null = "\000" |> Re.str |> Re.compile
122+
123+
let reg_slash = {|/|} |> Re.str |> Re.compile
124+
125+
let escape_map =
126+
[
127+
(* backward slash goes first as others will include backward slash*)
128+
(reg_backward_slash, {|\5d|})
129+
; (reg_star, {|\2a|})
130+
; (reg_left_bracket, {|\28|})
131+
; (reg_right_bracket, {|\29|})
132+
; (reg_null, {|\00|})
133+
; (reg_slash, {|\2f|})
134+
]
135+
136+
let escape str =
137+
List.fold_left
138+
(fun acc element ->
139+
let reg = fst element in
140+
let value = snd element in
141+
Re.replace_string reg ~by:value acc
142+
)
143+
str escape_map
144+
end
145+
146+
let escape str = Escape.escape str
147+
85148
type user = {
86149
name: string
87150
; display_name: string
@@ -200,12 +263,15 @@ module Ldap = struct
200263
; password_expired= logand user_account_control passw_expire_bit <> 0l
201264
}
202265

203-
let query_user sid domain_netbios kdc =
266+
let env_of_krb5 domain_netbios =
204267
let domain_krb5_cfg =
205268
Filename.concat domain_krb5_dir
206269
(Printf.sprintf "krb5.conf.%s" domain_netbios)
207270
in
208-
let env = [|Printf.sprintf "KRB5_CONFIG=%s" domain_krb5_cfg|] in
271+
[|Printf.sprintf "KRB5_CONFIG=%s" domain_krb5_cfg|]
272+
273+
let query_user sid domain_netbios kdc =
274+
let env = env_of_krb5 domain_netbios in
209275
let* stdout =
210276
try
211277
(* Query KDC instead of use domain here
@@ -227,9 +293,40 @@ module Ldap = struct
227293
args
228294
in
229295
Ok stdout
230-
with _ -> Error (generic_ex "ldap query failed")
296+
with _ -> Error (generic_ex "ldap query user info from sid failed")
231297
in
232298
parse_user stdout <!> generic_ex "%s"
299+
300+
let query_sid ~name ~kdc ~domain_netbios =
301+
let key = "objectSid" in
302+
let env = env_of_krb5 domain_netbios in
303+
let name = escape name in
304+
(* Escape name to avoid injection detection *)
305+
let query = Printf.sprintf "(|(sAMAccountName=%s)(name=%s))" name name in
306+
let args =
307+
[
308+
"ads"
309+
; "search"
310+
; "-d"
311+
; debug_level ()
312+
; "--server"
313+
; kdc
314+
; "--machine-pass"
315+
; query
316+
; key
317+
]
318+
in
319+
try
320+
Helpers.call_script ~env !Xapi_globs.net_cmd args
321+
|> Xapi_cmd_result.of_output ~sep:':' ~key
322+
|> fun x -> Ok x
323+
with
324+
| Forkhelpers.Spawn_internal_error (_, stdout, _) ->
325+
Error (generic_ex "Ldap query sid failed: %s" stdout)
326+
| Not_found ->
327+
Error (generic_ex "%s not found in ldap result" key)
328+
| _ ->
329+
Error (generic_ex "Failed to lookup sid from username %s" name)
233330
end
234331

235332
type domain_name_type = Name | NetbiosName
@@ -345,6 +442,25 @@ module Wbinfo = struct
345442
| _ ->
346443
Error (generic_ex "Invalid domain user name %s" uname)
347444

445+
let domain_and_user_of_uname uname =
446+
let open Astring.String in
447+
match String.split_on_char '\\' uname with
448+
| [netbios; user] ->
449+
let* domain =
450+
domain_name_of ~target_name_type:Name ~from_name:netbios
451+
in
452+
Ok (domain, user)
453+
| _ -> (
454+
match String.split_on_char '@' uname with
455+
| [user; domain] ->
456+
Ok (domain, user)
457+
| _ ->
458+
if is_infix ~affix:"@" uname || is_infix ~affix:{|\|} uname then
459+
Error (generic_ex "Invalid domain user name %s" uname)
460+
else
461+
Ok ((get_domain_info_from_db ()).service_name, uname)
462+
)
463+
348464
let all_domain_netbios () =
349465
(*
350466
* List all domains (trusted and own domain)
@@ -445,14 +561,6 @@ module Wbinfo = struct
445561
parse_uid_info stdout <!> fun () -> parsing_ex args
446562
end
447563

448-
type domain_info = {
449-
service_name: string
450-
; workgroup: string option
451-
(* For upgrade case, the legacy db does not contain workgroup *)
452-
; netbios_name: string option
453-
(* Persist netbios_name to support hostname change *)
454-
}
455-
456564
module Migrate_from_pbis = struct
457565
(* upgrade-pbis-to-winbind handles most of the migration from PBIS database
458566
* to winbind database
@@ -532,21 +640,6 @@ module Migrate_from_pbis = struct
532640
netbios_name
533641
end
534642

535-
let get_domain_info_from_db () =
536-
(fun __context ->
537-
let host = Helpers.get_localhost ~__context in
538-
let service_name =
539-
Db.Host.get_external_auth_service_name ~__context ~self:host
540-
in
541-
let workgroup, netbios_name =
542-
Db.Host.get_external_auth_configuration ~__context ~self:host |> fun l ->
543-
(List.assoc_opt "workgroup" l, List.assoc_opt "netbios_name" l)
544-
in
545-
{service_name; workgroup; netbios_name}
546-
)
547-
|> Server_helpers.exec_with_new_task
548-
"retrieving external auth domain workgroup"
549-
550643
let kdcs_of_domain domain =
551644
try
552645
Helpers.call_script ~log_output:On_failure net_cmd
@@ -625,11 +718,9 @@ let from_config ~name ~err_msg ~config_params =
625718
let all_number_re = Re.Perl.re {|^\d+$|} |> Re.Perl.compile
626719

627720
let get_localhost_name () =
628-
(fun __context ->
629-
Helpers.get_localhost ~__context |> fun host ->
630-
Db.Host.get_hostname ~__context ~self:host
631-
)
632-
|> Server_helpers.exec_with_new_task "retrieving hostname"
721+
Server_helpers.exec_with_new_task "retrieving hostname" @@ fun __context ->
722+
Helpers.get_localhost ~__context |> fun host ->
723+
Db.Host.get_hostname ~__context ~self:host
633724

634725
let assert_hostname_valid ~hostname =
635726
let all_numbers = Re.matches all_number_re hostname <> [] in
@@ -665,13 +756,12 @@ let persist_extauth_config ~domain ~user ~ou_conf ~workgroup ~netbios_name =
665756
]
666757
@ ou_conf
667758
in
668-
(fun __context ->
669-
Helpers.get_localhost ~__context |> fun self ->
670-
Db.Host.set_external_auth_configuration ~__context ~self ~value ;
671-
Db.Host.get_name_label ~__context ~self
672-
|> debug "update external_auth_configuration for host %s"
673-
)
674-
|> Server_helpers.exec_with_new_task "update external_auth_configuration"
759+
Server_helpers.exec_with_new_task "update external_auth_configuration"
760+
@@ fun __context ->
761+
Helpers.get_localhost ~__context |> fun self ->
762+
Db.Host.set_external_auth_configuration ~__context ~self ~value ;
763+
Db.Host.get_name_label ~__context ~self
764+
|> debug "update external_auth_configuration for host %s"
675765

676766
let disable_machine_account ~service_name = function
677767
| Some u, Some p -> (
@@ -833,21 +923,19 @@ module ClosestKdc = struct
833923
Error e
834924

835925
let update_db ~domain ~kdc =
836-
(fun __context ->
837-
let self = Helpers.get_localhost ~__context in
838-
Db.Host.get_external_auth_configuration ~__context ~self |> fun value ->
839-
(domain, kdc) :: List.remove_assoc domain value |> fun value ->
840-
Db.Host.set_external_auth_configuration ~__context ~self ~value
841-
)
842-
|> Server_helpers.exec_with_new_task "update domain closest kdc"
926+
Server_helpers.exec_with_new_task "update domain closest kdc"
927+
@@ fun __context ->
928+
let self = Helpers.get_localhost ~__context in
929+
Db.Host.get_external_auth_configuration ~__context ~self |> fun value ->
930+
(domain, kdc) :: List.remove_assoc domain value |> fun value ->
931+
Db.Host.set_external_auth_configuration ~__context ~self ~value
843932

844933
let from_db domain =
845-
(fun __context ->
846-
let self = Helpers.get_localhost ~__context in
847-
Db.Host.get_external_auth_configuration ~__context ~self
848-
|> List.assoc_opt domain
849-
)
850-
|> Server_helpers.exec_with_new_task "query domain closest kdc"
934+
Server_helpers.exec_with_new_task "query domain closest kdc"
935+
@@ fun __context ->
936+
let self = Helpers.get_localhost ~__context in
937+
Db.Host.get_external_auth_configuration ~__context ~self
938+
|> List.assoc_opt domain
851939

852940
let lookup domain =
853941
try
@@ -921,13 +1009,33 @@ let build_dns_hostname_option ~config_params =
9211009
| _ ->
9221010
[]
9231011

1012+
let closest_kdc_of_domain domain =
1013+
match ClosestKdc.from_db domain with
1014+
| Some kdc ->
1015+
kdc
1016+
| None ->
1017+
(* Just pick the first KDC in the list *)
1018+
kdc_of_domain domain
1019+
9241020
module AuthADWinbind : Auth_signature.AUTH_MODULE = struct
9251021
let get_subject_identifier' subject_name =
1022+
(* Called in the login path with a yet unauthenticated user *)
9261023
let* domain =
9271024
try Ok (get_domain_info_from_db ()).service_name with e -> Error e
9281025
in
9291026
let subject_name = domainify_uname ~domain subject_name in
930-
Wbinfo.sid_of_name subject_name
1027+
match Wbinfo.sid_of_name subject_name with
1028+
| Ok sid ->
1029+
Ok sid
1030+
| Error e ->
1031+
debug "Failed to query sid from cache, error: %s, retry ldap"
1032+
(ExnHelper.string_of_exn e) ;
1033+
let* domain, user = Wbinfo.domain_and_user_of_uname subject_name in
1034+
let* domain_netbios =
1035+
Wbinfo.domain_name_of ~target_name_type:NetbiosName ~from_name:domain
1036+
in
1037+
let kdc = closest_kdc_of_domain domain in
1038+
Ldap.query_sid ~name:user ~kdc ~domain_netbios
9311039

9321040
(* subject_id get_subject_identifier(string subject_name)
9331041
@@ -1004,14 +1112,7 @@ module AuthADWinbind : Auth_signature.AUTH_MODULE = struct
10041112
let query_subject_information_user (uid : int) (sid : string) =
10051113
let* {user_name; gecos; gid} = Wbinfo.uid_info_of_uid uid in
10061114
let* domain_netbios, domain = Wbinfo.domain_of_uname user_name in
1007-
let closest_kdc =
1008-
match ClosestKdc.from_db domain with
1009-
| Some kdc ->
1010-
kdc
1011-
| None ->
1012-
(* Just pick the first KDC in the list *)
1013-
kdc_of_domain domain
1014-
in
1115+
let closest_kdc = closest_kdc_of_domain domain in
10151116
let* {
10161117
name
10171118
; upn

ocaml/xapi/extauth_plugin_ADwinbind.mli

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ module Ldap : sig
6161
val string_of_user : user -> string
6262

6363
val parse_user : string -> (user, string) result
64+
65+
val escape : string -> string
6466
end
6567

6668
module Migrate_from_pbis : sig

0 commit comments

Comments
 (0)