1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections.Generic; 7 using System.Diagnostics; 8 using System.Globalization; 9 using System.Runtime.InteropServices; 10 using System.ComponentModel; 11 using System.Configuration; 12 using System.DirectoryServices.Protocols; 13 using System.DirectoryServices; 14 using System.Net; 15 using System.Text; 16 using System.Threading; 17 using System.Collections; 18 using System.Security.Permissions; 19 20 namespace System.DirectoryServices.AccountManagement 21 { 22 internal struct ServerProperties 23 { 24 public string dnsHostName; 25 public DomainControllerMode OsVersion; 26 public ContextType contextType; 27 public string[] SupportCapabilities; 28 public int portSSL; 29 public int portLDAP; 30 }; 31 32 internal enum DomainControllerMode 33 { 34 Win2k = 0, 35 Win2k3 = 2, 36 WinLH = 3 37 }; 38 39 static internal class CapabilityMap 40 { 41 public const string LDAP_CAP_ACTIVE_DIRECTORY_OID = "1.2.840.113556.1.4.800"; 42 public const string LDAP_CAP_ACTIVE_DIRECTORY_V51_OID = "1.2.840.113556.1.4.1670"; 43 public const string LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID = "1.2.840.113556.1.4.1791"; 44 public const string LDAP_CAP_ACTIVE_DIRECTORY_ADAM_OID = "1.2.840.113556.1.4.1851"; 45 public const string LDAP_CAP_ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID = "1.2.840.113556.1.4.1920"; 46 public const string LDAP_CAP_ACTIVE_DIRECTORY_V61_OID = "1.2.840.113556.1.4.1935"; 47 } 48 49 internal sealed class CredentialValidator 50 { 51 private enum AuthMethod 52 { 53 Simple = 1, 54 Negotiate = 2 55 } 56 57 private bool _fastConcurrentSupported = true; 58 59 private Hashtable _connCache = new Hashtable(4); 60 private LdapDirectoryIdentifier _directoryIdent; 61 private object _cacheLock = new object(); 62 63 private AuthMethod _lastBindMethod = AuthMethod.Simple; 64 private string _serverName; 65 private ContextType _contextType; 66 private ServerProperties _serverProperties; 67 68 private const ContextOptions defaultContextOptionsNegotiate = ContextOptions.Signing | ContextOptions.Sealing | ContextOptions.Negotiate; 69 private const ContextOptions defaultContextOptionsSimple = ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind; 70 CredentialValidator(ContextType contextType, string serverName, ServerProperties serverProperties)71 public CredentialValidator(ContextType contextType, string serverName, ServerProperties serverProperties) 72 { 73 _fastConcurrentSupported = !(serverProperties.OsVersion == DomainControllerMode.Win2k); 74 75 if (contextType == ContextType.Machine && serverName == null) 76 { 77 _serverName = Environment.MachineName; 78 } 79 else 80 { 81 _serverName = serverName; 82 } 83 84 _contextType = contextType; 85 _serverProperties = serverProperties; 86 } 87 BindSam(string target, string userName, string password)88 private bool BindSam(string target, string userName, string password) 89 { 90 StringBuilder adsPath = new StringBuilder(); 91 adsPath.Append("WinNT://"); 92 adsPath.Append(_serverName); 93 adsPath.Append(",computer"); 94 Guid g = new Guid("fd8256d0-fd15-11ce-abc4-02608c9e7553"); // IID_IUnknown 95 object value = null; 96 // always attempt secure auth.. 97 int authenticationType = 1; 98 object unmanagedResult = null; 99 100 try 101 { 102 if (Thread.CurrentThread.GetApartmentState() == ApartmentState.Unknown) 103 Thread.CurrentThread.SetApartmentState(ApartmentState.MTA); 104 // We need the credentials to be in the form <machine>\\<user> 105 // if they just passed user then append the machine name here. 106 if (null != userName) 107 { 108 int index = userName.IndexOf("\\", StringComparison.Ordinal); 109 if (index == -1) 110 { 111 userName = _serverName + "\\" + userName; 112 } 113 } 114 115 int hr = UnsafeNativeMethods.ADsOpenObject(adsPath.ToString(), userName, password, (int)authenticationType, ref g, out value); 116 117 if (hr != 0) 118 { 119 if (hr == unchecked((int)(ExceptionHelper.ERROR_HRESULT_LOGON_FAILURE))) 120 { 121 // This is the invalid credetials case. We want to return false 122 // instead of throwing an exception 123 return false; 124 } 125 else 126 { 127 throw ExceptionHelper.GetExceptionFromErrorCode(hr); 128 } 129 } 130 131 unmanagedResult = ((UnsafeNativeMethods.IADs)value).Get("name"); 132 } 133 catch (System.Runtime.InteropServices.COMException e) 134 { 135 if (e.ErrorCode == unchecked((int)(ExceptionHelper.ERROR_HRESULT_LOGON_FAILURE))) 136 { 137 return false; 138 } 139 else 140 { 141 throw ExceptionHelper.GetExceptionFromCOMException(e); 142 } 143 } 144 finally 145 { 146 if (value != null) 147 System.Runtime.InteropServices.Marshal.ReleaseComObject(value); 148 } 149 150 return true; 151 } 152 BindLdap(NetworkCredential creds, ContextOptions contextOptions)153 private bool BindLdap(NetworkCredential creds, ContextOptions contextOptions) 154 { 155 LdapConnection current = null; 156 bool useSSL = (ContextOptions.SecureSocketLayer & contextOptions) > 0; 157 158 if (_contextType == ContextType.ApplicationDirectory) 159 { 160 _directoryIdent = new LdapDirectoryIdentifier(_serverProperties.dnsHostName, useSSL ? _serverProperties.portSSL : _serverProperties.portLDAP); 161 } 162 else 163 { 164 _directoryIdent = new LdapDirectoryIdentifier(_serverName, useSSL ? LdapConstants.LDAP_SSL_PORT : LdapConstants.LDAP_PORT); 165 } 166 167 bool attemptFastConcurrent = useSSL && _fastConcurrentSupported; 168 int index = Convert.ToInt32(attemptFastConcurrent) * 2 + Convert.ToInt32(useSSL); 169 170 if (!_connCache.Contains(index)) 171 { 172 lock (_cacheLock) 173 { 174 if (!_connCache.Contains(index)) 175 { 176 current = new LdapConnection(_directoryIdent); 177 // First attempt to turn on SSL 178 current.SessionOptions.SecureSocketLayer = useSSL; 179 180 if (attemptFastConcurrent) 181 { 182 try 183 { 184 current.SessionOptions.FastConcurrentBind(); 185 } 186 catch (PlatformNotSupportedException) 187 { 188 current.Dispose(); 189 current = null; 190 _fastConcurrentSupported = false; 191 index = Convert.ToInt32(useSSL); 192 current = new LdapConnection(_directoryIdent); 193 // We have fallen back to another connection so we need to set SSL again. 194 current.SessionOptions.SecureSocketLayer = useSSL; 195 } 196 } 197 198 _connCache.Add(index, current); 199 } 200 else 201 { 202 current = (LdapConnection)_connCache[index]; 203 } 204 } 205 } 206 else 207 { 208 current = (LdapConnection)_connCache[index]; 209 } 210 211 // If we are performing fastConcurrentBind there is no need to prevent multithreadaccess. FSB is thread safe and multi cred safe 212 // FSB also always has the same contextoptions so there is no need to lock the code that is modifying the current connection 213 if (attemptFastConcurrent && _fastConcurrentSupported) 214 { 215 lockedLdapBind(current, creds, contextOptions); 216 } 217 else 218 { 219 lock (_cacheLock) 220 { 221 lockedLdapBind(current, creds, contextOptions); 222 } 223 } 224 return true; 225 } 226 lockedLdapBind(LdapConnection current, NetworkCredential creds, ContextOptions contextOptions)227 private void lockedLdapBind(LdapConnection current, NetworkCredential creds, ContextOptions contextOptions) 228 { 229 current.AuthType = ((ContextOptions.SimpleBind & contextOptions) > 0 ? AuthType.Basic : AuthType.Negotiate); 230 current.SessionOptions.Signing = ((ContextOptions.Signing & contextOptions) > 0 ? true : false); 231 current.SessionOptions.Sealing = ((ContextOptions.Sealing & contextOptions) > 0 ? true : false); 232 if ((null == creds.UserName) && (null == creds.Password)) 233 { 234 current.Bind(); 235 } 236 else 237 { 238 current.Bind(creds); 239 } 240 } 241 Validate(string userName, string password)242 public bool Validate(string userName, string password) 243 { 244 NetworkCredential networkCredential = new NetworkCredential(userName, password); 245 246 // empty username and password on the local box 247 // causes authentication to succeed. If the username is empty we should just fail it 248 // here. 249 if (userName != null && userName.Length == 0) 250 return false; 251 252 if (_contextType == ContextType.Domain || _contextType == ContextType.ApplicationDirectory) 253 { 254 try 255 { 256 if (_lastBindMethod == AuthMethod.Simple && (_fastConcurrentSupported || _contextType == ContextType.ApplicationDirectory)) 257 { 258 try 259 { 260 BindLdap(networkCredential, defaultContextOptionsSimple); 261 _lastBindMethod = AuthMethod.Simple; 262 return true; 263 } 264 catch (LdapException) 265 { 266 // we don't return false here even if we failed with ERROR_LOGON_FAILURE. We must check Negotiate 267 // because there might be cases in which SSL fails and Negotiate succeeds 268 } 269 270 BindLdap(networkCredential, defaultContextOptionsNegotiate); 271 _lastBindMethod = AuthMethod.Negotiate; 272 return true; 273 } 274 else 275 { 276 try 277 { 278 BindLdap(networkCredential, defaultContextOptionsNegotiate); 279 _lastBindMethod = AuthMethod.Negotiate; 280 return true; 281 } 282 catch (LdapException) 283 { 284 // we don't return false here even if we failed with ERROR_LOGON_FAILURE. We must check SSL 285 // because there might be cases in which Negotiate fails and SSL succeeds 286 } 287 288 BindLdap(networkCredential, defaultContextOptionsSimple); 289 _lastBindMethod = AuthMethod.Simple; 290 return true; 291 } 292 } 293 catch (LdapException ldapex) 294 { 295 // If we got here it means that both SSL and Negotiate failed. Tough luck. 296 if (ldapex.ErrorCode == ExceptionHelper.ERROR_LOGON_FAILURE) 297 { 298 return false; 299 } 300 301 throw; 302 } 303 } 304 else 305 { 306 Debug.Assert(_contextType == ContextType.Machine); 307 return (BindSam(_serverName, userName, password)); 308 } 309 } 310 Validate(string userName, string password, ContextOptions connectionMethod)311 public bool Validate(string userName, string password, ContextOptions connectionMethod) 312 { 313 // empty username and password on the local box 314 // causes authentication to succeed. If the username is empty we should just fail it 315 // here. 316 if (userName != null && userName.Length == 0) 317 return false; 318 319 if (_contextType == ContextType.Domain || _contextType == ContextType.ApplicationDirectory) 320 { 321 try 322 { 323 NetworkCredential networkCredential = new NetworkCredential(userName, password); 324 BindLdap(networkCredential, connectionMethod); 325 return true; 326 } 327 catch (LdapException ldapex) 328 { 329 if (ldapex.ErrorCode == ExceptionHelper.ERROR_LOGON_FAILURE) 330 { 331 return false; 332 } 333 334 throw; 335 } 336 } 337 else 338 { 339 return (BindSam(_serverName, userName, password)); 340 } 341 } 342 } 343 // ******************************************** 344 public class PrincipalContext : IDisposable 345 { 346 // 347 // Public Constructors 348 // 349 PrincipalContext(ContextType contextType)350 public PrincipalContext(ContextType contextType) : 351 this(contextType, null, null, PrincipalContext.GetDefaultOptionForStore(contextType), null, null) 352 { } 353 PrincipalContext(ContextType contextType, string name)354 public PrincipalContext(ContextType contextType, string name) : 355 this(contextType, name, null, PrincipalContext.GetDefaultOptionForStore(contextType), null, null) 356 { } 357 PrincipalContext(ContextType contextType, string name, string container)358 public PrincipalContext(ContextType contextType, string name, string container) : 359 this(contextType, name, container, PrincipalContext.GetDefaultOptionForStore(contextType), null, null) 360 { } 361 PrincipalContext(ContextType contextType, string name, string container, ContextOptions options)362 public PrincipalContext(ContextType contextType, string name, string container, ContextOptions options) : 363 this(contextType, name, container, options, null, null) 364 { } 365 PrincipalContext(ContextType contextType, string name, string userName, string password)366 public PrincipalContext(ContextType contextType, string name, string userName, string password) : 367 this(contextType, name, null, PrincipalContext.GetDefaultOptionForStore(contextType), userName, password) 368 { } 369 PrincipalContext(ContextType contextType, string name, string container, string userName, string password)370 public PrincipalContext(ContextType contextType, string name, string container, string userName, string password) : 371 this(contextType, name, container, PrincipalContext.GetDefaultOptionForStore(contextType), userName, password) 372 { } 373 PrincipalContext( ContextType contextType, string name, string container, ContextOptions options, string userName, string password)374 public PrincipalContext( 375 ContextType contextType, string name, string container, ContextOptions options, string userName, string password) 376 { 377 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Entering ctor"); 378 379 if ((userName == null && password != null) || 380 (userName != null && password == null)) 381 throw new ArgumentException(SR.ContextBadUserPwdCombo); 382 383 if ((options & ~(ContextOptions.Signing | ContextOptions.Negotiate | ContextOptions.Sealing | ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind | ContextOptions.ServerBind)) != 0) 384 throw new InvalidEnumArgumentException("options", (int)options, typeof(ContextOptions)); 385 386 if (contextType == ContextType.Machine && ((options & ~ContextOptions.Negotiate) != 0)) 387 { 388 throw new ArgumentException(SR.InvalidContextOptionsForMachine); 389 } 390 391 if ((contextType == ContextType.Domain || contextType == ContextType.ApplicationDirectory) && 392 (((options & (ContextOptions.Negotiate | ContextOptions.SimpleBind)) == 0) || 393 (((options & (ContextOptions.Negotiate | ContextOptions.SimpleBind)) == ((ContextOptions.Negotiate | ContextOptions.SimpleBind)))))) 394 { 395 throw new ArgumentException(SR.InvalidContextOptionsForAD); 396 } 397 398 if ((contextType != ContextType.Machine) && 399 (contextType != ContextType.Domain) && 400 (contextType != ContextType.ApplicationDirectory) 401 #if TESTHOOK 402 && (contextType != ContextType.Test) 403 #endif 404 ) 405 { 406 throw new InvalidEnumArgumentException("contextType", (int)contextType, typeof(ContextType)); 407 } 408 409 if ((contextType == ContextType.Machine) && (container != null)) 410 throw new ArgumentException(SR.ContextNoContainerForMachineCtx); 411 412 if ((contextType == ContextType.ApplicationDirectory) && ((String.IsNullOrEmpty(container)) || (String.IsNullOrEmpty(name)))) 413 throw new ArgumentException(SR.ContextNoContainerForApplicationDirectoryCtx); 414 415 _contextType = contextType; 416 _name = name; 417 _container = container; 418 _options = options; 419 420 _username = userName; 421 _password = password; 422 423 DoServerVerifyAndPropRetrieval(); 424 425 _credValidate = new CredentialValidator(contextType, name, _serverProperties); 426 } 427 428 // 429 // Public Properties 430 // 431 432 public ContextType ContextType 433 { 434 get 435 { 436 CheckDisposed(); 437 438 return _contextType; 439 } 440 } 441 442 public string Name 443 { 444 get 445 { 446 CheckDisposed(); 447 448 return _name; 449 } 450 } 451 452 public string Container 453 { 454 get 455 { 456 CheckDisposed(); 457 458 return _container; 459 } 460 } 461 462 public string UserName 463 { 464 get 465 { 466 CheckDisposed(); 467 468 return _username; 469 } 470 } 471 472 public ContextOptions Options 473 { 474 get 475 { 476 CheckDisposed(); 477 478 return _options; 479 } 480 } 481 482 public string ConnectedServer 483 { 484 get 485 { 486 CheckDisposed(); 487 488 Initialize(); 489 490 // Unless we're not initialized, connectedServer should not be null 491 Debug.Assert(_connectedServer != null || _initialized == false); 492 493 // connectedServer should never be an empty string 494 Debug.Assert(_connectedServer == null || _connectedServer.Length != 0); 495 496 return _connectedServer; 497 } 498 } 499 500 /// <summary> 501 /// Validate the passed credentials against the directory supplied. 502 // This function will use the best determined method to do the evaluation 503 /// </summary> 504 ValidateCredentials(string userName, string password)505 public bool ValidateCredentials(string userName, string password) 506 { 507 CheckDisposed(); 508 509 if ((userName == null && password != null) || 510 (userName != null && password == null)) 511 throw new ArgumentException(SR.ContextBadUserPwdCombo); 512 513 #if TESTHOOK 514 if ( contextType == ContextType.Test ) 515 { 516 return true; 517 } 518 #endif 519 520 return (_credValidate.Validate(userName, password)); 521 } 522 523 /// <summary> 524 /// Validate the passed credentials against the directory supplied. 525 // The supplied options will determine the directory method for credential validation. 526 /// </summary> ValidateCredentials(string userName, string password, ContextOptions options)527 public bool ValidateCredentials(string userName, string password, ContextOptions options) 528 { 529 // Perform credential validation using fast concurrent bind... 530 CheckDisposed(); 531 532 if ((userName == null && password != null) || 533 (userName != null && password == null)) 534 throw new ArgumentException(SR.ContextBadUserPwdCombo); 535 536 if (options != ContextOptions.Negotiate && _contextType == ContextType.Machine) 537 throw new ArgumentException(SR.ContextOptionsNotValidForMachineStore); 538 539 #if TESTHOOK 540 if ( contextType == ContextType.Test ) 541 { 542 return true; 543 } 544 #endif 545 546 return (_credValidate.Validate(userName, password, options)); 547 } 548 549 // 550 // Private methods for initialization 551 // Initialize()552 private void Initialize() 553 { 554 if (!_initialized) 555 { 556 lock (_initializationLock) 557 { 558 if (_initialized) 559 return; 560 561 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Initializing Context"); 562 563 switch (_contextType) 564 { 565 case ContextType.Domain: 566 DoDomainInit(); 567 break; 568 569 case ContextType.Machine: 570 DoMachineInit(); 571 break; 572 573 case ContextType.ApplicationDirectory: 574 DoApplicationDirectoryInit(); 575 break; 576 #if TESTHOOK 577 case ContextType.Test: 578 // do nothing 579 break; 580 #endif 581 default: 582 // Internal error 583 Debug.Fail("PrincipalContext.Initialize: fell off end looking for " + _contextType.ToString()); 584 break; 585 } 586 587 _initialized = true; 588 } 589 } 590 } 591 DoApplicationDirectoryInit()592 private void DoApplicationDirectoryInit() 593 { 594 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Entering DoApplicationDirecotryInit"); 595 596 Debug.Assert(_contextType == ContextType.ApplicationDirectory); 597 598 if (_container == null) 599 { 600 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoApplicationDirecotryInit: using no-container path"); 601 DoLDAPDirectoryInitNoContainer(); 602 } 603 else 604 { 605 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoApplicationDirecotryInit: using container path"); 606 DoLDAPDirectoryInit(); 607 } 608 } 609 DoMachineInit()610 private void DoMachineInit() 611 { 612 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Entering DoMachineInit"); 613 614 Debug.Assert(_contextType == ContextType.Machine); 615 Debug.Assert(_container == null); 616 617 DirectoryEntry de = null; 618 619 try 620 { 621 string hostname = _name; 622 623 if (hostname == null) 624 hostname = Utils.GetComputerFlatName(); 625 626 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoMachineInit: hostname is " + hostname); 627 628 // use the options they specified 629 AuthenticationTypes authTypes = SDSUtils.MapOptionsToAuthTypes(_options); 630 631 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoMachineInit: authTypes is " + authTypes.ToString()); 632 633 de = new DirectoryEntry("WinNT://" + hostname + ",computer", _username, _password, authTypes); 634 635 // Force ADSI to connect so we detect if the server is down or if the servername is invalid 636 de.RefreshCache(); 637 638 StoreCtx storeCtx = CreateContextFromDirectoryEntry(de); 639 640 _queryCtx = storeCtx; 641 _userCtx = storeCtx; 642 _groupCtx = storeCtx; 643 _computerCtx = storeCtx; 644 645 _connectedServer = hostname; 646 de = null; 647 } 648 catch (Exception e) 649 { 650 GlobalDebug.WriteLineIf(GlobalDebug.Error, 651 "PrincipalContext", 652 "DoMachineInit: caught exception of type " 653 + e.GetType().ToString() + 654 " and message " + e.Message); 655 656 // Cleanup the DE on failure 657 if (de != null) 658 de.Dispose(); 659 660 throw; 661 } 662 } 663 DoDomainInit()664 private void DoDomainInit() 665 { 666 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Entering DoDomainInit"); 667 668 Debug.Assert(_contextType == ContextType.Domain); 669 670 if (_container == null) 671 { 672 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoDomainInit: using no-container path"); 673 DoLDAPDirectoryInitNoContainer(); 674 return; 675 } 676 else 677 { 678 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoDomainInit: using container path"); 679 DoLDAPDirectoryInit(); 680 return; 681 } 682 } 683 DoServerVerifyAndPropRetrieval()684 private void DoServerVerifyAndPropRetrieval() 685 { 686 _serverProperties = new ServerProperties(); 687 if (_contextType == ContextType.ApplicationDirectory || _contextType == ContextType.Domain) 688 { 689 ReadServerConfig(_name, ref _serverProperties); 690 691 if (_serverProperties.contextType != _contextType) 692 { 693 throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, SR.PassedContextTypeDoesNotMatchDetectedType, _serverProperties.contextType.ToString())); 694 } 695 } 696 } 697 DoLDAPDirectoryInit()698 private void DoLDAPDirectoryInit() 699 { 700 // use the servername if they gave us one, else let ADSI figure it out 701 string serverName = ""; 702 703 if (_name != null) 704 { 705 if (_contextType == ContextType.ApplicationDirectory) 706 { 707 serverName = _serverProperties.dnsHostName + ":" + 708 ((ContextOptions.SecureSocketLayer & _options) > 0 ? _serverProperties.portSSL : _serverProperties.portLDAP); 709 } 710 else 711 { 712 serverName = _name; 713 } 714 715 serverName += "/"; 716 } 717 718 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInit: serverName is " + serverName); 719 720 // use the options they specified 721 AuthenticationTypes authTypes = SDSUtils.MapOptionsToAuthTypes(_options); 722 723 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInit: authTypes is " + authTypes.ToString()); 724 725 DirectoryEntry de = new DirectoryEntry("LDAP://" + serverName + _container, _username, _password, authTypes); 726 727 try 728 { 729 // Set the password port to the ssl port read off of the rootDSE. Without this 730 // password change/set won't work when we connect without SSL and ADAM is running 731 // on non-standard port numbers. We have already verified directory connectivity at this point 732 // so this should always succeed. 733 if (_serverProperties.portSSL > 0) 734 { 735 de.Options.PasswordPort = _serverProperties.portSSL; 736 } 737 738 StoreCtx storeCtx = CreateContextFromDirectoryEntry(de); 739 740 _queryCtx = storeCtx; 741 _userCtx = storeCtx; 742 _groupCtx = storeCtx; 743 _computerCtx = storeCtx; 744 745 _connectedServer = ADUtils.GetServerName(de); 746 de = null; 747 } 748 catch (System.Runtime.InteropServices.COMException e) 749 { 750 throw ExceptionHelper.GetExceptionFromCOMException(e); 751 } 752 catch (Exception e) 753 { 754 GlobalDebug.WriteLineIf(GlobalDebug.Error, "PrincipalContext", 755 "DoLDAPDirectoryInit: caught exception of type " 756 + e.GetType().ToString() + 757 " and message " + e.Message); 758 759 throw; 760 } 761 finally 762 { 763 // Cleanup the DE on failure 764 if (de != null) 765 de.Dispose(); 766 } 767 } 768 DoLDAPDirectoryInitNoContainer()769 private void DoLDAPDirectoryInitNoContainer() 770 { 771 byte[] USERS_CONTAINER_GUID = new byte[] { 0xa9, 0xd1, 0xca, 0x15, 0x76, 0x88, 0x11, 0xd1, 0xad, 0xed, 0x00, 0xc0, 0x4f, 0xd8, 0xd5, 0xcd }; 772 byte[] COMPUTERS_CONTAINER_GUID = new byte[] { 0xaa, 0x31, 0x28, 0x25, 0x76, 0x88, 0x11, 0xd1, 0xad, 0xed, 0x00, 0xc0, 0x4f, 0xd8, 0xd5, 0xcd }; 773 774 // The StoreCtxs that will be used in the PrincipalContext, and their associated DirectoryEntry objects. 775 DirectoryEntry deUserGroupOrg = null; 776 DirectoryEntry deComputer = null; 777 DirectoryEntry deBase = null; 778 779 ADStoreCtx storeCtxUserGroupOrg = null; 780 ADStoreCtx storeCtxComputer = null; 781 ADStoreCtx storeCtxBase = null; 782 783 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Entering DoLDAPDirectoryInitNoContainer"); 784 785 // 786 // Build a DirectoryEntry that represents the root of the domain. 787 // 788 789 // Use the RootDSE to find the default naming context 790 DirectoryEntry deRootDse = null; 791 string adsPathBase; 792 793 // use the servername if they gave us one, else let ADSI figure it out 794 string serverName = ""; 795 if (_name != null) 796 { 797 serverName = _name + "/"; 798 } 799 800 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInitNoContainer: serverName is " + serverName); 801 802 // use the options they specified 803 AuthenticationTypes authTypes = SDSUtils.MapOptionsToAuthTypes(_options); 804 805 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInitNoContainer: authTypes is " + authTypes.ToString()); 806 807 try 808 { 809 deRootDse = new DirectoryEntry("LDAP://" + serverName + "rootDse", _username, _password, authTypes); 810 811 // This will also detect if the server is down or nonexistent 812 string domainNC = (string)deRootDse.Properties["defaultNamingContext"][0]; 813 adsPathBase = "LDAP://" + serverName + domainNC; 814 815 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInitNoContainer: domainNC is " + domainNC); 816 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "DoLDAPDirectoryInitNoContainer: adsPathBase is " + adsPathBase); 817 } 818 finally 819 { 820 // Don't allow the DE to leak 821 if (deRootDse != null) 822 deRootDse.Dispose(); 823 } 824 825 try 826 { 827 // Build a DE for the root of the domain using the retrieved naming context 828 deBase = new DirectoryEntry(adsPathBase, _username, _password, authTypes); 829 830 // Set the password port to the ssl port read off of the rootDSE. Without this 831 // password change/set won't work when we connect without SSL and ADAM is running 832 // on non-standard port numbers. We have already verified directory connectivity at this point 833 // so this should always succeed. 834 if (_serverProperties.portSSL > 0) 835 { 836 deBase.Options.PasswordPort = _serverProperties.portSSL; 837 } 838 839 // 840 // Use the wellKnownObjects attribute to determine the default location 841 // for users and computers. 842 // 843 string adsPathUserGroupOrg = null; 844 string adsPathComputer = null; 845 846 PropertyValueCollection wellKnownObjectValues = deBase.Properties["wellKnownObjects"]; 847 848 foreach (UnsafeNativeMethods.IADsDNWithBinary value in wellKnownObjectValues) 849 { 850 if (Utils.AreBytesEqual(USERS_CONTAINER_GUID, (byte[])value.BinaryValue)) 851 { 852 Debug.Assert(adsPathUserGroupOrg == null); 853 adsPathUserGroupOrg = "LDAP://" + serverName + value.DNString; 854 855 GlobalDebug.WriteLineIf( 856 GlobalDebug.Info, 857 "PrincipalContext", 858 "DoLDAPDirectoryInitNoContainer: found USER, adsPathUserGroupOrg is " + adsPathUserGroupOrg); 859 } 860 861 // Is it the computer container? 862 if (Utils.AreBytesEqual(COMPUTERS_CONTAINER_GUID, (byte[])value.BinaryValue)) 863 { 864 Debug.Assert(adsPathComputer == null); 865 adsPathComputer = "LDAP://" + serverName + value.DNString; 866 867 GlobalDebug.WriteLineIf( 868 GlobalDebug.Info, 869 "PrincipalContext", 870 "DoLDAPDirectoryInitNoContainer: found COMPUTER, adsPathComputer is " + adsPathComputer); 871 } 872 } 873 874 if ((adsPathUserGroupOrg == null) || (adsPathComputer == null)) 875 { 876 // Something's wrong with the domain, it's not exposing the proper 877 // well-known object fields. 878 throw new PrincipalOperationException(SR.ContextNoWellKnownObjects); 879 } 880 881 // 882 // Build DEs for the Users and Computers containers. 883 // The Users container will also be used as the default for Groups. 884 // The reason there are different contexts for groups, users and computers is so that 885 // when a principal is created it will go into the appropriate default container. This is so users don't 886 // be default create principals in the root of their directory. When a search happens the base context is used so that 887 // the whole directory will be covered. 888 // 889 deUserGroupOrg = new DirectoryEntry(adsPathUserGroupOrg, _username, _password, authTypes); 890 deComputer = new DirectoryEntry(adsPathComputer, _username, _password, authTypes); 891 892 StoreCtx userStore = CreateContextFromDirectoryEntry(deUserGroupOrg); 893 894 _userCtx = userStore; 895 _groupCtx = userStore; 896 deUserGroupOrg = null; // since we handed off ownership to the StoreCtx 897 898 _computerCtx = CreateContextFromDirectoryEntry(deComputer); 899 900 deComputer = null; 901 902 _queryCtx = CreateContextFromDirectoryEntry(deBase); 903 904 _connectedServer = ADUtils.GetServerName(deBase); 905 906 deBase = null; 907 } 908 catch (Exception e) 909 { 910 GlobalDebug.WriteLineIf(GlobalDebug.Error, 911 "PrincipalContext", 912 "DoLDAPDirectoryInitNoContainer: caught exception of type " 913 + e.GetType().ToString() + 914 " and message " + e.Message); 915 916 // Cleanup on failure. Once a DE has been successfully handed off to a ADStoreCtx, 917 // that ADStoreCtx will handle Dispose()'ing it 918 if (deUserGroupOrg != null) 919 deUserGroupOrg.Dispose(); 920 921 if (deComputer != null) 922 deComputer.Dispose(); 923 924 if (deBase != null) 925 deBase.Dispose(); 926 927 if (storeCtxUserGroupOrg != null) 928 storeCtxUserGroupOrg.Dispose(); 929 930 if (storeCtxComputer != null) 931 storeCtxComputer.Dispose(); 932 933 if (storeCtxBase != null) 934 storeCtxBase.Dispose(); 935 936 throw; 937 } 938 } 939 940 #if TESTHOOK 941 942 static public PrincipalContext Test 943 { 944 get 945 { 946 StoreCtx storeCtx = new TestStoreCtx(true); 947 PrincipalContext ctx = new PrincipalContext(ContextType.Test); 948 ctx.SetupContext(storeCtx); 949 ctx.initialized = true; 950 951 storeCtx.OwningContext = ctx; 952 return ctx; 953 } 954 } 955 956 static public PrincipalContext TestAltValidation 957 { 958 get 959 { 960 TestStoreCtx storeCtx = new TestStoreCtx(true); 961 storeCtx.SwitchValidationMode = true; 962 PrincipalContext ctx = new PrincipalContext(ContextType.Test); 963 ctx.SetupContext(storeCtx); 964 ctx.initialized = true; 965 966 storeCtx.OwningContext = ctx; 967 return ctx; 968 } 969 } 970 971 static public PrincipalContext TestNoTimeLimited 972 { 973 get 974 { 975 TestStoreCtx storeCtx = new TestStoreCtx(true); 976 storeCtx.SupportTimeLimited = false; 977 PrincipalContext ctx = new PrincipalContext(ContextType.Test); 978 ctx.SetupContext(storeCtx); 979 ctx.initialized = true; 980 981 storeCtx.OwningContext = ctx; 982 return ctx; 983 } 984 } 985 986 #endif // TESTHOOK 987 988 // 989 // Public Methods 990 // 991 Dispose()992 public void Dispose() 993 { 994 if (!_disposed) 995 { 996 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "Dispose: disposing"); 997 998 // Note that we may end up calling Dispose multiple times on the same 999 // StoreCtx (since, for example, it might be that userCtx == groupCtx). 1000 // This is okay, since StoreCtxs allow multiple Dispose() calls, and ignore 1001 // all but the first call. 1002 1003 if (_userCtx != null) 1004 _userCtx.Dispose(); 1005 1006 if (_groupCtx != null) 1007 _groupCtx.Dispose(); 1008 1009 if (_computerCtx != null) 1010 _computerCtx.Dispose(); 1011 1012 if (_queryCtx != null) 1013 _queryCtx.Dispose(); 1014 1015 _disposed = true; 1016 GC.SuppressFinalize(this); 1017 } 1018 } 1019 1020 // 1021 // Private Implementation 1022 // 1023 1024 // Are we initialized? 1025 private bool _initialized = false; 1026 private object _initializationLock = new object(); 1027 1028 // Have we been disposed? 1029 private bool _disposed = false; 1030 internal bool Disposed { get { return _disposed; } } 1031 1032 // Our constructor parameters 1033 1034 // encryption nor zeroing out the string when you're done with it. 1035 private string _username; 1036 private string _password; 1037 1038 // Cached connections to the server for fast credential validation 1039 private CredentialValidator _credValidate; 1040 private ServerProperties _serverProperties; 1041 1042 internal ServerProperties ServerInformation 1043 { 1044 get 1045 { 1046 return _serverProperties; 1047 } 1048 } 1049 1050 private string _name; 1051 private string _container; 1052 private ContextOptions _options; 1053 private ContextType _contextType; 1054 1055 // The server we're connected to 1056 private string _connectedServer = null; 1057 1058 // The reason there are different contexts for groups, users and computers is so that 1059 // when a principal is created it will go into the appropriate default container. This is so users don't 1060 // by default create principals in the root of their directory. When a search happens the base context is used so that 1061 // the whole directory will be covered. User and Computers default are the same ( USERS container ), Computers are 1062 // put under COMPUTERS container. If a container is specified then all the contexts will point to the same place. 1063 1064 // The StoreCtx to be used when inserting a new User/Computer/Group Principal into this 1065 // PrincipalContext. 1066 private StoreCtx _userCtx = null; 1067 private StoreCtx _computerCtx = null; 1068 private StoreCtx _groupCtx = null; 1069 1070 // The StoreCtx to be used when querying against this PrincipalContext for Principals 1071 private StoreCtx _queryCtx = null; 1072 1073 internal StoreCtx QueryCtx 1074 { 1075 get 1076 { 1077 Initialize(); 1078 return _queryCtx; 1079 } 1080 1081 set 1082 { 1083 _queryCtx = value; 1084 } 1085 } 1086 ReadServerConfig(string serverName, ref ServerProperties properties)1087 internal void ReadServerConfig(string serverName, ref ServerProperties properties) 1088 { 1089 string[] proplist = new string[] { "msDS-PortSSL", "msDS-PortLDAP", "domainControllerFunctionality", "dnsHostName", "supportedCapabilities" }; 1090 LdapConnection ldapConnection = null; 1091 1092 try 1093 { 1094 bool useSSL = (_options & ContextOptions.SecureSocketLayer) > 0; 1095 1096 if (useSSL && _contextType == ContextType.Domain) 1097 { 1098 LdapDirectoryIdentifier directoryid = new LdapDirectoryIdentifier(serverName, LdapConstants.LDAP_SSL_PORT); 1099 ldapConnection = new LdapConnection(directoryid); 1100 } 1101 else 1102 { 1103 ldapConnection = new LdapConnection(serverName); 1104 } 1105 1106 ldapConnection.AutoBind = false; 1107 // If SSL was enabled on the initial connection then turn it on for the search. 1108 // This is requried bc the appended port number will be SSL and we don't know what port LDAP is running on. 1109 ldapConnection.SessionOptions.SecureSocketLayer = useSSL; 1110 1111 string baseDN = null; // specify base as null for RootDSE search 1112 string ldapSearchFilter = "(objectClass=*)"; 1113 SearchResponse searchResponse = null; 1114 1115 SearchRequest searchRequest = new SearchRequest(baseDN, ldapSearchFilter, System.DirectoryServices.Protocols 1116 .SearchScope.Base, proplist); 1117 1118 try 1119 { 1120 searchResponse = (SearchResponse)ldapConnection.SendRequest(searchRequest); 1121 } 1122 catch (LdapException ex) 1123 { 1124 throw new PrincipalServerDownException(SR.ServerDown, ex); 1125 } 1126 1127 // Fill in the struct with the casted properties from the serach results. 1128 // there will always be only 1 item on the rootDSE so all entry indexes are 0 1129 properties.dnsHostName = (string)searchResponse.Entries[0].Attributes["dnsHostName"][0]; 1130 properties.SupportCapabilities = new string[searchResponse.Entries[0].Attributes["supportedCapabilities"].Count]; 1131 for (int i = 0; i < searchResponse.Entries[0].Attributes["supportedCapabilities"].Count; i++) 1132 { 1133 properties.SupportCapabilities[i] = (string)searchResponse.Entries[0].Attributes["supportedCapabilities"][i]; 1134 } 1135 1136 foreach (string capability in properties.SupportCapabilities) 1137 { 1138 if (CapabilityMap.LDAP_CAP_ACTIVE_DIRECTORY_ADAM_OID == capability) 1139 { 1140 properties.contextType = ContextType.ApplicationDirectory; 1141 } 1142 else if (CapabilityMap.LDAP_CAP_ACTIVE_DIRECTORY_OID == capability) 1143 { 1144 properties.contextType = ContextType.Domain; 1145 } 1146 } 1147 1148 // If we can't determine the OS vesion so we must fall back to lowest level of functionality 1149 if (searchResponse.Entries[0].Attributes.Contains("domainControllerFunctionality")) 1150 { 1151 properties.OsVersion = (DomainControllerMode)Convert.ToInt32(searchResponse.Entries[0].Attributes["domainControllerFunctionality"][0], CultureInfo.InvariantCulture); 1152 } 1153 else 1154 { 1155 properties.OsVersion = DomainControllerMode.Win2k; 1156 } 1157 1158 if (properties.contextType == ContextType.ApplicationDirectory) 1159 { 1160 if (searchResponse.Entries[0].Attributes.Contains("msDS-PortSSL")) 1161 { 1162 properties.portSSL = Convert.ToInt32(searchResponse.Entries[0].Attributes["msDS-PortSSL"][0]); 1163 } 1164 if (searchResponse.Entries[0].Attributes.Contains("msDS-PortLDAP")) 1165 { 1166 properties.portLDAP = Convert.ToInt32(searchResponse.Entries[0].Attributes["msDS-PortLDAP"][0]); 1167 } 1168 } 1169 1170 GlobalDebug.WriteLineIf(GlobalDebug.Info, "ReadServerConfig", "OsVersion : " + properties.OsVersion.ToString()); 1171 GlobalDebug.WriteLineIf(GlobalDebug.Info, "ReadServerConfig", "dnsHostName : " + properties.dnsHostName); 1172 GlobalDebug.WriteLineIf(GlobalDebug.Info, "ReadServerConfig", "contextType : " + properties.contextType.ToString()); 1173 GlobalDebug.WriteLineIf(GlobalDebug.Info, "ReadServerConfig", "portSSL : " + properties.portSSL.ToString(CultureInfo.InvariantCulture)); 1174 GlobalDebug.WriteLineIf(GlobalDebug.Info, "ReadServerConfig", "portLDAP :" + properties.portLDAP.ToString(CultureInfo.InvariantCulture)); 1175 } 1176 finally 1177 { 1178 if (ldapConnection != null) 1179 { 1180 ldapConnection.Dispose(); 1181 } 1182 } 1183 } 1184 CreateContextFromDirectoryEntry(DirectoryEntry entry)1185 private StoreCtx CreateContextFromDirectoryEntry(DirectoryEntry entry) 1186 { 1187 StoreCtx storeCtx; 1188 1189 Debug.Assert(entry != null); 1190 1191 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "CreateContextFromDirectoryEntry: path is " + entry.Path); 1192 1193 if (entry.Path.StartsWith("LDAP:", StringComparison.Ordinal)) 1194 { 1195 if (this.ContextType == ContextType.ApplicationDirectory) 1196 { 1197 storeCtx = new ADAMStoreCtx(entry, true, _username, _password, _name, _options); 1198 } 1199 else 1200 { 1201 storeCtx = new ADStoreCtx(entry, true, _username, _password, _options); 1202 } 1203 } 1204 else 1205 { 1206 Debug.Assert(entry.Path.StartsWith("WinNT:", StringComparison.Ordinal)); 1207 storeCtx = new SAMStoreCtx(entry, true, _username, _password, _options); 1208 } 1209 1210 storeCtx.OwningContext = this; 1211 return storeCtx; 1212 } 1213 1214 // Checks if we're already been disposed, and throws an appropriate 1215 // exception if so. CheckDisposed()1216 internal void CheckDisposed() 1217 { 1218 if (_disposed) 1219 { 1220 GlobalDebug.WriteLineIf(GlobalDebug.Warn, "PrincipalContext", "CheckDisposed: accessing disposed object"); 1221 1222 throw new ObjectDisposedException("PrincipalContext"); 1223 } 1224 } 1225 1226 // Match the default context options to the store type. GetDefaultOptionForStore(ContextType storeType)1227 private static ContextOptions GetDefaultOptionForStore(ContextType storeType) 1228 { 1229 if (storeType == ContextType.Machine) 1230 { 1231 return DefaultContextOptions.MachineDefaultContextOption; 1232 } 1233 else 1234 { 1235 return DefaultContextOptions.ADDefaultContextOption; 1236 } 1237 } 1238 1239 // Helper method: given a typeof(User/Computer/etc.), returns the userCtx/computerCtx/etc. ContextForType(Type t)1240 internal StoreCtx ContextForType(Type t) 1241 { 1242 GlobalDebug.WriteLineIf(GlobalDebug.Info, "PrincipalContext", "ContextForType: type is " + t.ToString()); 1243 1244 Initialize(); 1245 1246 if (t == typeof(System.DirectoryServices.AccountManagement.UserPrincipal) || t.IsSubclassOf(typeof(System.DirectoryServices.AccountManagement.UserPrincipal))) 1247 { 1248 return _userCtx; 1249 } 1250 else if (t == typeof(System.DirectoryServices.AccountManagement.ComputerPrincipal) || t.IsSubclassOf(typeof(System.DirectoryServices.AccountManagement.ComputerPrincipal))) 1251 { 1252 return _computerCtx; 1253 } 1254 else if (t == typeof(System.DirectoryServices.AccountManagement.AuthenticablePrincipal) || t.IsSubclassOf(typeof(System.DirectoryServices.AccountManagement.AuthenticablePrincipal))) 1255 { 1256 return _userCtx; 1257 } 1258 else 1259 { 1260 Debug.Assert(t == typeof(System.DirectoryServices.AccountManagement.GroupPrincipal) || t.IsSubclassOf(typeof(System.DirectoryServices.AccountManagement.GroupPrincipal))); 1261 return _groupCtx; 1262 } 1263 } 1264 } 1265 } 1266 1267