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