using System; using System.Collections; using System.Collections.Specialized; using System.Diagnostics; using System.DirectoryServices; using System.Text; namespace Org.Khakipants.Identity { /// /// Summary description for LdapQueries. /// internal class LdapQueries { #region Ldap Queries private static string MEMBEROF = "memberof"; // this is not as efficient as it could be // because of poor connection management /// The path to the ldap server (i.e. "GC://" + server + ":3269"). internal static ArrayList GetGroups(string path, string userName){ Debug.WriteLine("Getting groups."); StringCollection allParentGroupDns = new StringCollection(); // key and value = a groupDn ///////////////////// // get all groups the user is an immediate memberof ///////////////////// ArrayList desiredProperties = new ArrayList(); desiredProperties.Add(MEMBEROF); SortedList properties = GetProperties(path, userName, desiredProperties); ArrayList immediateGroupDnList = (ArrayList) properties[MEMBEROF]; ///////////////////// // find all of the parents/ancestors of the immediateGroupDnList ///////////////////// DirectoryEntry anonymousEntry = new DirectoryEntry( path , ANONYMOUS_USERNAME , ANONYMOUS_PASSWORD); try { return GetAllParentGroups(anonymousEntry, immediateGroupDnList); } finally { anonymousEntry.Close(); } } private static string GROUPS_SEARCH_FILTER = "(|(objectCategory=CN=Group,CN=Schema,CN=Configuration,DC=ad,DC=khakipants,DC=org)(objectCategory=CN=Organizational-Unit,CN=Schema,CN=Configuration,DC=ad,DC=khakipants,DC=org))"; // "(|(objectClass=group)(objectClass=organizationalUnit))"; private static string BuildSearchFilter(ICollection childGroupDns ) { StringBuilder sb = new StringBuilder(); // improve performance slightly by only looking at groups sb.Append("(&"+ GROUPS_SEARCH_FILTER + "(|"); if( 0 < childGroupDns.Count ) { foreach( string s in childGroupDns ) { sb.Append(String.Format("(distinguishedName={0})", s)); } } else { // put a filter that will always be false... this allows me to // do some performance testing for negative results. sb.Append("(distinguishedName=SomethingThatWillNeverExist)"); } sb.Append("))"); return sb.ToString(); } private static ArrayList GetAllParentGroups(DirectoryEntry entry, ArrayList groupDns) { ArrayList needsToBeExpanded = ExtractUnique(groupDns); // need to expand all children Hashtable hasBeenSeen = new Hashtable(); foreach( string s in needsToBeExpanded) { hasBeenSeen.Add(s,s); } ArrayList desiredPropertiesList = new ArrayList(); desiredPropertiesList.Add(MEMBEROF); string [] desiredProperties = (string []) desiredPropertiesList.ToArray(typeof(string)); while( 0 < needsToBeExpanded.Count ) { #if DEBUG DateTime StartTime = DateTime.Now; #endif string SearchFilter = BuildSearchFilter(needsToBeExpanded); Debug.WriteLine(SearchFilter); // we don't need to expand these anymore needsToBeExpanded.Clear(); DirectorySearcher searcher = new DirectorySearcher( entry, SearchFilter, desiredProperties, SearchScope.Subtree); using( searcher ) { SearchResultCollection results = searcher.FindAll(); using(results) { foreach( SearchResult result in results ) { ResultPropertyValueCollection memberOfList = result.Properties[MEMBEROF]; if( memberOfList != null ) { foreach( string parentGroupDn in memberOfList ) { if( ! hasBeenSeen.Contains(parentGroupDn) ) { hasBeenSeen.Add(parentGroupDn, parentGroupDn); needsToBeExpanded.Add(parentGroupDn); } else { Debug.WriteLine(String.Format("already seen : {0}", parentGroupDn)); } } } } } } #if DEBUG Debug.WriteLine("Elapsed time = " + (DateTime.Now - StartTime).ToString()); #endif } return new ArrayList(hasBeenSeen.Keys); } internal static bool IsValidCredential(string path, string userName, string password) { try { string userDn = GetDistinguishedName(path, userName); DirectoryEntry ssoEntry = new DirectoryEntry( path, ExtractDomain(userDn) + "\\" + userName , password); using(ssoEntry) { Trace.WriteLine(String.Format("Checking password for {0}.", userName)); try{ // force an authentication object obj = ssoEntry.NativeObject; } catch (Exception ex) { Debug.WriteLine(ex); return false; } } } catch (InvalidUsernameException iue) { object o = iue; // to squash compiler warnings return false; } catch (Exception ex) { throw new Exception("Error authenticating user.", ex); } return true; } internal static string GetDistinguishedName(string path, string userName ) { Trace.WriteLine(String.Format("Getting distinquished name for {0}.", userName)); ArrayList desiredProperties = new ArrayList(); desiredProperties.Add("distinguishedname"); SortedList Properties = GetProperties(path, userName, desiredProperties); object DNArray = Properties["distinguishedname"]; if( null != DNArray) { return (string) ((ArrayList) DNArray )[0]; } else { throw new Exception("Could not load distinguishedname property."); } } // an example CN=Jacobs\, Nathan B.,CN=Users,DC=khakipants,DC=org internal static string ExtractDomain(string DN) { string [] strings = DN.Split(','); ArrayList DClist = new ArrayList(); foreach( string s in strings) { if( s.StartsWith("DC=") ) { DClist.Add(s.Substring(3)); } } StringBuilder sb = new StringBuilder(); foreach( string s in DClist) { sb.Append(s); sb.Append("."); // the trailing '.' is ok because of DNS standard } sb.Length = sb.Length - 1; return sb.ToString(); } /// If userName is not found in the given path. internal static ArrayList GetAvailableProperties(string path, string userName ) { Trace.WriteLine("Getting all available properties."); ArrayList properties = new ArrayList(); // connect anonymously to determine proper distinguishedname DirectoryEntry anonymousEntry = new DirectoryEntry( path , ANONYMOUS_USERNAME , ANONYMOUS_PASSWORD); using (anonymousEntry) { Debug.WriteLine(String.Format("Getting available properties for {0}.", userName)); // Bind to the native AdsObject to force authentication. Object obj = anonymousEntry.NativeObject; DirectorySearcher anonymousSearch = new DirectorySearcher(anonymousEntry); using(anonymousSearch) { anonymousSearch.ReferralChasing = ReferralChasingOption.All; anonymousSearch.Filter = "(&(objectClass=user)(objectCategory=person)(SamAccountName=" + userName + "))"; anonymousSearch.PropertiesToLoad.Add("*"); SearchResult result = anonymousSearch.FindOne(); if(null == result) { throw new InvalidUsernameException(userName, path); } foreach (string name in result.Properties.PropertyNames) { properties.Add(name); } } } return properties; } internal static SortedList GetProperties(string path, string userName, ArrayList desiredProperties ) { Trace.WriteLine("Getting properties."); desiredProperties = ExtractUnique(desiredProperties); SortedList properties = new SortedList(); // connect anonymously to determine proper distinguishedname DirectoryEntry anonymousEntry = new DirectoryEntry( path , ANONYMOUS_USERNAME , ANONYMOUS_PASSWORD); using(anonymousEntry) { Trace.WriteLine(String.Format("Getting properties for {0}.", userName)); // Bind to the native AdsObject to force authentication. Object obj = anonymousEntry.NativeObject; DirectorySearcher anonymousSearch = new DirectorySearcher(anonymousEntry); using(anonymousSearch) { anonymousSearch.ReferralChasing = ReferralChasingOption.All; anonymousSearch.Filter = "(&(objectClass=user)(objectCategory=person)(SamAccountName=" + userName + "))"; foreach ( string property in desiredProperties ) { anonymousSearch.PropertiesToLoad.Add(property); } SearchResult result = anonymousSearch.FindOne(); if(null == result) { throw new InvalidUsernameException(userName, path); } // note: using desiredProperties and not PropertyNames because of // spurious addition of adspath to result set. foreach (string name in desiredProperties ) { // result.Properties.PropertyNames) { if( null != result.Properties[name] ) { properties.Add(name, new ArrayList(result.Properties[name])); } else { properties.Add(name, new ArrayList()); } } } } return properties; } #endregion #region Helper Functions private static ArrayList ExtractUnique(ArrayList list) { Hashtable table = new Hashtable(); foreach( object o in list ) { if( ! table.Contains( o ) ) { table[o] = o; } } return new ArrayList(table.Keys); } #endregion private LdapQueries() { } } }