PSADSync.psm1
Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' function FindAdUser { [OutputType([System.DirectoryServices.SearchResult])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.DirectoryServices.DirectorySearcher]$DirectorySearcher ) $DirectorySearcher.FindAll() } function GetCurrentDomainName { [OutputType('string')] [CmdletBinding()] param () [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name } function GetAdUser { [CmdletBinding()] param ( [OutputType('System.DirectoryServices.AccountManagement.UserPrincipal','System.DirectoryServices.SearchResult')] [ValidateNotNullOrEmpty()] [hashtable]$Identity, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('SearchResult','UserPrincipal')] [string]$OutputAs = 'UserPrincipal' ) $domainDn = $(([adsisearcher]"").Searchroot.path) $DirectorySearcher = New-Object -TypeName System.DirectoryServices.DirectorySearcher $DirectorySearcher.PageSize = 1000 $DirectorySearcher.SearchRoot = $domainDN if ($PSBoundParameters.ContainsKey('Identity')) { $idField = ([array]$Identity.Keys)[0] $idValue = ([array]$Identity.Values)[0] $DirectorySearcher.Filter = "(&(objectCategory=person)(objectClass=User)({0}={1}))" -f $idField,$idValue } $result = FindAdUser -DirectorySearcher $DirectorySearcher if ($OutputAs -eq 'SearchResult') { $result } else { $Context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain', (GetCurrentDomainName)) @($result).foreach({ foreach ($user in ([System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($Context, ($_.path -replace 'LDAP://')))) { $user.GetUnderlyingObject().Properties } }) } } function PutAdUser { [OutputType([void])] [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [System.DirectoryServices.DirectoryEntry]$AdsUser, [Parameter()] [ValidateNotNullOrEmpty()] [string]$AttributeName, [Parameter()] [ValidateNotNullOrEmpty()] [object]$AttributeValue ) $AdsUser.Put($AttributeName,$AttributeValue) $AdsUser.SetInfo() } function SaveAdUser { [OutputType([void])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [System.DirectoryServices.DirectoryEntry]$AdsUser, [Parameter()] [ValidateNotNullOrEmpty()] [string]$AttributeName, [Parameter()] [ValidateNotNullOrEmpty()] [object]$AttributeValue ) if ([string]$AttributeValue -as [DateTime]) { $AttributeValue = [datetime]$AttributeValue } PutAdUser -AdsUser $AdsUser -AttributeName $AttributeName -AttributeValue $AttributeValue } function ConvertToSchemaAttribute { [OutputType('string')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Attribute ) switch ($Attribute) { 'accountExpires' { 'AccountExpirationDate' } default { $_ } } } function SetAdUser { [OutputType([void])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$Identity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$ActiveDirectoryAttributes ) $srcResultUser = GetAdUser -Identity $Identity -OutputAs SearchResult $adspath = $srcResultUser.Properties.adspath -as [string] $AdsUser = $adspath -as [adsi] foreach ($attrib in $ActiveDirectoryAttributes.GetEnumerator()) { $saveAdParams = @{ AdsUser = $AdsUser AttributeName = (ConvertToSchemaAttribute -Attribute $attrib.Key) AttributeValue = $attrib.Value } Write-Verbose -Message "Running SaveAdUser with params: [$($saveAdParams | Out-String)]" SaveAdUser @saveAdParams } } function Get-CompanyAdUser { [OutputType([System.DirectoryServices.AccountManagement.UserPrincipal])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap ) begin { $ErrorActionPreference = 'Stop' Write-Verbose -Message "Finding all AD users in domain with properties: $($FieldMatchMap.Values -join ',')" } process { try { $whereFilter = { $adUser = $_; $FieldMatchMap.Values | Where-Object { $adUser.$_ }} @(GetAdUser).where({$whereFilter}) } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } function GetCsvColumnHeaders { [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath ) (Get-Content -Path $CsvFilePath | Select-Object -First 1).Split(',') -replace '"' } function Get-AvailableAdUserAttributes { param() $schema =[DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema() $userClass = $schema.FindClass('user') foreach ($name in $userClass.GetAllProperties().Name | Sort-Object) { $output = [ordered]@{ ValidName = $name CommonName = $null } switch ($name) { 'sn' { $output.CommonName = 'SurName' } } [pscustomobject]$output } } function TestIsValidAdAttribute { [OutputType('bool')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name ) if ($Name -in (Get-AvailableAdUserAttributes).ValidName) { $true } else { $false } } function TestCsvHeaderExists { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath, [Parameter()] [ValidateNotNullOrEmpty()] [object[]]$Header ) $csvHeaders = GetCsvColumnHeaders -CsvFilePath $CsvFilePath ## Parse out the CSV headers used if the field is a scriptblock $commonHeaders = @($Header).foreach({ if ($_ -is 'scriptblock') { ParseScriptBlockHeaders -FieldScriptBlock $_ } else { $_ } }) $commonHeaders = $commonHeaders | Select-Object -Unique $matchedHeaders = $csvHeaders | Where-Object { $_ -in $commonHeaders } if (@($matchedHeaders).Count -ne @($commonHeaders).Count) { $false } else { $true } } function ParseScriptBlockHeaders { [OutputType('$')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [scriptblock[]]$FieldScriptBlock ) $headers = @($FieldScriptBlock).foreach({ $ast = [System.Management.Automation.Language.Parser]::ParseInput($_.ToString(),[ref]$null,[ref]$null) $ast.FindAll({$args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]},$true).Value }) $headers | Select-Object -Unique } function Get-CompanyCsvUser { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({Test-Path -Path $_ -PathType Leaf})] [string]$CsvFilePath, [Parameter()] [ValidateNotNullOrEmpty()] [hashtable]$Exclude ) begin { $ErrorActionPreference = 'Stop' Write-Verbose -Message "Enumerating all users in CSV file [$($CsvFilePath)]" } process { try { $whereFilter = { '*' } if ($PSBoundParameters.ContainsKey('Exclude')) { $conditions = $Exclude.GetEnumerator() | ForEach-Object { "(`$_.'$($_.Key)' -ne '$($_.Value)')" } $whereFilter = [scriptblock]::Create($conditions -join ' -and ') } Import-Csv -Path $CsvFilePath | Where-Object -FilterScript $whereFilter } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } function FindUserMatch { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser, [Parameter()] [object[]]$AdUsers = $script:adUsers ) $ErrorActionPreference = 'Stop' foreach ($matchId in $FieldMatchMap.GetEnumerator()) { ## FieldMatchMap = @{ 'AD_LOGON' = 'samAccountName' } $adMatchField = $matchId.Value $csvMatchField = $matchId.Key Write-Verbose "Match fields: CSV - [$($csvMatchField)], AD - [$($adMatchField)]" if ($csvMatchVal = $CsvUser.$csvMatchField) { Write-Verbose -Message "CsvFieldMatchValue is [$($csvMatchVal)]" if ($matchedAdUser = @($AdUsers).where({ $_.$adMatchField -eq $csvMatchVal })) { Write-Verbose -Message "Found AD match for CSV user [$csvMatchVal]: [$($matchedAdUser.$adMatchField)]" [pscustomobject]@{ MatchedAdUser = $matchedAdUser CsvIdMatchedOn = $csvMatchField AdIdMatchedOn = $adMatchField } ## Stop after making a single match break } else { Write-Verbose -Message "No user match found for CSV user [$csvMatchVal]" } } else { Write-Verbose -Message "CSV field match value [$($csvMatchField)] could not be found." } } } function FindAttributeMismatch { [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser ) $ErrorActionPreference = 'Stop' $FieldSyncMap.GetEnumerator().foreach({ if ($_.Key -is 'scriptblock') { ## Replace $_ with $CsvUser $csvFieldScript = $_.Key.ToString() -replace '$_','$CsvUser' $csvFieldName = & ([scriptblock]::Create($csvFieldScript)) } else { $csvFieldName = $_.Key } $adAttribName = $_.Value ## Remove the null fields if (-not $AdUser.$adAttribName) { $AdUser | Add-Member -MemberType NoteProperty -Name $adAttribName -Force -Value '' } if (-not $CsvUser.$csvFieldName) { $CsvUser.$csvFieldName = '' } ## Compare the two property values and return the AD attribute name and value to be synced if ($AdUser.$adAttribName -ne $CsvUser.$csvFieldName) { @{ ActiveDirectoryAttribute = @{ $adAttribName = $AdUser.$adAttribName } CSVField = @{ $csvFieldName = $CsvUser.$csvFieldName } ADShouldBe = @{ $adAttribName = $CsvUser.$csvFieldName } } Write-Verbose -Message "AD attribute mismatch found on AD attribute: [$($adAttribName)]. Value is [$($AdUser.$adAttribName)] and should be [$($CsvUser.$csvFieldName)]" } }) } function SyncCompanyUser { [OutputType()] [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable[]]$ActiveDirectoryAttributes, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Identifier ) $ErrorActionPreference = 'Stop' try { $setParams = @{ Identity = @{ $Identifier = [string]($AdUser.$Identifier) } } foreach ($ht in $ActiveDirectoryAttributes) { $setParams.ActiveDirectoryAttributes = $ht if ($PSCmdlet.ShouldProcess("User: [$($AdUser.$Identifier)] AD attribs: [$($ht.Keys -join ',')]",'Set AD attributes')) { Write-Verbose -Message "Running SetAdUser with params: [$($setParams | Out-String)]" SetAdUser @setParams } } } catch { $PSCmdlet.ThrowTerminatingError($_) } } function WriteLog { [OutputType([void])] [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$FilePath = '.\PSAdSync.csv', [Parameter()] [ValidateNotNullOrEmpty()] [string]$CsvIdentifierField, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvIdentifierValue, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$Attributes ) $ErrorActionPreference = 'Stop' $time = Get-Date -Format 'g' $Attributes['CsvIdentifierValue'] = $CsvIdentifierValue $Attributes['CsvIdentifierField'] = $CsvIdentifierField $Attributes['Time'] = $time ([pscustomobject]$Attributes) | Export-Csv -Path $FilePath -Append -NoTypeInformation } function GetCsvIdField { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap ) $FieldMatchMap.Keys | ForEach-Object { [pscustomobject]@{ Field = $_ Value = $CSVUser.$_ } } } function Write-ProgressHelper { param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [int]$StepNumber, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Message ) Write-Progress -Activity 'Active Directory Report/Sync' -Status $Message -PercentComplete (($StepNumber / $script:totalSteps) * 100) } function Invoke-AdSync { [OutputType()] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$ReportOnly, [Parameter()] [ValidateNotNullOrEmpty()] [hashtable]$Exclude ) begin { $ErrorActionPreference = 'Stop' } process { try { $getCsvParams = @{ CsvFilePath = $CsvFilePath } if ($PSBoundParameters.ContainsKey('Exclude')) { if (-not (TestCsvHeaderExists -CsvFilePath $CsvFilePath -Header ([array]$Exclude.Keys))) { throw 'One or more CSV headers excluded with -Exclude do not exist in the CSV file.' } $getCsvParams.Exclude = $Exclude } if (-not (TestCsvHeaderExists -CsvFilePath $CsvFilePath -Header ([array]$FieldMatchMap.Keys))) { throw 'One or more CSV headers in FieldMatchMap do not exist in the CSV file.' } $FieldSyncMap.GetEnumerator().where({$_.Value -is 'string'}).foreach({ if (-not (TestIsValidAdAttribute -Name $_.Value)) { throw 'One or more AD attributes in FieldSyncMap do not exist. Use Get-AvailableAdUserAttributes for a list of available attributes.' } }) Write-Host 'Enumerating all Active Directory users. This may take a few minutes depending on the number of users...' if (-not ($script:adUsers = Get-CompanyAdUser -FieldMatchMap $FieldMatchMap)) { throw 'No AD users found' } Write-Host 'Active Directory user enumeration complete.' Write-Host 'Enumerating all CSV users...' if (-not ($csvusers = Get-CompanyCsvUser @getCsvParams)) { throw 'No CSV users found' } Write-Host 'CSV user enumeration complete.' $script:totalSteps = @($csvusers).Count $stepCounter = 0 @($csvUsers).foreach({ if ($ReportOnly.IsPresent) { $prgMsg = "Attempting to find attribute mismatch for user in CSV row [$($stepCounter + 1)]" } else { $prgMsg = "Attempting to find and sync AD any attribute mismatches for user in CSV row [$($stepCounter + 1)]" } Write-ProgressHelper -Message $prgMsg -StepNumber ($stepCounter++) $csvUser = $_ if ($adUserMatch = FindUserMatch -CsvUser $csvUser -FieldMatchMap $FieldMatchMap) { Write-Verbose -Message 'Match' $csvIdMatchedon = $aduserMatch.CsvIdMatchedOn $adIdMatchedon = $aduserMatch.AdIdMatchedOn $csvIdValue = $csvUser.$csvIdMatchedon $csvIdField = $csvIdMatchedon $findParams = @{ AdUser = $adUserMatch.MatchedAdUser CsvUser = $csvUser FieldSyncMap = $FieldSyncMap } $attribMismatches = FindAttributeMismatch @findParams if ($attribMismatches) { $logAttribs = @{ CSVAttributeName = ([array]($attribMismatches.CSVField.Keys))[0] CSVAttributeValue = ([array]($attribMismatches.CSVField.Values))[0] ADAttributeName = ([array]($attribMismatches.ActiveDirectoryAttribute.Keys))[0] ADAttributeValue = ([array]($attribMismatches.ActiveDirectoryAttribute.Values))[0] } if (-not $ReportOnly.IsPresent) { $syncParams = @{ AdUser = $adUserMatch.MatchedADUser CsvUser = $csvUser ActiveDirectoryAttributes = $attribMismatches.ADShouldBe Identifier = $adIdMatchedOn } Write-Verbose -Message "Running SyncCompanyUser with params: [$($syncParams | Out-String)]" SyncCompanyUser @syncParams } } else { Write-Verbose -Message "No attributes found to be mismatched between CSV and AD user account for user [$csvIdValue]" $logAttribs = @{ CSVAttributeName = 'AlreadyInSync' CSVAttributeValue = 'AlreadyInSync' ADAttributeName = 'AlreadyInSync' ADAttributeValue = 'AlreadyInSync' } } } else { if (-not ($csvIds = @(GetCsvIdField -CsvUser $csvUser -FieldMatchMap $FieldMatchMap).where({ $_.Field }))) { throw 'No CSV id fields were found.' } $csvIdField = $csvIds.Field -join ',' ## No ID fields are populated if (-not ($csvIds | Where-Object {$_.Value})) { $csvIdValue = 'N/A' Write-Verbose -Message 'No CSV user identifier could be found' } elseif ($csvIds | Where-Object { $_.Value}) { ## at least one ID field is populated $csvIdValue = $csvIds.Value -join ',' } $logAttribs = @{ CSVAttributeName = 'NoMatch' CSVAttributeValue = 'NoMatch' ADAttributeName = 'NoMatch' ADAttributeValue = 'NoMatch' } } WriteLog -CsvIdentifierField $csvIdField -CsvIdentifierValue $csvIdValue -Attributes $logAttribs }) } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } |