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.Net; 6 using System.Security.Principal; 7 using System.Diagnostics; 8 using System.Runtime.InteropServices; 9 using System.ComponentModel; 10 using System.Security.Permissions; 11 using System.IO; 12 13 namespace System.DirectoryServices.ActiveDirectory 14 { 15 public enum DirectoryContextType 16 { 17 Domain = 0, 18 Forest = 1, 19 DirectoryServer = 2, 20 ConfigurationSet = 3, 21 ApplicationPartition = 4 22 } 23 24 public class DirectoryContext 25 { 26 private string _name = null; 27 private DirectoryContextType _contextType; 28 private NetworkCredential _credential = null; 29 internal string serverName = null; 30 internal bool usernameIsNull = false; 31 internal bool passwordIsNull = false; 32 private bool _validated = false; 33 private bool _contextIsValid = false; 34 35 internal static LoadLibrarySafeHandle ADHandle; 36 internal static LoadLibrarySafeHandle ADAMHandle; 37 38 #region constructors 39 DirectoryContext()40 static DirectoryContext() 41 { 42 // load ntdsapi.dll for AD and ADAM 43 GetLibraryHandle(); 44 } 45 46 // Internal Constructors InitializeDirectoryContext(DirectoryContextType contextType, string name, string username, string password)47 internal void InitializeDirectoryContext(DirectoryContextType contextType, string name, string username, string password) 48 { 49 _name = name; 50 _contextType = contextType; 51 _credential = new NetworkCredential(username, password); 52 if (username == null) 53 { 54 usernameIsNull = true; 55 } 56 if (password == null) 57 { 58 passwordIsNull = true; 59 } 60 } 61 DirectoryContext(DirectoryContextType contextType, string name, DirectoryContext context)62 internal DirectoryContext(DirectoryContextType contextType, string name, DirectoryContext context) 63 { 64 _name = name; 65 _contextType = contextType; 66 67 if (context != null) 68 { 69 _credential = context.Credential; 70 this.usernameIsNull = context.usernameIsNull; 71 this.passwordIsNull = context.passwordIsNull; 72 } 73 else 74 { 75 _credential = new NetworkCredential(null, "", null); 76 this.usernameIsNull = true; 77 this.passwordIsNull = true; 78 } 79 } 80 DirectoryContext(DirectoryContext context)81 internal DirectoryContext(DirectoryContext context) 82 { 83 _name = context.Name; 84 _contextType = context.ContextType; 85 _credential = context.Credential; 86 this.usernameIsNull = context.usernameIsNull; 87 this.passwordIsNull = context.passwordIsNull; 88 if (context.ContextType != DirectoryContextType.ConfigurationSet) 89 { 90 // 91 // only for configurationset, we select a server, so we should not copy over that 92 // information, for all other types, this is either the same as name of the target or if the target is netbios name 93 // (for domain and forest) it could be the dns name. We should copy over this information. 94 // 95 this.serverName = context.serverName; 96 } 97 } 98 #endregion constructors 99 100 #region public constructors 101 DirectoryContext(DirectoryContextType contextType)102 public DirectoryContext(DirectoryContextType contextType) 103 { 104 // 105 // this constructor can only be called for DirectoryContextType.Forest or DirectoryContextType.Domain 106 // since all other types require the name to be specified 107 // 108 if (contextType != DirectoryContextType.Domain && contextType != DirectoryContextType.Forest) 109 { 110 throw new ArgumentException(SR.OnlyDomainOrForest, "contextType"); 111 } 112 113 InitializeDirectoryContext(contextType, null, null, null); 114 } 115 DirectoryContext(DirectoryContextType contextType, string name)116 public DirectoryContext(DirectoryContextType contextType, string name) 117 { 118 if (contextType < DirectoryContextType.Domain || contextType > DirectoryContextType.ApplicationPartition) 119 { 120 throw new InvalidEnumArgumentException("contextType", (int)contextType, typeof(DirectoryContextType)); 121 } 122 123 if (name == null) 124 { 125 throw new ArgumentNullException("name"); 126 } 127 128 if (name.Length == 0) 129 { 130 throw new ArgumentException(SR.EmptyStringParameter, "name"); 131 } 132 133 InitializeDirectoryContext(contextType, name, null, null); 134 } 135 DirectoryContext(DirectoryContextType contextType, string username, string password)136 public DirectoryContext(DirectoryContextType contextType, string username, string password) 137 { 138 // 139 // this constructor can only be called for DirectoryContextType.Forest or DirectoryContextType.Domain 140 // since all other types require the name to be specified 141 // 142 if (contextType != DirectoryContextType.Domain && contextType != DirectoryContextType.Forest) 143 { 144 throw new ArgumentException(SR.OnlyDomainOrForest, "contextType"); 145 } 146 147 InitializeDirectoryContext(contextType, null, username, password); 148 } 149 DirectoryContext(DirectoryContextType contextType, string name, string username, string password)150 public DirectoryContext(DirectoryContextType contextType, string name, string username, string password) 151 { 152 if (contextType < DirectoryContextType.Domain || contextType > DirectoryContextType.ApplicationPartition) 153 { 154 throw new InvalidEnumArgumentException("contextType", (int)contextType, typeof(DirectoryContextType)); 155 } 156 157 if (name == null) 158 { 159 throw new ArgumentNullException("name"); 160 } 161 162 if (name.Length == 0) 163 { 164 throw new ArgumentException(SR.EmptyStringParameter, "name"); 165 } 166 167 InitializeDirectoryContext(contextType, name, username, password); 168 } 169 170 #endregion public methods 171 172 #region public properties 173 174 public string Name => _name; 175 176 public string UserName => usernameIsNull ? null : _credential.UserName; 177 178 internal string Password 179 { 180 get => passwordIsNull ? null : _credential.Password; 181 } 182 183 public DirectoryContextType ContextType => _contextType; 184 185 internal NetworkCredential Credential => _credential; 186 187 #endregion public properties 188 189 #region private methods IsContextValid(DirectoryContext context, DirectoryContextType contextType)190 internal static bool IsContextValid(DirectoryContext context, DirectoryContextType contextType) 191 { 192 bool contextIsValid = false; 193 194 if ((contextType == DirectoryContextType.Domain) || ((contextType == DirectoryContextType.Forest) && (context.Name == null))) 195 { 196 string tmpTarget = context.Name; 197 198 if (tmpTarget == null) 199 { 200 // GetLoggedOnDomain returns the dns name of the logged on user's domain 201 context.serverName = GetLoggedOnDomain(); 202 contextIsValid = true; 203 } 204 else 205 { 206 // check for domain 207 int errorCode = 0; 208 DomainControllerInfo domainControllerInfo; 209 errorCode = Locator.DsGetDcNameWrapper(null, tmpTarget, null, (long)PrivateLocatorFlags.DirectoryServicesRequired, out domainControllerInfo); 210 211 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 212 { 213 // try with force rediscovery 214 215 errorCode = Locator.DsGetDcNameWrapper(null, tmpTarget, null, (long)PrivateLocatorFlags.DirectoryServicesRequired | (long)LocatorOptions.ForceRediscovery, out domainControllerInfo); 216 217 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 218 { 219 contextIsValid = false; 220 } 221 else if (errorCode != 0) 222 { 223 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 224 } 225 else 226 { 227 Debug.Assert(domainControllerInfo != null); 228 Debug.Assert(domainControllerInfo.DomainName != null); 229 context.serverName = domainControllerInfo.DomainName; 230 contextIsValid = true; 231 } 232 } 233 else if (errorCode == NativeMethods.ERROR_INVALID_DOMAIN_NAME_FORMAT) 234 { 235 // we can get this error if the target it server:port (not a valid domain) 236 contextIsValid = false; 237 } 238 else if (errorCode != 0) 239 { 240 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 241 } 242 else 243 { 244 Debug.Assert(domainControllerInfo != null); 245 Debug.Assert(domainControllerInfo.DomainName != null); 246 context.serverName = domainControllerInfo.DomainName; 247 contextIsValid = true; 248 } 249 } 250 } 251 else if (contextType == DirectoryContextType.Forest) 252 { 253 Debug.Assert(context.Name != null); 254 255 // check for forest 256 int errorCode = 0; 257 DomainControllerInfo domainControllerInfo; 258 errorCode = Locator.DsGetDcNameWrapper(null, context.Name, null, (long)(PrivateLocatorFlags.GCRequired | PrivateLocatorFlags.DirectoryServicesRequired), out domainControllerInfo); 259 260 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 261 { 262 // try with force rediscovery 263 264 errorCode = Locator.DsGetDcNameWrapper(null, context.Name, null, (long)((PrivateLocatorFlags.GCRequired | PrivateLocatorFlags.DirectoryServicesRequired)) | (long)LocatorOptions.ForceRediscovery, out domainControllerInfo); 265 266 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 267 { 268 contextIsValid = false; 269 } 270 else if (errorCode != 0) 271 { 272 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 273 } 274 else 275 { 276 Debug.Assert(domainControllerInfo != null); 277 Debug.Assert(domainControllerInfo.DnsForestName != null); 278 context.serverName = domainControllerInfo.DnsForestName; 279 contextIsValid = true; 280 } 281 } 282 else if (errorCode == NativeMethods.ERROR_INVALID_DOMAIN_NAME_FORMAT) 283 { 284 // we can get this error if the target it server:port (not a valid forest) 285 contextIsValid = false; 286 } 287 else if (errorCode != 0) 288 { 289 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 290 } 291 else 292 { 293 Debug.Assert(domainControllerInfo != null); 294 Debug.Assert(domainControllerInfo.DnsForestName != null); 295 context.serverName = domainControllerInfo.DnsForestName; 296 contextIsValid = true; 297 } 298 } 299 else if (contextType == DirectoryContextType.ApplicationPartition) 300 { 301 Debug.Assert(context.Name != null); 302 303 // check for application partition 304 int errorCode = 0; 305 DomainControllerInfo domainControllerInfo; 306 errorCode = Locator.DsGetDcNameWrapper(null, context.Name, null, (long)PrivateLocatorFlags.OnlyLDAPNeeded, out domainControllerInfo); 307 308 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 309 { 310 // try with force rediscovery 311 312 errorCode = Locator.DsGetDcNameWrapper(null, context.Name, null, (long)PrivateLocatorFlags.OnlyLDAPNeeded | (long)LocatorOptions.ForceRediscovery, out domainControllerInfo); 313 314 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 315 { 316 contextIsValid = false; 317 } 318 else if (errorCode != 0) 319 { 320 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 321 } 322 else 323 { 324 contextIsValid = true; 325 } 326 } 327 else if (errorCode == NativeMethods.ERROR_INVALID_DOMAIN_NAME_FORMAT) 328 { 329 // we can get this error if the target it server:port (not a valid application partition) 330 contextIsValid = false; 331 } 332 else if (errorCode != 0) 333 { 334 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 335 } 336 else 337 { 338 contextIsValid = true; 339 } 340 } 341 else if (contextType == DirectoryContextType.DirectoryServer) 342 { 343 // 344 // if the servername contains a port number, then remove that 345 // 346 string tempServerName = null; 347 string portNumber; 348 tempServerName = Utils.SplitServerNameAndPortNumber(context.Name, out portNumber); 349 350 // 351 // this will validate that the name specified in the context is truely the name of a machine (and not of a domain) 352 // 353 DirectoryEntry de = new DirectoryEntry("WinNT://" + tempServerName + ",computer", context.UserName, context.Password, Utils.DefaultAuthType); 354 try 355 { 356 de.Bind(true); 357 contextIsValid = true; 358 } 359 catch (COMException e) 360 { 361 if ((e.ErrorCode == unchecked((int)0x80070035)) || (e.ErrorCode == unchecked((int)0x80070033)) || (e.ErrorCode == unchecked((int)0x80005000))) 362 { 363 // if this returns bad network path 364 contextIsValid = false; 365 } 366 else 367 { 368 throw ExceptionHelper.GetExceptionFromCOMException(context, e); 369 } 370 } 371 finally 372 { 373 de.Dispose(); 374 } 375 } 376 else 377 { 378 // no special validation for ConfigurationSet 379 contextIsValid = true; 380 } 381 382 return contextIsValid; 383 } 384 isRootDomain()385 internal bool isRootDomain() 386 { 387 if (_contextType != DirectoryContextType.Forest) 388 return false; 389 390 if (!_validated) 391 { 392 _contextIsValid = IsContextValid(this, DirectoryContextType.Forest); 393 _validated = true; 394 } 395 return _contextIsValid; 396 } 397 isDomain()398 internal bool isDomain() 399 { 400 if (_contextType != DirectoryContextType.Domain) 401 return false; 402 403 if (!_validated) 404 { 405 _contextIsValid = IsContextValid(this, DirectoryContextType.Domain); 406 _validated = true; 407 } 408 return _contextIsValid; 409 } 410 isNdnc()411 internal bool isNdnc() 412 { 413 if (_contextType != DirectoryContextType.ApplicationPartition) 414 return false; 415 416 if (!_validated) 417 { 418 _contextIsValid = IsContextValid(this, DirectoryContextType.ApplicationPartition); 419 _validated = true; 420 } 421 return _contextIsValid; 422 } 423 isServer()424 internal bool isServer() 425 { 426 if (_contextType != DirectoryContextType.DirectoryServer) 427 return false; 428 429 if (!_validated) 430 { 431 _contextIsValid = IsContextValid(this, DirectoryContextType.DirectoryServer); 432 _validated = true; 433 } 434 return _contextIsValid; 435 } 436 isADAMConfigSet()437 internal bool isADAMConfigSet() 438 { 439 if (_contextType != DirectoryContextType.ConfigurationSet) 440 return false; 441 442 if (!_validated) 443 { 444 _contextIsValid = IsContextValid(this, DirectoryContextType.ConfigurationSet); 445 _validated = true; 446 } 447 return _contextIsValid; 448 } 449 450 // 451 // this method is called when the forest name is explicitly specified 452 // and we want to check if that matches the current logged on forest 453 // isCurrentForest()454 internal bool isCurrentForest() 455 { 456 bool result = false; 457 458 Debug.Assert(_name != null); 459 DomainControllerInfo domainControllerInfo = Locator.GetDomainControllerInfo(null, _name, null, (long)(PrivateLocatorFlags.DirectoryServicesRequired | PrivateLocatorFlags.ReturnDNSName)); 460 461 DomainControllerInfo currentDomainControllerInfo; 462 string loggedOnDomain = GetLoggedOnDomain(); 463 464 int errorCode = Locator.DsGetDcNameWrapper(null, loggedOnDomain, null, (long)(PrivateLocatorFlags.DirectoryServicesRequired | PrivateLocatorFlags.ReturnDNSName), out currentDomainControllerInfo); 465 466 if (errorCode == 0) 467 { 468 Debug.Assert(domainControllerInfo.DnsForestName != null); 469 Debug.Assert(currentDomainControllerInfo.DnsForestName != null); 470 471 result = (Utils.Compare(domainControllerInfo.DnsForestName, currentDomainControllerInfo.DnsForestName) == 0); 472 } 473 // 474 // if there is no forest associated with the logged on domain, then we return false 475 // 476 else if (errorCode != NativeMethods.ERROR_NO_SUCH_DOMAIN) 477 { 478 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 479 } 480 481 return result; 482 } 483 useServerBind()484 internal bool useServerBind() 485 { 486 return ((ContextType == DirectoryContextType.DirectoryServer) || (ContextType == DirectoryContextType.ConfigurationSet)); 487 } 488 GetServerName()489 internal string GetServerName() 490 { 491 if (serverName == null) 492 { 493 switch (_contextType) 494 { 495 case DirectoryContextType.ConfigurationSet: 496 { 497 AdamInstance adamInst = ConfigurationSet.FindAnyAdamInstance(this); 498 try 499 { 500 serverName = adamInst.Name; 501 } 502 finally 503 { 504 adamInst.Dispose(); 505 } 506 break; 507 } 508 case DirectoryContextType.Domain: 509 case DirectoryContextType.Forest: 510 { 511 // 512 // if the target is not specified OR 513 // if the forest name was explicitly specified and the forest is the same as the current forest 514 // we want to find a DC in the current domain 515 // 516 if ((_name == null) || ((_contextType == DirectoryContextType.Forest) && (isCurrentForest()))) 517 { 518 serverName = GetLoggedOnDomain(); 519 } 520 else 521 { 522 serverName = GetDnsDomainName(_name); 523 } 524 break; 525 } 526 case DirectoryContextType.ApplicationPartition: 527 { 528 // if this is an appNC the target should not be null 529 Debug.Assert(_name != null); 530 531 serverName = _name; 532 break; 533 } 534 case DirectoryContextType.DirectoryServer: 535 { 536 // this should not happen (We should have checks for this earlier itself) 537 Debug.Assert(_name != null); 538 serverName = _name; 539 break; 540 } 541 default: 542 { 543 Debug.Fail("DirectoryContext::GetServerName - Unknown contextType"); 544 break; 545 } 546 } 547 } 548 549 return serverName; 550 } 551 GetLoggedOnDomain()552 internal static string GetLoggedOnDomain() 553 { 554 string domainName = null; 555 556 NegotiateCallerNameRequest requestBuffer = new NegotiateCallerNameRequest(); 557 int requestBufferLength = (int)Marshal.SizeOf(requestBuffer); 558 559 IntPtr pResponseBuffer = IntPtr.Zero; 560 NegotiateCallerNameResponse responseBuffer = new NegotiateCallerNameResponse(); 561 int responseBufferLength; 562 int protocolStatus; 563 int result; 564 565 LsaLogonProcessSafeHandle lsaHandle; 566 567 // 568 // since we are using safe handles, we don't need to explicitly call NativeMethods.LsaDeregisterLogonProcess(lsaHandle) 569 // 570 result = NativeMethods.LsaConnectUntrusted(out lsaHandle); 571 572 if (result == 0) 573 { 574 // 575 // initialize the request buffer 576 // 577 requestBuffer.messageType = NativeMethods.NegGetCallerName; 578 579 result = NativeMethods.LsaCallAuthenticationPackage(lsaHandle, 0, requestBuffer, requestBufferLength, out pResponseBuffer, out responseBufferLength, out protocolStatus); 580 581 try 582 { 583 if (result == 0 && protocolStatus == 0) 584 { 585 Marshal.PtrToStructure(pResponseBuffer, responseBuffer); 586 587 // 588 // callerName is of the form domain\username 589 // 590 Debug.Assert((responseBuffer.callerName != null), "NativeMethods.LsaCallAuthenticationPackage returned null callerName."); 591 int index = responseBuffer.callerName.IndexOf('\\'); 592 Debug.Assert((index != -1), "NativeMethods.LsaCallAuthenticationPackage returned callerName not in domain\\username format."); 593 domainName = responseBuffer.callerName.Substring(0, index); 594 } 595 else 596 { 597 if (result == NativeMethods.STATUS_QUOTA_EXCEEDED) 598 { 599 throw new OutOfMemoryException(); 600 } 601 else if ((result == 0) && (UnsafeNativeMethods.LsaNtStatusToWinError(protocolStatus) == NativeMethods.ERROR_NO_SUCH_LOGON_SESSION)) 602 { 603 // If this is a directory user, extract domain info from username 604 if (!Utils.IsSamUser()) 605 { 606 WindowsIdentity identity = WindowsIdentity.GetCurrent(); 607 608 int index = identity.Name.IndexOf('\\'); 609 Debug.Assert(index != -1); 610 domainName = identity.Name.Substring(0, index); 611 } 612 } 613 else 614 { 615 throw ExceptionHelper.GetExceptionFromErrorCode(UnsafeNativeMethods.LsaNtStatusToWinError((result != 0) ? result : protocolStatus)); 616 } 617 } 618 } 619 finally 620 { 621 if (pResponseBuffer != IntPtr.Zero) 622 { 623 NativeMethods.LsaFreeReturnBuffer(pResponseBuffer); 624 } 625 } 626 } 627 else if (result == NativeMethods.STATUS_QUOTA_EXCEEDED) 628 { 629 throw new OutOfMemoryException(); 630 } 631 else 632 { 633 throw ExceptionHelper.GetExceptionFromErrorCode(UnsafeNativeMethods.LsaNtStatusToWinError(result)); 634 } 635 636 // If we're running as a local user (i.e. NT AUTHORITY\LOCAL SYSTEM, IIS APPPOOL\APPPoolIdentity, etc.), 637 // domainName will be null and we fall back to the machine's domain 638 domainName = GetDnsDomainName(domainName); 639 640 if (domainName == null) 641 { 642 // 643 // we should never get to this point here since we should have already verified that the context is valid 644 // by the time we get to this point 645 // 646 throw new ActiveDirectoryOperationException(SR.ContextNotAssociatedWithDomain); 647 } 648 649 return domainName; 650 } 651 GetDnsDomainName(string domainName)652 internal static string GetDnsDomainName(string domainName) 653 { 654 DomainControllerInfo domainControllerInfo; 655 int errorCode = 0; 656 657 // 658 // Locator.DsGetDcNameWrapper internally passes the ReturnDNSName flag when calling DsGetDcName 659 // 660 errorCode = Locator.DsGetDcNameWrapper(null, domainName, null, (long)PrivateLocatorFlags.DirectoryServicesRequired, out domainControllerInfo); 661 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 662 { 663 // try again with force rediscovery 664 errorCode = Locator.DsGetDcNameWrapper(null, domainName, null, (long)((long)PrivateLocatorFlags.DirectoryServicesRequired | (long)LocatorOptions.ForceRediscovery), out domainControllerInfo); 665 if (errorCode == NativeMethods.ERROR_NO_SUCH_DOMAIN) 666 { 667 return null; 668 } 669 else if (errorCode != 0) 670 { 671 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 672 } 673 } 674 else if (errorCode != 0) 675 { 676 throw ExceptionHelper.GetExceptionFromErrorCode(errorCode); 677 } 678 679 Debug.Assert(domainControllerInfo != null); 680 Debug.Assert(domainControllerInfo.DomainName != null); 681 682 return domainControllerInfo.DomainName; 683 } 684 GetLibraryHandle()685 private static void GetLibraryHandle() 686 { 687 // first get AD handle 688 string systemPath = Environment.SystemDirectory; 689 IntPtr tempHandle = UnsafeNativeMethods.LoadLibrary(systemPath + "\\ntdsapi.dll"); 690 if (tempHandle == (IntPtr)0) 691 { 692 throw ExceptionHelper.GetExceptionFromErrorCode(Marshal.GetLastWin32Error()); 693 } 694 else 695 { 696 ADHandle = new LoadLibrarySafeHandle(tempHandle); 697 } 698 699 // not get the ADAM handle 700 // got to the windows\adam directory 701 DirectoryInfo windowsDirectory = Directory.GetParent(systemPath); 702 tempHandle = UnsafeNativeMethods.LoadLibrary(windowsDirectory.FullName + "\\ADAM\\ntdsapi.dll"); 703 if (tempHandle == (IntPtr)0) 704 { 705 ADAMHandle = ADHandle; 706 } 707 else 708 { 709 ADAMHandle = new LoadLibrarySafeHandle(tempHandle); 710 } 711 } 712 713 #endregion private methods 714 } 715 } 716