Как стать автором
Обновить

Тонзиллэктомия ректально: работаем с AD в Powershell без AD cmdlets

Время на прочтение 23 мин
Количество просмотров 19K
В Windows Server 2008 впервые появились замечательные командлеты PowerShell для работы с ActiveDirectory. Эти прекрасные, логичные, интуитивно понятные и чрезвычайно мощные инструменты вызывали у меня чувство грусти, если не сказать — «досады»: они были недоступны мне, эникейщику непрофильной конторы. Все одиннадцать сетей, которые я обслуживал были построены на базе Windows 2003 R2.

Одиннадцать несвязанных доменов в одиннадцати несвязанных сетях в разных городах, разбросанных по Дальнему востоку. И ни в одной из них — ни то, что «Семёрки», даже «Висты» нет, что ставит крест на попытках использования AD cmdlets в связке с две тысячи третьей.


Задача была сформулирована следующим образом — «создать код, способный выполнять основные операции по управлению AD из сценариев PowerShell, исполняемый в Windows XP / 2003». О том, как она была решена, читайте под хабракатом (осторожно, костыли; много текста и кода).

Контекст


В последнее время унылая, в общем-то, работа эникейщика несколько осложнялась периодом аномальной активности системных администраторов и прочих начальников по линии ИТ: они решили внести массу изменений настройки серверных служб, рабочих станций пользователей и прочего ИТ-хозяйства. Естественно, руками эникейщиков, т.к. нормальных инструментов типа Альтириса или MS SCCM у них не было.

В какой-то момент я понял, что физически не успеваю воплощать в жизнь их гениальные идеи во вверенных мне сетях, и задумался об автоматизации. Проведенный анализ рабочего времени показал, что, в первую очередь, нужно ускорить процесс тиражирования изменений в AD. Для этого, как мне тогда казалось, были необходимы Active Directory Comandlets (для работы которых требовалось закупить хотя бы одну лицензию для Windows 7).

Руководители бизнеса и сисадмины были неумолимы: «Зачем нам новая система, если старая отлично работает? Ах, она работает не отлично? Но ведь есть ты, чтобы все было замечательно! Иначе зачем тебе столько платят? Да, кстати, если тебе так нужна новая система, купи её сам на свои деньги и используй! И вообще, если не способен решать задачи бизнеса в рамках предложенного инструментария, пшёл вон из Компании!»

«Okay,» — ответил я: «идти вон» очень сильно не хотелось. В конце концов, реализация «хотелок» руководства — одно из ключевых отличий эникейщика от настоящего системного администратора. Стало ясно, что в очередной раз придется «применять смекалку» и «выкручиваться». В качестве платформы для построения системы «эрзац-управления AD» был выбран PowerShell (в основном, за легкость работы с .NET и COM).

Код


Получение уникального имени подразделения

Если вы работали с ActiveDirectory или знакомы с иной реализацией LDAP, вам известно, насколько широко используются в самых разных случаях уникальные (отличимые) имена — DNs (Distinguished Names).

Уникальное имя LDAP состоит из нескольких относительных уникальных имен — RDNs (Relative Distinguished Names), представляющих собой пары «Атрибут = Значение». Пример относительного уникального имени для подразделения (Organization Unit):
ou=Managers


Пример уникального имени для этого же подразделения:
ou=Managers,DC=example,dc=com


Эта запись говорит о том, что в домене AD с именем «example.com» на первом уровне находится подразделение «Managers».

По своему опыту могу сказать, что уникальные имена LDAP не являются интуитивно понятными и вызывают у начинающих эникейщиков некоторое затруднение. Поэтому я счел необходимым написать простую функцию, конвертирующую интуитивно понятное представление вида «example.com/Managers/Accounting/» в нотацию DN:
<#
.SYNOPSIS
	Принимает путь к OU в формате "example.com/123/456", возвращает его Distinguished Name.
	
.Description
	Корректность пути и его существование не проверяется, что позвоялет использовать функцию
	для несуществующих путей.
	ВНИМАНИЕ: имя домена должно быть полным DNS-именем, а не NEIBIOS именем (т.е. example.com, а не EXAMPLE)
	
.PARAMETER Path
    Путь, который будет сконвертирован в DN
 
.OUTPUTS	
	System.String. Возвращает Distinguished Name, соответствующий переданному пути.
