Ostatnio bawię się odpytywaniem Active Directory o użytkowników, grupy i jednostki organizacyjne i natrafiłem na kilka dość ciekawych przypadków kiedy to znaki specjalne nie są zamieniane, albo zamienione nie działają poprawnie :) co jest trochę… dziwne ale tak bywa :)

Ogólny problem ze znakami specjalnymi w AD jest taki, iż w niektórych przypadkach można zastosować backslash w celu określenia, że kolejny znak po nim ma być traktowany tak jak został zapisany a nie jako znak specjalny, a w niektórych nie. Czyli na przykład nazwa grupy USERS/POWER USERS powinna zostać zamieniona na USERS/POWER USERS ale w zależności od kontekstu znak / musi zostać zamieniony na inny.

  1. Mianowicie, kiedy my odpytujemy się AD o dany obiekt zawierający znak specjalny to my musimy zadbać o konwersję znaków na poprawne.
  2. Kiedy pobieramy wynik zapytania w postaci ścieżki (SearchResult.Path), .NET Framework zadba o konwersję znaków specjalnych na poprawne escape characters oraz dodatkowo doda nam protokół LDAP:// do początku Distinguished Name od obiektu, którego znaleźliśmy.
  3. Kiedy pobieramy własność Distinguished Name obiektu z DirecotryEntry to dostaniemy ciąg znaków bez zamienionych znaków specjalnych oraz bez protokołu LDAP://.

Różnica pomiędzy 2 i 3 jest taka, że SearchResult.Path zwraca własność ADsPath zaś Distinguished Name jest trochę inną wartością, jednakże za pomocą obydwóch wartości możemy obiekt DirectoryEntry, który zwróci nam odpowiedni obiekt z AD.

Żeby było zabawniej kiedy wyszukujemy musimy wykonać inną podmianę niż w przypadku 2 i 3 :)

Dla elementów, które wyszukujemy musimy podmienić wszystkie zwykłe nawiasy (), Slash, gwiazdkę, backslash, i tak zwany character return/NULL (), zaś w Distinguished Name musi zapewnić by przecinki były dobrze odbierane jak i slash – znaków jest więcej a można do nich zaliczyć <>, „”, przecinek, +, =, ; i spację.

Sprawę pierwszą z wyszukiwaniem, możemy rozwiązać za pomocą Extension Method:

/// <summary>
/// Replaces special characters in search string.
/// </summary>
/// <param name="source">The search string.</param>
/// <param name="escapeWildcards">if set to <c>true</c> escape wildcards.</param>
/// <returns>String with proper characters.</returns>
/// <remarks>
/// All characters are replaced, please see the full list:
/// http://msdn.microsoft.com/en-us/library/aa746475%28VS.85%29.aspx
/// </remarks>
public static string ReplaceSpecialCharsInSearch(this string source, bool escapeWildcards)
{
    source = source.Replace("\", "\5c").Replace("/", "\2f");;
    source = source.Replace("(", "\28").Replace(")", "\29");
    source = source.Replace("", "\00");
 
    if(escapeWildcards)
        source = source.Replace("*", "\2a");
 
    return source;
}

Z drugą jest już gorzej ale i lepiej :) Mimo pełnej listy zakazanych znaków, tylko jeden jest domyślnie nie konwertowany, slash, więc kolejna Extension Method wygląda następująco:

/// <summary>
/// Replaces special chars in distinguished name.
/// </summary>
/// <param name="source">The distinguished name.</param>
/// <returns>String with proper characters.</returns>
/// <remarks>
/// Only / is replaced as others should be done by .NET Framework:
/// http://msdn.microsoft.com/en-us/library/aa366101%28VS.85%29.aspx
/// http://msdn.microsoft.com/en-us/library/aa772316%28VS.85%29.aspx
/// http://msdn.microsoft.com/en-us/library/aa746384%28VS.85%29.aspx
/// </remarks>
public static string ReplaceSpecialCharsInDistinguishedName(this string source)
{
    string formatString;
 
    if(source.Contains(@"/"))
    {
        return source;
    }
 
    int indexOf;
 
    if((indexOf = source.IndexOf("://")) > -1)
    {
        var protocol = source.Substring(0, indexOf + 3);
        source = source.Remove(0, protocol.Length).Replace("/", @"/");
        formatString = string.Format("{0}{1}", protocol, source);
    }
    else
    {
        source = source.Replace("/", @"/");
        formatString = "{0}";
    }
 
    return string.Format(formatString, source);
}

Używam wykrycia protokołu ze względu na to by już nie myśleć czy aby na pewno ktoś nie połączył już distinguished name z protokołem, dodatkowo, nie koniecznie musi być LDAP, może być GC (Global Catalog).

Metody te zaoszczędziły mi trochę czasu i okazują się bardzo przydatne, wyszukiwanie podmieniam podczas deklaracji filtru w DirectorySearcher, na przykład:

using(DirectorySearcher deSearch = new DirectorySearcher())
{
    deSearch.SearchRoot = AdUtils.GetDirectoryObject();
    deSearch.Filter = string.Format("(&(objectClass=user)(objectCategory=person)(sAMAccountName={0}))", userName.ReplaceSpecialCharsInSearch());
    deSearch.SearchScope = SearchScope.Subtree;
    SearchResult results = deSearch.FindOne();
 
    if (results == null)
    {
        return null;
    }
 
    return AdUtils.GetDirectoryObject(results.Path);
}

Zaś metodę rozszerzającą string dla distinguished name, używam podczas tworzenia DirectoryEntry:

/// <summary>
/// Gets the <see cref="DirectoryEntry"/> object.
/// </summary>
/// <param name="domainReference">The domain reference (AD Path).</param>
/// <param name="userName">Name of the user.</param>
/// <param name="password">The password.</param>
/// <returns>
/// New <see cref="DirectoryEntry"/> object.
/// </returns>
internal static DirectoryEntry GetDirectoryObject(string domainReference, string userName, string password)
{
    if(string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
        return new DirectoryEntry(domainReference.ReplaceSpecialCharsInDistinguishedName());
    else
        return new DirectoryEntry(domainReference.ReplaceSpecialCharsInDistinguishedName(), userName, password, AuthenticationTypes.Secure);
}

Tutaj warto odwołać się jeszcze do przykładu z wyszukiwaniem. Mianowicie, większość obiektów, z których korzystamy podczas pracy z AD implementuje IDisposable poprzez dziedziczenie z klasykomponentu, czyli zarówno obiekty DirectorySearcher jak i DirectoryEntry, powinny zostać zniszczone pod koniec pracy z nimi. Normalnie można tego dokonać wywołując metodę Close na DirectoryEntry oraz Dispose na DirectorySearcher. To powoduje kolejną rzecz, o której musimy pamiętać, dlatego też warto stworzyć sobie extension method do DirectorySearcher, które podczas wywołania metody FindAll zamiast zwracać kolekcję SearchResultCollection, która powinna zostać zniszczona po skończeniu jej użytkowania zwraca IEnumerable<SearchResult>.

/// <summary>
/// Executing a safe find all method, which helps with disposable collection.
/// </summary>
/// <param name="searcher">The searcher.</param>
/// <returns>
/// List of search results.
/// </returns>
internal static IEnumerable<SearchResult> DisposeSafeFindAll(this DirectorySearcher searcher)
{
    using(SearchResultCollection results = searcher.FindAll())
    {
        foreach(SearchResult result in results)
        {
            yield return result;
        }
    }
}

To chyba na tyle z takich ciekawostek i pomocnych funkcji/metod przy zabawie z AD. Mimo wszystkich postów na temat LDAP, które wychwalają protokół jak nie wiem co, wciąż jednak możemy natrafić na problemy w trakcie zabawy. Pytanie teraz tylko pozostaje, czy to jest poprawne działanie – to znaczy czy jest to działanie zamierzone? – czy jest to po prostu Bug, który istnieje i nie został do tej pory naprawiony.

Znacie jeszcze jakieś podobne problemy z odpytywaniem AD? Zachęcam do komentowania, jeżeli o czymś zapomniałem to napiszcie, postaram się poprawić/dodać przykłady.