Автоматизация миграции базы данных DocsVision

    Преамбула



    Казалось бы — если система закрытая, то должны быть удобные инструменты? Ну, или хотя бы API для возможности написания этих удобных инструментов самостоятельно.

    К сожалению, обычно все плохо: инструменты есть, но настолько неудобные, что от их наличия — никакого счастья. Приходится выкручиваться.

    Итак, дано — система DocsVision (далее DV) версии 4.5 SR1. И, стоит задача переместить базу с одного сервера на другой (скажем, клиенты купили новый). Проблема, которая при этом возникает — ровно одна.

    Права на объекты для локальных учетных записей при переносе базы на новое место превратятся в тыкву. А так как стандартные группы DV являются именно локальными — то проблем не избежать.

    Кто заинтересован — прошу пожаловать под кат.



    Стандартные инструменты



    Для начала рассмотрим тот инструмент, что нам предлагает для этих целей компания DocsVision.
    Он описан по ссылке dvprofessionals.blogspot.com/2010/03/blog-post_22.html

    Казалось бы — это то, что нам нужно, но — только на первый взгляд.

    image

    Как видно из скриншота — только по одной записи за раз, что даже в случае пары десятков локальных учеток — ну очень неудобно. А, исходя из реального опыта — клиенты без домена встречаются не реже, чем клиенты с доменом. И в этом случае — все пользователи заведены на сервере локально. И руками обрабатывать сотню-другую учеток — ничуть не улыбается.

    Поэтому и было принято решение — написать нечто свое.

    Исследование структуры БД



    DocsVision — платформа закрытая и документирована только в рамках предоставленных разработчикам API. Но, кое-какая информация все-таки по интернету гуляет, и ее оказалось достаточно.

    Основными интересующими нас таблицами являются dvsys_instances и dvsys_security. Первая содержит информацию обо всех объектах системы (карточки, папки, справочники, файлы и т.д.), вторая же. не мудрствуя лукаво, хранит описатели безопасности. Структура таблиц приведена ниже.

    image
    image


    Связь таблиц осуществляется по полям dvsys_instances.SDID и dvsys_security.ID. Также, необходимо отметить, что количество записей в этих таблицах различается очень сильно, спасибо наследованию прав. Например, у одного из клиентов эти значения составляют 277571 и 6639 соответственно.

    Итак, задача поставлена — необходимо пройтись по всем строкам второй таблицы, раскодировать описатели безопасности, заменить старые SID для необходимых записей на новые и, предварительно закодировав, записать обратно. Что ж, приступим.

    Реализация задуманного



    В качестве языка разработки был выбран PowerShell, так как большую часть необходимых задач с его помощью можно решить легко и непринужденно.

    Для удобства использования все настройки были вынесены в отдельный файл. К таковым можно отнести таблицу соответствия аккаунтов и настройки доступа к БД.

    $SIDReplacement = @{
    "S-1-5-21-2179127525-659978549-3108502893-1019" = "a31003\akushsky"; #akushsky
    "S-1-5-21-2179127525-659978549-3108502893-1016" = "a31003\ASPNET"; #ASPNET
    "S-1-5-21-2179127525-659978549-3108502893-1008" = "a31003\DocsVision Administrators"; #DV Administrators
    "S-1-5-21-2179127525-659978549-3108502893-1010" = "a31003\DocsVision Power Users"; #DV Power Users
    "S-1-5-21-2179127525-659978549-3108502893-1012" = "a31003\DocsVision Users"; #DV Users
    "S-1-5-21-2179127525-659978549-3108502893-1026" = "a31003\dobriy"; #DV Editors
    }
    
    $SQLServerName = "a31003"
    $SQLDatabaseName = "DV-BASE"
    


    Мне показалось логичнее запускать скрипт на целевом сервере, поэтому старые учетные записи присутствуют в виде SID-ов (для их получение также можно использовать PowerShell, об этом знает Гугл). А новые — укажем в виде учеток и их SID-ы получим в процессе работы скрипта.

    Также, как выяснилось в процессе разработки, PowerShell не умеет создавать экземпляры объектов generic-ков, поэтому из недр Гугла для этого пришлось достать функцию New-GenericObject. Ее код приводить не буду, в конце статьи будет ссылка на репозитарий — все там.

    Для удобства, при работе скрипта, в консоль выводится лог, поэтому было решено создать структуру для более удобного накапливания информации.

    # Struct for log
    add-type @"
    public struct DVObject {
       public string ID;
       public string Description;
       public string Accounts;
    }
    "@
    


    Для наиболее полного логирования запрос, конечно же, возвращает гораздо больше информации, чем мог бы, но — красота требует жертв.

    SELECT I.InstanceID, I.Description, S.ID, S.SecurityDesc
    FROM [DV-BASE].[dbo].[dvsys_security] S
    LEFT JOIN [DV-BASE].[dbo].[dvsys_instances] I
    ON I.SDID = S.ID
    


    Поэтому, чтобы не обрабатывать один и тот же описатель много раз — идентификаторы записываются.

    # Replace SDDL in each row only once 
    $IDList = New-Object System.Collections.Generic.HashSet[Guid]
    
    ...
    
    # We need only one SQL-request for each ID
    if ($IDList.Contains($row["ID"])) {continue}
    else {$isOk = $IDList.Add($row["ID"])}
    


    А далее начинается самое интересное:
    1. Полученный SecurityDesc представляет собой бинарную последовательность символов, закодированную Base64. А, для подмены SID-ов нам необходимо получить SDDL-строку. Описание формата есть на MSDN. А теперь то же самое, только кодом:

      	# Convert SDDL from Base64 to binary form
      	$ObjectWithSDDL = ([wmiclass]"Win32_SecurityDescriptorHelper")
      .BinarySDToSDDL([System.Convert]::FromBase64String($row["SecurityDesc"]))
      	$sddl = $ObjectWithSDDL.SDDL
      

    2. Далее, вычленяя при помощи регулярки SID-ы из SDDL-строки, проверяем их по таблице соответствия и, в случае совпадения, подменяем.

      	# Match all SIDs and replace
          [regex]::Matches($sddl,"(S(-\d+){2,8})") | sort index -desc | % {
      		if ($SIDReplacement.ContainsKey($_.ToString()))
      		{	
      			# Translate NT account name to SID
      			$objUser = New-Object System.Security.Principal.NTAccount($SIDReplacement[$_.ToString()])
      			$strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
      
      			# Replace it in SDDL
      			$sddl = $sddl.Remove($_.index,$_.length)
      			$sddl = $sddl.Insert($_.index,$strSID.Value)
      
      			# Add to list of current object accounts
      			$dvobject.Accounts += $SIDReplacement[$_.ToString()]
      			$dvobject.Accounts += "`n"
      
      			# Set replace completed
      			$replaceComplete = $true
      		}
      	}
      

    3. Если замена была произведена, то тогда необходимо произвести обратные действия и записать данное изменение в БД:
      	if ($replaceComplete)
      	{
      		# Add current info object to list
      		$DVObjectList.Add($dvobject)
      
      		$binarySDDL = ([wmiclass]"Win32_SecurityDescriptorHelper").SDDLToBinarySD($sddl).BinarySD
      		$ret = [System.Convert]::ToBase64String($binarySDDL)
      
      		##### Update database #####
      
      		# Update query for currently replaced SDDL
      		$SqlQuery = 
      		"UPDATE [dbo].[dvsys_security]
      		 SET Hash = '" + $binarySDDL.GetHashCode() + "', SecurityDesc = '" + $ret + "'
      		 WHERE ID = CONVERT(uniqueidentifier, '" + $row["ID"] + "')"
      
      		# Attach query to command
      		$UpdateSqlCmd.CommandText = $SqlQuery
      
      		# Execute update query
      		if ($UpdateSqlCmd.ExecuteNonQuery())
      		{
      			Write-host "Update true for ID: " $row["ID"]
      		}
      		else
      		{
      			Write-host "Update false for ID: " $row["ID"]
      		}
      	}
      



    Таким образом, получен универсальный механизм для любой миграции — между серверами заказчика, между сервером разработки и заказчиком, да мало ли еще как.

    В конце, как и было обещано: ссылка на репозитарий с описанным кодом.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Вот спасибо. После отпуска предстоит, как раз, серваки менять сначала бд потом workflow.
      На курсах сказали, что все достаточно просто. Но про сиды локальных учеток не заикались…
        0
        Судя по их документации — все просто тривиально. А на деле — ух… Столько раз я уже с подобными приколами сталкивался…
        0
        ох, радной DocsVision, когда-то разрабатывал на нем… По сабжу — почему бы не использовать специально предназначенный инструмент — MS SqlServer Integration Services (SSIS)?
          0
          Видимо потому, что до сего момента никогда об этом не слышал. Спасибо за наводку, почитаю.

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

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