In Windows Server 2008, the first wonderful
PowerShell cmdlets to work with ActiveDirectory appeared . These wonderful, logical, intuitive and extremely powerful tools made me feel sad, if not to say “annoyance”: they were inaccessible to me, an enikeyschiku non-core office. All eleven networks that I served were built based on Windows 2003 R2.
Eleven unrelated domains in eleven unrelated networks in different cities scattered across the Far East. And in none of them - not the fact that there is no “Seven”, even “Vista”, which puts an end to attempts to
use AD cmdlets in conjunction with two thousand and three .

The task was formulated as follows: “create code capable of performing basic AD management operations from PowerShell scripts running in Windows XP / 2003”. Read about how it was resolved, read under habrakat (
carefully, crutches; a lot of text and code ).
Context
Recently, the work of the enikeyschik has been a little bit dismal because of the abnormal activity of system administrators and other IT managers: they decided to make a lot of changes to the settings of server services, user workstations, and other IT facilities. Naturally, with the hands of enikeyschikov, because they did not have normal tools like
Altiris or
MS SCCM .
')
At some point, I realized that I physically did not have time to implement their brilliant ideas in the networks entrusted to me, and thought about automation. The analysis of working time showed that, first of all, it is necessary to accelerate the process of replicating changes in AD. For this, as it seemed to me then, Active Directory Comandlets were needed (for which they needed to purchase at least one license for Windows 7).
Business executives and system administrators were implacable: “Why do we need a new system if the old one works fine? Ah, does it work not perfectly? But you have everything to be wonderful! Otherwise, why do you pay so much? By the way, if you need a new system so much, buy it yourself with your money and use it! And in general, if you are not able to solve business problems within the framework of the proposed toolkit, get out of the Company! ”
“Okay,” I replied: “I didn’t want to go out very much. In the end, the implementation of the “help” of the manual is one of the key differences between an enikeyschik and a real system administrator. It became clear that once again it would be necessary to "use ingenuity" and "get out." PowerShell was chosen as the platform for building the “ersatz-management
AD ” system (mainly for the ease of working with
.NET and
COM ).
Code
Getting the unique name of the unit
If you have worked with ActiveDirectory or are familiar with a different
LDAP implementation , you know how unique (distinguishable) names (DNs (
Distinguished Names )) are widely used in various cases.
A unique LDAP name consists of several relative unique names - RDNs (
Relative Distinguished Names ), which are “Attribute = Value” pairs. Example of a relative unique name for the
Organization Unit :
ou=Managers
An example of a unique name for the same unit:
ou=Managers,DC=example,dc=com
This entry indicates that in the AD domain named “example.com”, the “Managers” division is located on the first level.
From my own experience, I can say that the unique LDAP names are not intuitive and cause some difficulty for the beginner enikeyschikov. Therefore, I found it necessary to write a simple function that converts an intuitive view of the form “example.com/Managers/Accounting/” to the DN notation:
<# .SYNOPSIS OU , 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 ) { $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+ } } 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+ if ($ResOU -ne $null) { $Res = $ResOU + + $ResDC } else { $Res = $ResDC } } Return $Res }
Usage example:
$Var = Convert-ADPath2DN -Path "example.com/Test/" $Var OU=Test,DC=example,DC=com
Although this feature is not directly related to ActiveDirectory management, it is very useful and is used in other functions.
Creating an organization unit
Each enikeyshchik should know that it is necessary to test scripts for ActiveDirectory on a dedicated test domain, preferably physically disconnected from the main network of the enterprise.
Equally important is compliance with the company's IT cost reduction policy (I think that many non-core organizations have similar guidelines), which means that neither a PC capable of “pulling” a Windows 2003 virtual machine, nor, moreover, a separate server, on which you can deploy a test environment, as a rule, there is no enikeeder at its disposal.
Therefore, our hero, contrary to the strict instructions of his older comrades, is testing his achievements right in the “combat” AD. We will not blame him for it, but try instead to help.
The first thing he should do is create a separate
unit (
Organization Unit ), within which further development of script writing skills will take place. Well, since the topic of the topic is related to Powershell programming, we implement the corresponding function on it.
To do this, we need to use
the Create
constructor of the
System.DirectoryServices.DirectoryEntry class. The final version might look like this:
<# .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 " } } }
A clear disadvantage of this implementation is the inability of this function to create a full branch of departments: if you need to create an OU "
example.com/Users/HR/Women " in a domain that does not have a unit "
example.com/Users ", you cannot use her to solve this problem.
More precisely, you can, but it will be extremely inconvenient, because you will have to first create an OU "Users", then "HR" and only after that - "Women"
Such a scenario clearly contradicts the principle of automation of routine, and therefore is unacceptable. Instead, it is much better to use a function that creates the entire branch automatically, for example, the following:
<# .SYNOPSIS AD . .Description , ( ). .PARAMETER Path . .OUTPUTS $null. . #> Function New-ADOrganizationUnit ([STRING]$Path) { #, (example.com/Unit1/Unit2...) If ($Path -match ) { $i = 0 # , $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 } } }
Using a function is trivial:
# "Test" "MyFirstOU" example.com New-ADOrganizationUnit -Path "example.com/Test/MyFirstOU/" | Out-Null
Adding Security Groups
Mature sysadmins, having read serious books, recommend their Padawans to prefer
security groups to single user accounts in any AD administration scenarios and, especially, when planning its structure (if, of course, Padawan is allowed to participate in this task).
The argument is usually given the replicability of the resulting structures: if, for example, you allow some group policy to read, allowing the director to launch arbitrary software personally (more precisely, his account), then if there is a need to provide such an opportunity to someone else, then setting permissions to repeat again for another Important Uncle.
If you create a group, for example, "
gAllowRunAnything " and give permission to launch arbitrary executable files to the members of this group, the process will be much simpler: it will be enough to include the account of the second employee in this group. Obviously, this option is much easier for the enikeyschik (who will have to perform these manipulations) and is more transparent for the system administrator (who then will need to figure out what he has done with the enikeyschik).
We will not argue with senior colleagues about the importance of groups, but simply implement the possibility of creating them as a PowerShell function:
<# .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 " } } }
As you can see, here is also used one of the versions
of the Create constructor of the
System.DirectoryServices.DirectoryEntry class.
And again, the use of the function is elementary (I specifically tried to simplify the syntax so that my colleagues, such enikeyshchiki, who do not have special knowledge in the field of organization and administration of AD / LDAP, could quickly figure out):
# "gSales" "Unit1". - . New-ADGroup -Path "example.com/Test/MySuperScriptingResults/Fatal/Unit1" -Name "gSales" -Force
Creating and linking GPOs
What is ActiveDirectory ? Wikipedia
says this is an LDAP compatible directory service implementation from MS. The system administrator will probably remember about
forests, domains and trust , the Bezopasnik - about the
authentication mechanism, and the enikeyschik - about
group policies .
Why about them? They include the entire professional life of an enikeyschik:
installing software on workstations, blocking
IE settings , preventing users from installing various "* Bars",
SRPs that solve problems with games during working hours and even
blocking the taskbar that Mariwanna likes to move to the left , demanding from enikeyschiku immediately "return, as it was, but it is impossible to work." However, I was too deep in memories. I think no one will argue with the thesis about the importance of
GPO in Windows-based networks .
And since GPOs are necessary and important, it means that you need to include some mechanisms for working with them in the library being created. What should I do with the GPO in the first place? Of course, create it (obviously, no operation can precede creation):
<# .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 }
This function creates a GPO object in the object repository, and nothing more. It is useful, but for practical use it is not enough: the created object needs to be tied (
Link ) to the division. Without this, it will remain as if “inactive” (strictly speaking, it is active, the area of ​​impact is simply not defined for it). And in order to attach a policy to an OU, you need to get its object representation. It is logical to try to get it, knowing the name of the GPO, for example, like this:
<# .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 }
Having in the arsenal a search engine GPO object by name, you can try to bind:
<# .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" } }
The latter function allows you to bind not only the newly created, but also any GPO object in the repository. One of the applications of this function was the practice of quickly “entering into production” of previously created GPOs: they were first tested in a special separate unit (OU), and then
suddenly at night after testing and receiving approval from the system administrator were contacted by “combat” units. It was convenient.
A meticulous reader may ask for what purpose the
GPMC COM object is used here. Everything is very simple: .NET provides a very high level of abstraction and does not allow performing some operations. For example, I did not find a way to bind a GPO to a department, I was unable to import and export a GPO (see below) using
System.DirectoryServices.DirectoryEntry . Using GPMC to perform these actions is not only
possible , but relatively
simple .
Import and export of GPO settings
So, we have the opportunity to create group policy objects and link them to departments. Remember the goals - why do we even develop all these functions? To help enikeyschiku in his hard work. What kind of help does enikeyschik need? In the automation of routine operations that he should perform. Which of them are related to GPO? As a rule, we are talking about making changes, developed by a system administrator, into the parameters of a GPO in a variety of networks (or in different OUs of the same domain).
Usually this happens as follows: the security man decides that, for example, in order to prohibit the "removal" of information on removable media, it should be prohibited to use it. The sysadmin prepares a
description of the appropriate settings of the GPO (instruction) and instructs the enikeyschikam to propagate these changes in their networks.
There may be a lot of changes, and, even worse, networks too. As a result, it may happen that the enikeer is busy replicating all day, and cannot get the jammed sheet out of the printer, provide the necessary repertoire for an audio system in the restroom, find an abstract on the Web for the daughter-student accountant and, worst of all, show the user where there is that "any key".
And here, the almighty MS comes to the aid of our keyboard and screwdriver fighter with its own
GPO settings import / export mechanism implemented within the framework of GPMC. This mechanism allows you to import settings that are specified in one GPO to another, even if the two GPOs (donor and recipient) are in different domains. Well, Powershell allows you to bring the process to full automatism.
We will leave the procedure for exporting the reference policy to the system administrator, while we ourselves will deal with the
import . To begin with, let's look at what a GPO backup is (MS itself insists that it is
Backup , and not Export):

The folder name in this example is the unique identifier of the exported GPO. We will need it when importing, so now it would be nice to learn how to get this
GUID . I risk to pass for the famous character of network folklore, but the easiest way to get this ID is just by looking at the folder name, for example, like this:
<# .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 .
Those. , 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
Conclusion
«» , ( ) AD — , . , , .
— , AD . , « », XML- ( , , , ..).
. , ,
CM_ActiveDirectory ,
PasteBin. , ,
CM_System , (
) .
, . , 2012 , , , . , — ! , «» .
, !