#>	
Function Convert-ADPath2DN([STRING]$Path)
{	$Res = ""
	$ResOU = $null
	#Преобразуем путь к OU в OU DN
	$P = Join-Path $Path "/"
	$P = $P.Replace("\","/")
	#Проверяем, получилось ли у нас что-то похожее на путь
	if ($P -match "^(?<DNS_DOMAIN_NAME>(\w+\.\w+)+)\/.+$")
	{
	
	
		$i = 0
		$DNS_DOMAIN_NAME = $Matches.DNS_DOMAIN_NAME		
		#Формируем путь по OU
		While (-not ($P -eq $DNS_DOMAIN_NAME))
		{
			$i++
			$OU = Split-Path -Leaf $P		
			$P = Split-Path -Parent $P
			If ($i -ne 1)
			{
				$ResOU = $ResOU + ","
			}
			$ResOU = $ResOU+ "OU=$OU"
		}		
	}	
	else 
	{
		$DNS_DOMAIN_NAME = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name		
	}
	
	#Меняем точки на слэши, чтобы модно было использовать удобные функции для работы с путями
	$DNS_DOMAIN_NAME = $DNS_DOMAIN_NAME.Replace(".","\")
		
 
 	$DC_NAMES = @()		
	While ($DNS_DOMAIN_NAME -ne "")
	{
		$DC = Split-Path -Leaf $DNS_DOMAIN_NAME
		$DNS_DOMAIN_NAME = Split-Path -parent $DNS_DOMAIN_NAME					
		$DC_NAMES = $DC_NAMES + $DC
	}
		
		
		
	$Count = $DC_NAMES.Count	
	for ($i=$Count;$i -gt 0;$i--)
	{
		$DC = $DC_NAMES[$i - 1]
		If ($i -ne $Count)
		{
			$ResDC = $ResDC + ","
		}
		$ResDC = $ResDC+ "DC=$DC"		
		if ($ResOU -ne $null)
		{
			$Res = $ResOU + "," + $ResDC		
		}
		else
		{
			$Res = $ResDC
		}
	}
		
	Return $Res
}


Пример использования:
$Var = Convert-ADPath2DN -Path "example.com/Test/"
$Var
OU=Test,DC=example,DC=com


Хотя эта функция и не имеет непосредственного отношения к управлению ActiveDirectory, она очень полезна и используется в других функциях.

Создание подразделения организации

Каждый эникейщик должен знать, что тестировать скрипты для ActiveDirectory нужно на специально выделенном для этого тестовом домене, желательно физически отключенном от основной сети предприятия.

Не менее важным является и соблюдение политики компании по снижению расходов на ИТ (думаю, что во многих непрофильных организациях есть подобные указания), а значит, ни ПК, способного «потянуть» виртуальную машину с Windows 2003, ни, тем более, отдельного сервера, на котором можно развернуть тестовую среду, в распоряжении эникейщика, как правило, нет.

Поэтому наш герой, вопреки строгим наказам старших товарищей, тестирует свои наработки прямо в «боевой» AD. Не будем его за это осуждать, а попытаемся вместо этого — помочь.

Первое, что ему следует сделать — создать отдельное подразделение (Organization Unit), внутри которого будет происходить дальнейшая отработка навыков скриптописательства. Ну, а поскольку тема топика связана с программированием на Powershell, реализуем на нём соответствующую функцию.

Для этого нам понадобится задействовать конструктор Create класса System.DirectoryServices.DirectoryEntry. Итоговый вариант может выглядеть, например, так:
<#
.SYNOPSIS
	Создает подразделение организации в AD с заданным именем по заданному пути.	
.Description
	При создании подразделения существование родителя не проверяется, что может вызвать проблемы.
	Эта функция - служебная. Для создания подразделений рекомендуется использовать функцию 
	New-ADOrganizationUnit
	
.PARAMETER Path
	Путь к подразделению AD, в котором будем создавать OU (т.е. родителю).
	
.PARAMETER Name
	Имя создаваемого подразделения (Organization Unit).
 
.OUTPUTS	
	$null. Функция не возвращает значений.
#>	
Function New-ADOrganizationUnit_simple([STRING]$Path,[STRING]$Name)
{
	#Отключаем сообщения об исключениях, т.к. они будут возникать довольно часто.
	#основная причина - попытка создать OU, который уже существует.	
	$ErrorActionPreference = "SilentlyContinue"
	
	#Конвертируем путь в нотацию DN (см. описание функции выше)
	$OUDN = Convert-ADPath2DN -Path $Path	
	
	#Приводим путь к стандарту ADsPath
	$OUDN = "LDAP://$OUDN"	
	
	#Создаем объект, представляющий текущий домен
	$objDomain = [ADSI]$OUDN	
	
	#Создаем подразделение
	$objOU = $objDomain.Create("organizationalUnit", "ou=$Name")
	$objOU.SetInfo()
	
	#Обработка исключений
	Trap
	{
		#Если дело в том, что такое подразделение уже есть, оповестим об этом пользователя.	
		if ($_.Exception.ErrorCode -eq $SYSTEM_ERROR_CODE_OU_ALREADY_EXISTS)
		{
			Write-Host "OU $Name в $Path уже существует"
		}
	}
	
}


Явным недостатком указанной реализации является неспособность этой функции к созданию полной ветки подразделений: если вам потребуется создать OU "example.com/Users/HR/Women" в домене, в котором нет подразделения "example.com/Users", то вы не сможете использовать её для решения данной задачи.

Точнее, сможете, но это будет крайне неудобно, т.к. придется сначала создать OU «Users», затем «HR» и только после этого — «Women».

Такой сценарий явно противоречит принципу автоматизации рутины, а потому — неприемлем. Вместо этого гораздо лучше использовать функцию, которая создаст всю ветку автоматически, например, такую:
<#
.SYNOPSIS
	Создает подразделение организации в AD по заданному пути.
	
.Description
	Будет создан не только последний элемент, но и вся родительская иерархия (все родительские подразделения).
	
.PARAMETER Path
	Путь к создаваемому подразделению.
 
.OUTPUTS	
	$null. Функция не возвращает значений.
#>	
Function New-ADOrganizationUnit ([STRING]$Path)
{
	#Проверяем, соответствует ли переданная строка пути к подразделению (example.com/Unit1/Unit2...)
	If ($Path -match "^(?<DNS_DOMAIN_NAME>(\w+\.\w+)+)\/.+$")
	{
		$i = 0	
		#Меняем направление слешей, чтобы использовать командлеты "*-Path"
		$Pth = Join-Path $Path "/"
		$Pth = $Pth.Replace("\","/")
				
		$OUs = @()
		#Создаем OU по одному
		While ($Pth -ne "")
		{
			$i++
			$Pos = $Pth.IndexOf("/") 			
			
			If ($i -eq 1)
			{
				$DNS_DOMAIN_NAME = $Pth.Substring(0,$Pos)
			}
			else
			{
				$OU = $Pth.Substring(0,$Pos)			
				$OUs = $OUs + $OU
			}	
			$Pth = $Pth.Substring($Pos+1,($Pth.Length - $Pos -1))			
		}
		
		#Создаем весь путь OU
		$Pth = $DNS_DOMAIN_NAME		
		For ($i=0;$i -lt $OUs.Count;$i++)
		{	
			$OU = $OUs[$i]		
			#Вызываем предыдущую функцию для непосредственного создания OU. 
			#Подразделения создаются "сверху вниз", поэтому ситуация создания
			#дочернего подразделения при отсутствии родительского - исключена.
			New-ADOrganizationUnit_simple -Name $OU -Path $Pth
			$Pth = $Pth + "/" + $OU
				
		}		
	}	
}


Использование функции тривиально:
#Создаём подразделения "Test" и "MyFirstOU" в домене example.com
New-ADOrganizationUnit -Path "example.com/Test/MyFirstOU/" | Out-Null


Добавление групп безопасности

Матёрые сисадмины, начитавшись серьёзных книг, рекомендуют своим падаванам предпочитать группы безопасности одиночным учетным записям пользователей в любых сценариях администрирования AD и, тем более, при планировании её структуры (если, конечно, падавана вообще допускают участию в решении этой задачи).

В качестве аргумента обычно приводят тиражируемость получаемых конструкций: если, например, разрешить чтение некоторой групповой политики, разрешающей запуск произвольного ПО лично директору (точнее, его учётной записи), то при возникновении необходимости предоставить такую возможность ещё кому-то, то придется все шаги по настройке прав доступа повторить заново и для другого Важного Дяди.

Если же создать группу, например, "gAllowRunAnything" и дать разрешение запускать произвольные исполняемые файлы членам этой группы, то процесс сильно упростится: достаточно будет включить учетную запись второго сотрудника в эту группу. Очевидно, такой вариант гораздо проще для эникейщика (которому придется производить эти манипуляции) и прозрачнее для системного администратора (которому потом нужно будет разобраться с тем, что там намудрил эникейщик).

Не будем спорить со старшими коллегами о важности групп, а просто реализуем возможность их создания в виде функции PowerShell:
<#
.SYNOPSIS
	Создает группу AD в заданном OU. 	
	
.Description
	Создает глобальную группу безопасности в заданном подразделении организации (Organization Unit).
	
.PARAMETER Path
	Путь к подразделению организации, в котором нужно создать группу.
	
.PARAMETER Name
	Имя создаваемой группы
	
.PARAMETER Force
	Модификатор создания пути. Если он задан, то путь к OU будет принудительно создан.
	
.OUTPUTS	
	$null. Данная функция не возвращает значений. 
#>	
Function New-ADGroup([STRING]$Path, [STRING]$Name, [System.Management.Automation.SwitchParameter]$Force)
{
	#Если указано, принудительно создаём иерархию подразделений
	$ErrorActionPreference = "SilentlyContinue"
	If ($Force -eq $true)
	{
		New-ADOrganizationUnit -Path $Path		
	}	
	#Конвертируем понятное неспециалисту представление пути в DN
	$OUDN = Convert-ADPath2DN -Path $Path
	
	#Создаем объект - представление домена
	$OUDN = "LDAP://$OUDN"
	$OU = [ADSI]"$OUDN"	
	
	#Создаем и сохраняем группу безопасности в подразделении
	$Group = $OU.Create("group", "cn=$Name")
	$Group.Put("sAMAccountName", "$Name")
	$Group.SetInfo() 
	
	#Обработка исключений
	Trap
	{
		#Наиболее часто возникающее исключение, не требующее остановки:
		# "такая группа уже существует"
		if ($_.Exception.ErrorCode -eq $SYSTEM_ERROR_CODE_OU_ALREADY_EXISTS)
		{
			Write-Host "Группа $Name в $Path уже существует"
		}
	}
}


Как видите, здесь тоже используется одна из версий конструктора Create класса System.DirectoryServices.DirectoryEntry.

И снова использование функции элементарно (я специально старался упрощать синтаксис, чтобы мои коллеги, такие же эникейщики, не имеющие специальных знаний в области организации и администрирования AD / LDAP, могли быстро разобраться):

#Создадим группу "gSales" в подразделении "Unit1". При необходимости - создадим само подразделение и все родительские подразделения.
New-ADGroup -Path "example.com/Test/MySuperScriptingResults/Fatal/Unit1" -Name "gSales" -Force 


Создание и привязка объектов групповых политик

Что такое ActiveDirectory? Википедия говорит, что это LDAP-совместимая реализация службы каталогов от MS. Системный администратор, наверное, вспомнит о лесах, доменах и доверии, безопасник — о механизме аутентификации, а эникейщик — о групповых политиках.

Почему о них? В них — вся профессиональная жизнь эникейщика: установка ПО на рабочие станции, блокировка настроек IE, не позволяющая пользователям устанавливать различные "*-Бары", SRP, решающие проблему с играми в рабочее время и даже блокировка панели задач, которую Мариванна так любит сдвинуть влево, требуя от эникейщика незамедлительно «вернуть, как было, а то работать невозможно». Впрочем, я слишком углубился в воспоминания. Думаю, с тезисом о важности GPO в Windows-сетях никто спорить не станет.

А раз GPO нужны и важны, значит следует включить некие механизмы для работы с ними в создаваемую библиотеку. Что нужно сделать с объектом GPO в первую очередь? Разумеется, создать его (очевидно, никакая операция не может предшествовать созданию):
<#
.SYNOPSIS
	Создает новый объект групповой политики в заданном домене. 
	
.Description
	Привязка созданного объекта к подразделению организации не выполняется.	Для работы используется
	COM-объект GPMC (соответственно, GPMC должна быть установлена).
	
.PARAMETER DomainDNSName
	FQDN-имя домена, в котором будет создан объект групповой политики.

.PARAMETER GPOName
	Имя создаваемого объекта групповой политики.
 
.OUTPUTS	
	GPMGPO. Возвращает созданный объект групповой политики.
#>	
Function New-GPO([STRING]$DomainDNSName,[STRING]$GPOName)
{	
	$GPM = New-Object -ComObject GPMgmt.GPM	
	#Список предопределенных констант
	$GPMConstants = $GPM.GetConstants()		
	#Объект домена
	$GPMDomain = $GPM.GetDomain($DNS_DOMAIN_NAME, $DNS_DOMAIN_NAME, $Constants.UseAnyDC)
	#Создаем GPO	
	$GPMGPO = $GPMDomain.CreateGPO()
	$GPMGPO.DisplayName = $GPOName
	
	Return $GPMGPO	
}


Эта функция создает объект GPO в репозитории объектов, и ничего более. Она полезна, но для практического исользования — недостаточна: созданный объект требуется еще привязать (Link) к подразделению. Без этого он останется как бы «неактивным» (строго говоря, он активен, для него просто не определена область воздействия). А для того, чтобы присоединить политику к OU, нужно получить её объектное представление. Логично попытаться получить его, зная имя GPO, например, так:
<#
.SYNOPSIS
	Получает COM-объект заданной GPO по её имени.	
	
.Description
	Позволяет получить пригодный для модификации объект GPO, зная её имя.	
	
.PARAMETER DomainDNSName
	FQDN-имя домена, в котором выполняется поиск GPO.

.PARAMETER GPOName
 	Имя GPO, который нужно найти.
	
.OUTPUTS	
	GPMGPO. Возвращает ссылку на COM-объект найденной GPO ( или $null, если GPO не была найдена).
#>	
Function Get-GPO([STRING]$DomainDNSName,[STRING]$GPOName)
{
	$GPMGPO = $null
	#Отключаем реакции на возникающие исключения
	$ErrorActionPreference = "SilentlyContinue"
	
	#Создаем объект, представляющий GPMC
	$GPM = New-Object -ComObject GPMgmt.GPM	
	
	#Список предопределенных констант
	$GPMConstants = $GPM.GetConstants()		
	
	#Получаем объект GPMDomain, представляющий текущий домен
	$GPMDomain = $GPM.GetDomain($DomainDNSNAme, $DomainDNSNAme, $GPMConstants.UseAnyDC)	
	
	#Ищем групповую политику в домене по её имени
	$GPMGPO = $GPMDomain.SearchGPOs($GPM.CreateSearchCriteria()) | Where-Object{$_.DisplayName -eq $GPOName}		
	
	#Возвращаем объект GPO. Если политика не найдена - вернется $null.
	Return $GPMGPO
}


Имея в арсенале механизм поиска объекта GPO по имени, можно попытаться выполнить привязку:
<#
.SYNOPSIS
	Выполняет присоединение (Link) объекта групповой политики к заданному подразделению организации (OU). 
	
.Description
	Присоединяет существующий объект GPO к существующему подразделению организации в рамках одного домена.
	Кросс-доменное присоединение не поддерживается.
	
.PARAMETER	DomainDNSName
	FQDN-имя домена, в котором выполняется операция присоединения.
	
.PARAMETER	GPOName
	Имя присоединяемого объекта GPO. Объект должен быть доступен в репозитории объектов групповых политик заданного домена.
	
.PARAMETER	OUPath
	Путь к подразделению организации, к которому нужно присоединить заданный объект GPO.
 
.OUTPUTS	
	$Null. Данная функция не возвращает результат.
#>	
Function Mount-GPOToOU ([STRING]$GPOName,[STRING]$OUPath,[STRING]$DomainDNSName)
{
	#Ищем нужный объект GPO в репозитории домена
	$GPMGPO = Get-GPO -DomainDNSName $DomainDNSName -GPOName $GPOName
	
	#Убеждаемся в том, что объект найден
	If ($GPMGPO -ne $Null)
	{	
		#Приводим путь к нотации DN
		$OUDN = Convert-ADPath2DN -Path $OUPath
		
		#Получаем представление домена в виде COM-объекта GPMC
		$GPM = New-Object -ComObject GPMgmt.GPM		
		$GPMConstants = $GPM.GetConstants()	
		$GPMDomain = $GPM.GetDomain($DomainDNSName, $DomainDNSName, $Constants.UseAnyDC)
		
		#Получаем представление интересующего подразделения
		$GPMSOM = $GPMDomain.GetSOM($OUDN)				

		#Выполняем привязку (Link) объекта GPO к подразделению
		$GPMSOM.CreateGPOLink(-1, $GPMGPO) | Out-Null		

        trap
        {
            #Исключения создаются внутри COM-объекта. У нас нет возможности определять их причину.
            #Как правило, она заключается в том, что указанный линк уже существует. Поэтому продолжаем
            continue           
        }
		
	}
	#Если объект GPO по заданному имени не найден, сообщаем пользователю и выбрасываем исключение
	else
	{
		Write-Host "Cannot find a GPO object named $GPOName"
		Throw "Cannot_find_GPO"
	}
}


Последняя функция позволяет привязывать не только свежесозданный, но и любой имеющийся в репозитории объект GPO. Одним из применений этой функции стала практика быстрого «ввода в продакшн» ранее созданных объектов групповой политики: сначала они тестировались в специальном обособленном подразделении (OU), а затем внезапно ночью по окончании тестирования и получения одобрения от системного администратора связывались с «боевыми» подразделениями. Было удобно.

Дотошный читатель может спросить о том, с какой целью здесь использован COM-объект GPMC. Всё очень просто: .NET предоставляет слишком высокий уровень абстракции и не позволяет выполнять некоторые операции. Например, я не нашел способа привязать GPO к подразделению, не смог выполнить импорт и экспорт GPO (см. далее) с помощью System.DirectoryServices.DirectoryEntry. Используя GPMC выполнить указанные действия не только возможно, но и относительно просто.

Импорт и экспорт настроек GPO

Итак, у нас появилась возможность создания объектов групповых политик и их привязки к подразделениям. Вспомним о целях — зачем мы вообще разрабатываем все эти функции? Чтобы помочь эникейщику в его нелёгком труде. В какой именно помощи нуждается эникейщик? В автоматизации рутинных операций, которые ему следует выполнить. Какие из них связаны с GPO? Как правило, речь идет о внесении изменений, разработанных системным администратором, в параметры GPO во множестве сетей (или в разных OU одного домена).

Обычно это происходит так: безопасник решает, что, например, в целях запрета «выноса» информации на съемных носителях следует запретить использование оных. Сисадмин подготавливает описание соответствующих настроек GPO (инструкцию) и дает эникейщикам указание растиражировать указанные изменения в своих сетях.

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

И здесь на помощь нашему бойцу клавиатуры и отвёртки приходит всемогущая MS со своим механизмом импорта / экспорта настроек GPO, реализованным в рамках GPMC. Этот механизм позволяет импортировать настройки, заданные в одном GPO в другой, даже если эти два объекта групповой политики (донор и реципиент) находятся в разных доменах. Ну, а Powershell позволяет довести процесс до полного автоматизма.

Процедуру экспорта эталонной политики мы оставим системному администратору, а сами займемся импортом. Для начала рассмотрим, что же представляет из себя резервная копия GPO (сама MS настаивает на том, что это именно Backup, а не Export):


Имя папки в данном примере — это уникальный идентификатор экспортированного объекта GPO. Он нам понадобится при импорте, поэтому уже сейчас было бы неплохо научиться этот GUID получать. Рискую сойти за известного персонажа сетевого фольклора, но проще всего получить этот идентификатор просто взглянув на имя папки, например, так:
<#
.SYNOPSIS
	Позволяет определить GUID резервной копии GPO по её (копии) содержимому.	
	
.Description
	Функция просматривает список дочерних папок в резервной копии GPO, находит среди них папку, имя которой - GUID, и
	возвращает это значение в качестве GUID'а резервной копии. Идея метода в том, что корректная резервная копия GPO
	содержит одну папку с именем в виде GUID'а. Этот имя этой папки - всегда соответствует GUID'у резервной копии.
	
.PARAMETER Path
	Path - путь к папке, в которой находится резервная копия GPO. 	
 
.OUTPUTS	
	System.String. GUID резервной копии GPO, расположенной по заданному пути.	
#>	
Function Get-ExportedBackupGUID([STRING]$Path)
{
	#Проверяем, доступна ли указанная папка, если нет - выбрасываем исключение
	If (-not (Test-Path $Path))
	{
		Write-Host "Папки $Path не существует"
		Throw "Backup dir path not found"		
	}
	
	#Получаем список подпапок
	$Children = Get-ChildItem -Force -LiteralPath $Path
	
	#Ищем в среди подпапок ту, которая именована GUID'ом
	Foreach ($Child in $Children)
	{	
		If ($Child.FullName -match "^*\{\w{8}\-(\w{4}\-){3}\w{12}\}$")
		{
			Return $Matches[0]
		}
		
	}	
	#Папка доступна, но резервных копий GPO в ней нет.
	Write-Host "В папке $Path не найдены резервные копии политик" 
	Throw "GPO Backup(s) not found"	
}


После того, как GUID экспортированного объекта GPO стал известен, можно загрузить его настройки в произвольный объект групповой политики в репозитории AD:
<#
.SYNOPSIS
	Импортирует настройки GPO из резервной копии.	
	
.Description
	Позволяет импортировать в заданный объект групповой политики настройки, сохраненные в виде резервной копии объетка GPO. 
	Речь идет именно об импрорте, а не восстановлении из резервной копии, поэтому допускается загрузка параметров из любого GPO. 
	Если целевого объекта GPO не существует, он будет создан.
	
.PARAMETER	BackupPath
	Путь к папке с резервной копией GPO

.PARAMETER 	DNS_DOMAIN_NAME
	FQDN-имя домена, в котором находится целевая политика (реципиент).

.PARAMETER	MigrationTablePath
	Не используется. Зарезервировано для будущих версий.

.PARAMETER	NewGPOName
	Имя объекта групповой политики, в который нужно загрузить настройки из резервной копии. Если этот параметр не указан, имя будет взято из
	резервной копии (т.е. параметры будут загружены в GPO с тем же именем, которое было у оригианльного GPO-донора).
	
.OUTPUTS	
	$null. Функция не возвращает значений.
#>	
Function Import-GPO ([STRING]$BackupPath, [STRING]$DNS_DOMAIN_NAME, [STRING]$MigrationTablePath, [STRING]$NewGPOName = "")
{	
	#Проверяем доступность папки с резервной копией GPO. Если она недоступна, выбрасываем исключение
	If (-not (Test-Path $BackupPath))
	{
		Write-Host "Неверно указана папка с архивом GPO: $BackupPath"
		Throw "GPO Backup path not found"
	}
	
	#Получаем COM-объект GPMC - представление домена
	$GPM = New-Object -ComObject GPMgmt.GPM		
	$GPMConstants = $GPM.GetConstants()			
	$GPMDomain = $GPM.GetDomain($DNS_DOMAIN_NAME, $DNS_DOMAIN_NAME, $Constants.UseAnyDC)
	
	#Получаем объект GPMBackupDir, представляющий папку с резервной копией
	$GPMBackupDir = $GPM.GetBackupDir($BackupPath)	
	#Определяем GUID экспортированной GPO
	$BackupGUID = Get-ExportedBackupGUID -Path $BackupPath
	#Получаем объект - представление экспортированного GPO
	$GPMBackup = $GPMBackupDir.GetBackup($BackupGUID)
	
	#Имя GPO, в которую будем импортировать настройки: если не указано, импортируем в объект с 
	#тем же именем, которое имеет резервная копия
	If ($NewGPOName -eq "")
	{
		$TargetGPOName = $GPMBackup.GPODisplayName
	}
	else
	{
		$TargetGPOName = $NewGPOName
	}
		
	#Находим в репозитории объект GPO, в который будем импортировать настройки
	$GPMGPO = Get-GPO -DomainDNSNAme $DNS_DOMAIN_NAME -GPOName $TargetGPOName
	
	#Если GPO с заданным именем не нашелся, создаем его
	If ($GPMGPO -eq $Null)	
	{
		$GPMGPO = New-GPO  -DomainDNSNAme $DNS_DOMAIN_NAME -GPOName $TargetGPOName
	}
	
	#Импортируем содержимое GPO
	$GPMGPO.Import(0, $GPMBackup) | Out-Null		
}



Обобщенным примером использования описанных функций для работы с GPO может служить следующий код:

##Дано: в папке "c:\good_gpo\" находится резервная копия объекта групповой политики, содержащая некие полезные настройки, созданные системным администратором.

#Задача: применить эти настройки к объектам (пользователям и компьютерам), находящимся в подразделении "example.com/TestUnits/OU1"

##Решение:

# Импортируем настройки в созданную GPO с именем "Imported_good_GPO" (он будет создан при необходиости).
Import-GPO -BackupPath "c:\good_gpo\" -Dns_Domain_Name "example.com"  -NewGPOName "Imported_good_GPO"

#Привязываем полученный объект к указанному OU
Mount-GPOToOU -GPOName "Imported_good_GPO" -OUPath "example.com/TestUnits/OU1" -DomainDNSName "example.com"



Доступ к GPO

Всякий, кому приходилось заниматься обслуживанием доменов Windows AD, наверняка, на определенном этапе своей карьеры сталкивался с противоречием настроек, указанных в различных объектах GPO, присоединенных к одному и тому же подразделению организации.

Например, к OU «Users», в котором находятся учетные записи Главновского И.И. (руководителя) и Логинова-Паролева В.А. (эникейщика) может быть присоединено две объекта групповых политик: "pAllow_Run_Any_Executable", разрешающий, как следует из названия, запускать всё, что угодно, и "pDisallow_Run_Anything_Except_HelpDesk", запрещающий запуск всего, кроме клиента хелпдеска.

Очевидно, что первый GPO должен воздействовать на учетную запись г. Главновского, не затрагивая пользователя Логинова-Паролева, а второй — наоборот. Не менее очевидно, что указанные GPO противоречат друг другу по своей сути.

Т.е. необходим некий механизм, позволяющий разграничить области действия указанных GPO. Можно, конечно, создать для одного из фигурантов персональное подразделение (OU) и привязать к нему только нужные GPO, но за подобное решение сисадмин может больно стукнуть эникейщика томиком Шетки по голове. И будет совершенно прав, ибо при таком подходе буквально за несколько месяцев структура AD разрастется так, что и сам админ не разберёт.

Гораздо более гибким (и уже поэтому — правильным) является решение, основанное на разграничении доступа к GPO. В этой схеме предусматривается создание специальных групп безопасности, регулирующих право на чтение и применение каждого объекта групповой политики, с последующим включением всех учетных записей в соответствующие группы.

Для приведенного примера можно было бы создать группу "gAllow_Run_Any_Executable", предоставить право чтения и применения GPO "pAllow_Run_Any_Executable" только членам этой группы и включить в неё учетную запись Руководителя. С политикой для эникейщика — поступить по аналогии.

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

MSDN учит нас, что право доступа к объекту GPO (да, и вообще, к любому объекту AD) можно представить в виде объекта класса System.DirectoryServices.ExtendedRightAccessRule. Такой объект содержит в себе информацию о записи в правилах доступа: кому, что именно делать запрещено или разрешено:
#На PowerShell такой объект, представляющий собой запись ACE, можно создать следующим образом
$NewRule = New-Object -TypeName System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $objTrustee, $objRihgt, $objACT


Попробуем разобраться с аргументами, переданными через ArgumentList. Начнём с очевидного: $objTrustee — это субъект безопасности, к которому относится данное правило. Например, пользователь или группа безопасности. Проще говоря, это «тот, к кому относится данное правило». В конструкции «Машка разрешила Ваське трогать её за ляжку вносить изменения в домен sales.example.com» субъектом безопасности будет Васька.

С точки зрения рядового эникейщика (именно эти сотрудники являются, по задумке автора, основными пользователями приведенного в этой статье набора костылей), проще всего указать субъекта безопасности по его имени. Но здесь возникает нюанс, который следует учитывать: существует довольно много стандартных участников безопасности, имена которых различны в различных языковых версиях Windows. Например, «Администратор» в англоязычной версии Windows имеет имя «Administrator».

Для решения этой проблемы в MS придумали использовать специальные идентификаторы, которые неизменны во всех локализациях ОС. Нам же, в свою очередь, осталось только научиться их использовать. Проще всего использовать перечислимый тип System.Security.Principal.WellKnownSidType:
#Получаем SID группы администраторов домена
$SID = [System.Security.Principal.WellKnownSidType]::AccountDomainAdminsSid


Если же субъект права не является Well-Known (т.е. не входит в перечень стандартных субъектов безопасности), его представление в виде объекта класса System.Security.Principal.NTAccount можно получить по имени:
[STRING]$FQDN = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
$objTrustee = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList "$FQDN", "$Trustee"


Таким образом, представление субъекта права (кому будем разрешать / запрещать) мы создавать научились. Теперь нужно создать представление самого права, $objRihgt (что именно будем разрешать или запрещать). В приведенном выше примере речь идет о праве "вносить изменения" [в домен sales.example.com].

Как известно, количество видов разрешений («чтение», «запись», «удаление» и т.д.) — ограничено, все они входят в список допустимых значений перечислимого типа System.DirectoryServices.ActiveDirectoryRights, поэтому проще всего создать их представления с помощью соответствующего конструктора, для простоты использования обёрнутого в функцию:

<#
.SYNOPSIS
	Создает объект типа System.DirectoryServices.ActiveDirectoryRights по его имени.
	
.Description
	Использует стандартный конструктор класса System.DirectoryServices.ActiveDirectoryRights.
	Написана для удобства перехвата исключений.
	
.PARAMETER	StrRight
	Строковое наименование элемента перечисления System.DirectoryServices.ActiveDirectoryRights.
		
.OUTPUTS	
	System.DirectoryServices.ActiveDirectoryRights или $null. Возвращает объект типа System.DirectoryServices.ActiveDirectoryRights, 
	соотвтетствующий заданному имени, или $null (если такой объект не может быть создан)
#>	
Function Convert-ToAccessRight([STRING]$StrRight)
{
	$Res = $null
	$ErrorActionPreference = "SilentlyContinue"	
	$Res = [System.DirectoryServices.ActiveDirectoryRights]::$StrRight
	$ErrorActionPreference = "Stop"
	Return $Res
}


Осталось только научиться создавать представление характера действия (в примере с Машкой и Васькой речь идет о слове "разрешила"), $objACT — запрета или разрешения (ведь, каждое право может быть как «дано» (Allow), так и «отобрано» (Deny)). К счастью, и на этот раз в MSDN есть нужный перечислимый тип, System.Security.AccessControl.AccessControlType, поэтому получение представления типа правила (разрешающее оно или запрещающее) не вызывает сложностей:
#Представление разрешения
$objACT = [System.Security.AccessControl.AccessControlType]::Allow

#Представление запрета
$objACT = [System.Security.AccessControl.AccessControlType]::Deny


Рассмотрим практический пример установки специфического разрешения на доступ к объекту групповой политики:
<#
Дано:
Необходимо запретить членам группы "gForbidden_Users" чтение объекта GPO "pForbidden_GPO" в домене "Example.com"
#>

#Решение:
#Определяем полное имя домена, оно нам понадобится
[STRING]$FQDN = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
#Создаём представление группы "gForbidden_Users", которой мы будем запрещать чтение
$objTrustee = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList "$FQDN", "gForbidden_Users"

#Создаём представление запрещающего действия
$objActDeny = [System.Security.AccessControl.AccessControlType]::Deny

#Создаём представление права чтения
$objRihgt = [System.DirectoryServices.ActiveDirectoryRights]::GenericRead
 
#Создаем правило доступа "Запретить группе 'gForbidden_Users' чтение". Чуть позже это правило применим к GPO 'gForbidden_Users'
$NewRule = New-Object -TypeName System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $objTrustee, $objRihgt, $objACT

#Создаем представление объекта GPO по его имени.
$GPMGPO = Get-GPO -DomainDNSName "Example.com" -GPOName "pForbidden_GPO"
			
#Получаем из COM-объекта интересующей нас политики .NET-объект класса System.DirectoryServices.DirectoryEntry
[STRING]$GPOPath = $GPMGPO.Path
$objGPO = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList "LDAP://$GPOPath"

#Добавляем новую запись к имеющимся правилам доступа к данному GPO
#Если нужно было бы добавить несколько правил, добавляли бы их по очереди
$objGPO.ObjectSecurity.AddAccessRule($NewRule) | Out-Null

#Сохраняем изменения.
$objGPO.CommitChanges()	| Out-Null	 


В принципе, на этом изменение прав доступа к объекту GPO AD завершено. Однако и тут есть нюанс: дело в том, каждому объекту групповой политики соответствует папка в доменной DFS (\\domanname.example.com\SYSVOL\). И права доступа на эту папку обычно соответствуют тем, которые установлены для самого объекта GPO. Более того, если эти права будут отличаться, GPMC выдаст соответствующую ошибку:


Подобная рассинхронизация, если вдуматься, логична: в представленном выше коде мы меняем права доступа к объекту GPO, не трогая файловую систему. Можно, конечно, по аналогии добавить код, изменяющий разрешения и для папки в SYSVOL, но есть способ проще. Он основан на том, что GPMC автоматически проставляет разрешения для папки при изменении разрешений самого GPO. Таким образом достаточно внести фейковое изменение с помощью консоли GPMC, и она (консоль) сама позаботится о соответствиях:
#FQDN-имя домена
$DomainDNSName = "Example.com"
#Имя объекта GPO, для которого нужно установить соответствие разрешений
$GPOName = "pForbidden_GPO"

#Получаем представление объекта GPO по его имени
$GPMGPO = Get-GPO -DomainDNSName $DomainDNSName -GPOName $GPOName

#Получаем разрешения 
$GPOSecurityInfo = $GPMGPO.GetSecurityInfo()

#Устанавливаем те же самые, только что полученные разрешения.
#Консоль GPMC автоматически выставит такие же разрешения для папки в SYSVOL.
$GPMGPO.SetSecurityInfo($GPOSecurityInfo) | Out-Null  


Заключение


После некоторого периода «обкатки» я понял, что писать (или править написанные ранее) скрипты под каждую отдельную задачу по управлению AD — гораздо приятнее, чем решать эту же задачу вручную. Особенно, если в распоряжении есть удобный набор функций для всех операций, которые приходится выполнять.

Однако эникейщики — народ ленивый, и через некоторое время написание сценариев для выполнения заданий по администрированию AD откровенно наскучило. Появились мысли о дальнейшем развитии механизма, которые воплотились в создании «Универсального сценария администрирования», способного скачивать задания в формате XML-файлов с сервера сисадминов и выполнять эти задания без моего участия (я в это время мог спокойно выполнять свои сугубо эникейские обязанности, например, вводить логины в окна авторизации, протирать клавиатуры и т.д.).

Полное описание получившегося механизма явно выходит за рамки этой статьи. Отмечу только тот факт, что сценарий получил модульную структуру, и одним из модулей стал CM_ActiveDirectory, доступный вместе с описанием на PasteBin. Для корректной работы он, как и все остальные модули этой системы, требует загрузки модуля CM_System, который (вместе с соответствующим файлом описания) можно скачать оттуда же.

Надеюсь, читателям изложенная информация не понадобится. Хочется верить, что в 2012 году людям, которым требуется интенсивно администрировать множество доменов, компании предоставляют адекватные инструменты, и описанная контора является единственным неприятным исключением. Но если вы оказались в подобной ситуации, — не отчаивайтесь! Как видите, даже с использованием «устаревших» средств можно получать нужный результат.

Адекватных вам инструментов, коллеги!
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+16
Комментарии 13
Комментарии Комментарии 13

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн