Active Directory Sync

    По долгу службы пришлось разбираться с Active Directory. Пришлось почитать, поэкспериментировать с классами, но всё в результате заработало превосходно.

    В первую очередь хотелось бы описать немного Directory Synchronization объект, который появился в .net framework 2.0. О нём, и о других преимуществах 2го framework вы сможете почитать на сайте microsoft (http://msdn.microsoft.com/en-us/magazine/cc188700.aspx ). Лично мне статья помогла разобраться, хотя я обилия информации в сети на предмет dyrSync нет.


    Для синхронизации я использую Web-Service, действия происходят следующим образом:

    1. Веб сервис получает LDAP путь к запросу.
      [WebMethod]
      public void Synchronize (string DomainPath, string Filter, string EntryPath)
      {

            DirectoryEntry de = new DirectoryEntry (DomainPath);
            using (SqlConnection conn = new SqlConnection(Globals.ConnectionString))
            {
              conn.Open();
              SqlCommand command = conn.CreateCommand();
              command.CommandText = «INSERT INTO SyncTable (Snapshot, OU) VALUES (@Snapshot, @OU, @ExchangeServer)»;
              command.Parameters.AddWithValue("@Snapshot", GetSyncData(de, filter));
              command.Parameters.AddWithValue(" @OU", EntryPath);

              command.ExecuteNonQuery();
              conn.Close();
            }

      }


    2. GetSyncData возвращает AD cookie:
        public byte[] DirectorySync (DirectoryEntry DomainDE, string Filter)
        {
          ADInit.Init();
          try
          {
            using (DirectorySearcher srch = new DirectorySearcher(DomainDE, Filter))
            {
              srch.SearchScope = SearchScope.Base;
              srch.DirectorySynchronization = new DirectorySynchronization();

              foreach (SearchResult se in srch.FindAll())
              {
              }

              MemoryStream ms = new MemoryStream();
              BinaryFormatter bFormat = new BinaryFormatter();
              bFormat.Serialize(ms, srch.DirectorySynchronization.GetDirectorySynchronizationCookie());
              ms.Close();

              return ms.GetBuffer();         
            }
          }
          catch (Exception Ex)
          {
            throw Ex;
          }
        }


    Приходится использовать Домен в качестве точки входа, потому как DirSynch работает с корневым элементом дерева. Фильтр может быть в виде: OU=myOU или что-либо в этом роде. В моём случае я достаточно просто обошел эту особенность: создал иерархическую структуру, из которой можно выбрать и LDAP путь до домена, и LDAP путь до данного объекта. Фильтр же – через имя объекта, который нужно синхронизировать.

    3. Обратная синхронизация + сверка
        [WebMethod]
        public bool GetRegionEntry (string Domain, string Filter, string EntryPath)
        {
          DirectoryEntry de = new DirectoryEntry (Domain);

          RegionInfo _regionInfo = regionEntry.GetEntry();
          using (SqlConnection conn = new SqlConnection(Globals.ConnectionString))
          {
            conn.Open();
            SqlCommand command = conn.CreateCommand();
            command.CommandText = «SELECT * FROM SyncTable WHERE OU= @OU»;
            command.Parameters.AddWithValue(" @OU", EntryPath);

            SqlDataReader reader = command.ExecuteReader();
            if (reader.Read())
            {
              return regionEntry.GetSyncDelta(de, Filter, (byte[])reader[«Snapshot»]);
            }
            else
            {
              throw new Exception (“Can’t read Sync Cookie from Database!”);

            }
            conn.Close();
          }
          return _regionInfo;
        }


    Здесь поступайте на своё усмотрение, либо получите bool (есть разница – нет разницы), либо получите дельту в полном объеме.
        ///public Dictionary<string, object> GetSyncDelta (DirectoryEntry DomainDE, string Filter, byte[] _cookie)

        public bool GetSyncDelta (DirectoryEntry DomainDE, string Filter, byte[] _cookie)
        {
          Dictionary<string, object> _delta = new Dictionary<string, object>();

          BinaryFormatter bf = new BinaryFormatter();
          byte[] cookie = (byte[]) bf.Deserialize(new MemoryStream(_cookie));

          DirectorySynchronization dirSync = new DirectorySynchronization(cookie);
          DirectorySearcher srch = new DirectorySearcher(DomainDE, Filter);
          srch.DirectorySynchronization = dirSync;

          foreach(SearchResult sr in srch.FindAll())
          {
            foreach (string attrName in sr.Properties.PropertyNames)
            {
              _delta.Add( attrName, sr.Properties[attrName]);
            }
            return true;
          }

          return false;
          //return _delta;
        }


    (Dictionary<string, object> не сериализируется стандартными методами в XML, попробуйте использовать свои массивы с keyvaluepair, либо перепишите сериализацию).

    4. Для того, чтобы сохранить всю историю изменений записи, создайте еще одну таблицу, добавьте к ней поле-идентификатор и поставьте триггер на INSERT / UPDATE:
    ALTER TRIGGER [SyncTableTrg]
      ON [dbo].[SyncTable]
      AFTER INSERT, UPDATE
    AS
    BEGIN
      INSERT INTO SyncTableLog (OU, Snapshot)
        SELECT OU, Snapshot FROM inserted
    END


    Таким образом получите неплохую историю о каждой из записей, которые синхронизируете. Хотя это – не единственный способ.

    спасибо за внимание

    з.ы. не судите строго. прежде не блоггил( first experience

    Комментарии 7

      +1
      Не совсем понял что есть
      foreach (SearchResult se in srch.FindAll())
      {
      }
      в GetSyncData может туплю после тяжкого дня, не мне непонятно.
      Как я вижу база у вас небольшая, триггерами еще пользуетесь :)
      Перепишите на хранимки лучше - так и логика и управляемость кода будут гораздо выше.
      PS а чем раскрашивали код? Если не секрет?
        0
        Объясню :)
        долго очень пытался понять, в чём проблема, пока не прочёл внимательно строку что result should be enumerated. вообще, можно просто сделать srch.FindAll(), а потом проверить count на 0. суть в том же. snippet старый) забыл переписать, если честно)))

        бд.. у нас в принципе не оч большая)) 25тыс пользователей в AD + порядка 1000 OU. а роли я не храню в БД.

        на предмет хранимок не совсем понял, что имеете ввиду) с удовольствием перепишу, если поясните..

        код раскрашивал по рекоммендациям храбра source code highliter, http://source.virtser.net/.
          0
          В смысле insert/update вынести на уровень БД.
          Если все правильно понял пользуетесь MS SQL Server? Я в нем не силен - я оракловод. Но примерно можно типа того сваять:

          create or replace procedure InsertInMeTable(mySnapshot number,
          myExchangeServer varchar2) is

          INSERT INTO SyncTable(Snapshot, OU) VALUES(mySnapshot,
          myExchangeServer);
          exception when others then raise_application_error(-20001,
          'Мы все умрем, Ошибка:' ||
          chr(10) || sqlerrm);
          end InsertInMeTable;

          Т.е. вынести логику работы с базой в базе и оставить, а из клиента вызывать только хранимую процедуру. Это вас может сильно спасти как в случае с измененим логики, так и в случае падения производительности вашего приложения.
          Да, то, что вы в триггере написали тоже стоит вынести в хранимые процедуры вставки/изменения/удаления. Так проще потом будет - триггеры вещь такая, может ОЧЕНЬ сильно повлиять на производительность вашей базы при интенсивном изменении данных в таблице. К тому же, не знаю как работает MS сервер, но после компиляции хранимых объектов при их вызове не тратиться время на синтаксический и семантический анализ запроса.
          А код вызова хранимой процедуры тогда будет либо
          command.CommandText = "begin InsertInMeTable; end;" либо меняйте тип command на StoredProcedure по моему. Вот как то так, извините за сумбурное объяснение - могз кипит. Просто хотел помочь :) Удачи!
            0
            айай :) неправильно сформулировал вопрос :(((
            дело вот в чём... я понимаю что такое ХП. просто в данном конкретном примере не прибегал к их использованию. в продуктиве у меня само собой в хард-коде нет запросов (уволили б))) вопрос именно в том, каким образом триггер из текущего его вида вынести в ХП..

            если я правильно понимаю, просто пишем ХП, в которой:
            insert into [log_tableName] (
            select * from [tableName] where =@
            )
            (это грубо говоря, само собой)...
              0
              Это просто - любые действия с БД вынести в ХП: функция вставки(возвращает ID), процедуры обновления и удаления.
              Т.е. для обновления/удаления просто дергаете процедуру UpdateInMyTable, а в ней идет update SyncTable..., а потом сразу insert into SyncTableLog ...
              таким образом вся логика в одном месте. Все просто, понятно и наглядно. В что куда триггер вставляет не всегда прозрачно и потенциально несет проблемы. Например у нас на триггерах работает только репликация с подчиненными узлами распределенной системы.
              Суть вы вроде бы поняли верно :) И еще мой вам совет - в лог таблицу пишите время и имя пользователя/машины. Обезопасите себя от гневных реплик типа "чо за херня???"
        0
        Спасибо за статью! Очень интересно!
          0
          Тоже в свое время пришлось работать с AD. Нагуглил неплохую книгу на эту тему:
          Addison.Wesley.The.dot.NET.Developers.Guide.to.Directory.Services.Programming.May.2006

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое