PSADSync.psm1
Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' $PSAdSyncConfiguration = Import-PowerShellDataFile -Path "$PSScriptRoot\Configuration.psd1" function ConvertToSchemaAttributeType { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AttributeName, [Parameter(Mandatory)] [AllowEmptyString()] [AllowNull()] $AttributeValue, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateSet('Read','Set')] [string]$Action ) if ($AttributeValue) { switch ($AttributeName) { 'accountExpires' { if ((-not $AttributeValue) -or ($AttributeValue -eq '9223372036854775807')) { 0 } else { if ([string]$AttributeValue -as [DateTime]) { $date = ([datetime]$AttributeValue).Date } else { $date = ([datetime]::FromFileTime($AttributeValue)).Date } switch ($Action) { 'Read' { $date.AddDays(-1) } 'Set' { $date.AddDays(2) } default { throw "Unrecognized input: [$_]" } } } } default { $AttributeValue } } } else { ## If $AttributeValue is null, return an emptry string to prevent any references to the value from failing '' } } function SetAdUser { [OutputType([void])] [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Identity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$ActiveDirectoryAttributes ) $replaceHt = @{} foreach ($attrib in $ActiveDirectoryAttributes.GetEnumerator()) { $attribName = $attrib.Key $convertParams = @{ AttributeName = $attrib.Key AttributeValue = $attrib.Value Action = 'Set' } $replaceHt.$attribName = (ConvertToSchemaAttributeType @convertParams) } $setParams = @{ Identity = $Identity Replace = $replaceHt } if ($PSCmdlet.ShouldProcess("User: [$($Identity)] AD attribs: [$($replaceHt.Keys -join ',')] to [$($ActiveDirectoryAttributes.Values -join ',')]",'Set AD attributes')) { Write-Verbose -Message "Replacing AD attribs: [$($setParams.Replace | Out-String)]" Set-AdUser @setParams } } function Get-CompanyAdUser { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap ) begin { $ErrorActionPreference = 'Stop' } process { try { $userSyncProperties = [array]($FieldSyncMap.Values) @($FieldMatchMap.GetEnumerator()).foreach({ if ($_.Value -is 'scriptblock') { $userSyncProperties += ParseScriptBlockHeaders -FieldScriptBlock $_.Value | Select-Object -Unique } else { $userSyncProperties += $_.Value } }) $userIdProperties = [array]($FieldMatchMap.Values) @(Get-AdUser -Filter '*' -Properties '*').where({ $adUser = $_ ## Ensure at least one ID field is populated @($userIdProperties).where({ $adUser.($_) }) }) } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } function NewUserName { [OutputType('string')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Pattern, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMap ) if (-not (TestFieldMapIsValid -UserMatchMap $FieldMap)) { throw 'One or more values in FieldMap parameter are missing.' } switch ($Pattern) { 'FirstInitialLastName' { '{0}{1}' -f ($CsvUser.($FieldMap.FirstName)).SubString(0, 1), $CsvUser.($FieldMap.LastName) } 'FirstNameLastName' { '{0}{1}' -f $CsvUser.($FieldMap.FirstName), $CsvUser.($FieldMap.LastName) } 'FirstNameDotLastName' { '{0}.{1}' -f $CsvUser.($FieldMap.FirstName), $CsvUser.($FieldMap.LastName) } default { throw "Unrecognized UserNamePattern: [$_]" } } } function GetCsvColumnHeaders { [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath ) (Get-Content -Path $CsvFilePath | Select-Object -First 1).Split(',') -replace '"' } # .ExternalHelp PSADSync-Help.xml function Get-AvailableAdUserAttribute { 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-AvailableAdUserAttribute).ValidName) { $true } else { $false } } function TestCsvHeaderExists { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath, [Parameter()] [ValidateNotNullOrEmpty()] [object[]]$Header, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$ParseScriptBlockHeaders ) $csvHeaders = GetCsvColumnHeaders -CsvFilePath $CsvFilePath ## Parse out the CSV headers used if the field is a scriptblock $commonHeaders = @($Header).foreach({ $_ | ForEach-Object { if ($_ -is 'scriptblock') { ## It's extremely hard to figure out what values inside of the scriptblock are actual CSV headers ## Give the option here. if ($ParseScriptBlockHeaders.IsPresent) { ParseScriptBlockHeaders -FieldScriptBlock $_ } } else { $_ } } }) ## Assuming that ParseScriptBlockHeaders was not used and all of the headers ## are scriptblocks. We check nothing but still return true. if (-not ($commonHeaders = $commonHeaders | Select-Object -Unique)) { $true } else { $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, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('Comma','Tab')] [string]$Delimiter = 'Comma' ) 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 ') } $importCsvParams = @{ Path = $CsvFilePath } if ($Delimiter -eq 'Comma') { $importCsvParams.Delimiter = ',' } elseif ($Delimiter -eq 'Tab') { $importCsvParams.Delimiter = "`t" } Import-Csv @importCsvParams | Where-Object -FilterScript $whereFilter } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } function New-CompanyAdUser { [OutputType([void])] [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser, [Parameter(Mandatory,ParameterSetName = 'Password')] [ValidateNotNullOrEmpty()] [securestring]$Password, [Parameter(Mandatory,ParameterSetName = 'RandomPassword')] [ValidateNotNullOrEmpty()] [switch]$RandomPassword, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$UserMatchMap, [Parameter()] [ValidateNotNullOrEmpty()] [ValidateSet('FirstInitialLastName','FirstNameLastName','FirstNameDotLastName')] [string]$UsernamePattern = $PSAdSyncConfiguration.NewUserCreation.AccountNamePattern ) $userName = NewUserName -CsvUser $CsvUser -Pattern $UsernamePattern -FieldMap $UserMatchMap $newAdUserParams = @{ Name = $userName PassThru = $true GivenName = $CsvUser.($UserMatchMap.FirstName) Surname = $CsvUser.($UserMatchMap.LastName) } if ($RandomPassword.IsPresent) { $pw = NewRandomPassword } else { $Password = $pw } $otherAttribs = @{} $FieldSyncMap.GetEnumerator().foreach({ $adAttribName = $_.Value $adAttribValue = $CsvUser.($_.Key) $otherAttribs.$adAttribName = $adAttribValue }) $FieldMatchMap.GetEnumerator().foreach({ $adAttribName = $_.Value $adAttribValue = $CsvUser.($_.Key) $otherAttribs.$adAttribName = $adAttribValue }) $newAdUserParams.OtherAttributes = $otherAttribs if ($PSCmdlet.ShouldProcess("User: [$($userName)] AD attribs: [$($newAdUserParams | Out-String)]",'New AD User')) { if (Get-AdUser -Filter "samAccountName -eq '$userName'") { throw "The user to be created [$($userName)] already exists." } else { if ($newUser = New-ADUser @newAdUserParams) { Set-ADAccountPassword -Identity $newUser.DistinguishedName -Reset -NewPassword $pw } } } } function TestFieldMapIsValid { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory,ParameterSetName = 'Sync')] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory,ParameterSetName = 'Match')] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory,ParameterSetName = 'Value')] [ValidateNotNullOrEmpty()] [hashtable]$FieldValueMap, [Parameter(Mandatory,ParameterSetName = 'UserMatch')] [ValidateNotNullOrEmpty()] [hashtable]$UserMatchMap, [Parameter()] [ValidateNotNullOrEmpty()] [string]$CsvFilePath ) <# FieldSyncMap -------------- Valid: @{ <scriptblock>; <string> } @{ { if ($_.'NICK_NAME') { 'NICK_NAME' } else { 'FIRST_NAME' }} = 'givenName' } @{ <string>; <string> } FieldMatchMap -------------- Valid: @{ <scriptblock>; <string> } @{ <array>; <array> } @{ { if ($_.'csvIdField2') { $_.'csvIdField2' } else { $_.'csvIdField3'} } = 'adIdField2' } @{ <string>; <string> } FieldValueMap -------------- Valid: @{ <string>; <scriptblock> } @{ 'SUPERVISOR' = { $supId = $_.'SUPERVISOR_ID'; (Get-AdUser -Filter "EmployeeId -eq '$supId'").DistinguishedName }} #> if (-not $PSBoundParameters.ContainsKey('CsvFilePath') -and -not $UserMatchMap) { throw 'CSVFilePath is required when testing any map other than UserMatchMap.' } $result = $true switch ($PSCmdlet.ParameterSetName) { 'Sync' { $mapHt = $FieldSyncMap.Clone() if ($FieldSyncMap.GetEnumerator().where({ $_.Value -is 'scriptblock' })) { Write-Warning -Message 'Scriptblocks are not allowed as a value in FieldSyncMap.' $result = $false } } 'Match' { $mapHt = $FieldMatchMap.Clone() if ($FieldMatchMap.GetEnumerator().where({ $_.Value -is 'scriptblock' })) { Write-Warning -Message 'Scriptblocks are not allowed as a value in FieldMatchMap.' $result = $false } elseif ($FieldMatchMap.GetEnumerator().where({ @($_.Key).Count -gt 1 -and @($_.Value).Count -eq 1 })) { $result = $false } } 'Value' { $mapHt = $FieldValueMap.Clone() if ($FieldValueMap.GetEnumerator().where({ $_.Value -isnot 'scriptblock' })) { Write-Warning -Message 'A scriptblock must be a value in FieldValueMap.' $result = $false } } 'UserMatch' { $mapHt = $UserMatchMap.Clone() if (($UserMatchMap.Keys | Where-Object { $_ -in @('FirstName','LastName') }).Count -ne 2) { $result = $false } } default { throw "Unrecognized input: [$_]" } } if ($result -and (-not $UserMatchMap)) { if (-not (TestCsvHeaderExists -CsvFilePath $CsvFilePath -Header ([array]($mapHt.Keys)))) { Write-Warning -Message 'CSV header check failed.' $false } else { $true } } else { $result } } function FindUserMatch { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser, [Parameter()] [object[]]$AdUsers = $script:adUsers ) $ErrorActionPreference = 'Stop' <# Possibilities $FieldMatchMap = @{ @( { if ($_.'NICK_NAME') { 'NICK_NAME' } else { $_.'FIRST_NAME' }}, 'LAST_NAME' ) @( 'givenName','surName' ) } @($AdUsers).where({ $_.givenName -eq 'nick' -and $_.surName -eq 'last' }) $FieldMatchMap = @{ @( 'FIRST_NAME', 'LAST_NAME' ) @( 'givenName', 'surName' ) } @($AdUsers).where({ $_.givenName -eq 'first' -and $_.surName -eq 'last' }) $CsvUser = [pscustomobject]@{ NICK_NAME = 'nick' FIRST_NAME = 'first' LAST_NAME = 'last' } #> $whereFilterElements = @() [string[]]$fieldVals = $FieldMatchmap.Values | Select-Object $fieldKeys = @() $i = 0 $FieldMatchMap.Keys.foreach({ ## @( { if ($_.'NICK_NAME') { 'NICK_NAME' } else { 'FIRST_NAME'} },'LAST_NAME') foreach ($k in $_) { if ($k -is 'scriptblock') { ## { if ($_.'NICK_NAME') { 'NICK_NAME' } else { 'FIRST_NAME'} } ## 'NICK_NAME' $csvProp = EvaluateCsvFieldCondition -Condition $k -CsvUser $CsvUser } else { $csvProp = $k } $fieldKeys += $csvProp ## 'Joel' if ($value = $CsvUser.$csvProp) { $adProp = $fieldVals[$i] $whereFilterElements += '$_.{0} -eq "{1}"' -f $adProp,$value } $i++ } }) if (@($FieldMatchMap.Keys).Count -gt 1) { $whereFilter = [scriptblock]::Create($whereFilterElements -join ' -or ') } else { $whereFilter = [scriptblock]::Create($whereFilterElements -join ' -and ') } if ($adUserMatch = @($AdUsers).where($whereFilter)) { if (@($adUserMatch).Count -gt 1) { Write-Warning -Message 'More than one AD user found to match found. Skipping user...' } else { [pscustomobject]@{ MatchedAdUser = $adUserMatch CSVAttemptedMatchIds = ($fieldKeys -join ',') ADAttemptedMatchIds = ($fieldVals -join ',') } } } else { Write-Verbose -Message 'No user match found for CSV user' } } function EvaluateCsvFieldCondition { [OutputType('string')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [scriptblock]$Condition, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser ) $csvFieldScript = $Condition.ToString() -replace '\$_','$CsvUser' & ([scriptblock]::Create($csvFieldScript)) } function FindAttributeMismatch { [OutputType([hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] $AdUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser ) $ErrorActionPreference = 'Stop' Write-Verbose -Message "Starting AD attribute mismatch check..." $FieldSyncMap.GetEnumerator().foreach({ if ($_.Key -is 'scriptblock') { $csvFieldName = EvaluateCsvFieldCondition -Condition $_.Key -CsvUser $CsvUser } else { $csvFieldName = $_.Key } $adAttribName = $_.Value $adAttribValue = $AdUser.$adAttribName $csvAttribValue = $CsvUser.$csvFieldName ## Do not return mismatches if either the CSV value or the field is null. The field can be null either when ## the actual CSV field is null in the file or the expression evaluates to null. if ($csvAttribValue -and $csvFieldName) { Write-Verbose -Message "Checking CSV field [$($csvFieldName)] / AD field [$($adAttribName)] for mismatches..." $adConvertParams = @{ AttributeName = $adAttribName AttributeValue = $adAttribValue Action = 'Read' } $adAttribValue = ConvertToSchemaAttributeType @adConvertParams Write-Verbose -Message "Comparing AD attribute value [$($adattribValue)] with CSV value [$($csvAttribValue)]..." ## Compare the two property values and return the AD attribute name and value to be synced if ($adattribValue -ne $csvAttribValue) { @{ ActiveDirectoryAttribute = @{ $adAttribName = $adattribValue } CSVField = @{ $csvFieldName = $csvAttribValue } ADShouldBe = @{ $adAttribName = $csvAttribValue } } Write-Verbose -Message "AD attribute mismatch found on AD attribute: [$($adAttribName)]." } } }) } function NewRandomPassword { [CmdletBinding()] [OutputType([System.Security.SecureString])] param ( [Parameter()] [ValidateRange(8, 64)] [int]$Length = (Get-Random -Minimum 20 -Maximum 32), [Parameter()] [ValidateRange(0, 8)] [int]$Complexity = 3 ) $ErrorActionPreference = 'Stop' Add-Type -AssemblyName 'System.Web' # Generate a password with the specified length and complexity. Write-Verbose ('Generating password {0} characters in length and with a complexity of {1}.' -f $Length, $Complexity); $pw = [System.Web.Security.Membership]::GeneratePassword($Length, $Complexity) ConvertTo-SecureString -String $pw -AsPlainText -Force } function InvokeUserTermination { [OutputType('void')] [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser ) switch ($PSAdSyncConfiguration.UserTermination.Action) { 'Disable' { if ($PSCmdlet.ShouldProcess("AD User [$($AdUser.Name)]",'Disable')) { Disable-AdAccount -Identity $AdUser.samAccountName -Confirm:$false } } default { throw "Unrecognized user termination action: [$_]" } } } function TestUserTerminated { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser ) if (-not (TestIsUserTerminationEnabled)) { throw 'User termination checking is not enabled in the configuration' } else { $csvField = $PSAdSyncConfiguration.UserTermination.CsvField $csvValue = $PSAdSyncConfiguration.UserTermination.CsvValue if ($CsvUser.$csvField -eq $csvValue) { $true } else { $false } } } function TestIsUserTerminationEnabled { [OutputType('bool')] [CmdletBinding()] param () if ($PSAdSyncConfiguration.UserTermination.Enabled) { $true } else { $false } } function TestIsUserCreationEnabled { [OutputType('bool')] [CmdletBinding()] param () if ($PSAdSyncConfiguration.UserCreation.Enabled) { $true } else { $false } } function SyncCompanyUser { [OutputType()] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Identity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable[]]$ActiveDirectoryAttributes ) $ErrorActionPreference = 'Stop' try { foreach ($ht in $ActiveDirectoryAttributes) { SetAdUser -Identity $Identity -ActiveDirectoryAttributes $ht } } 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 -Confirm:$false } 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 GetManagerEmailAddress { [OutputType('string')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser ) $ErrorActionPreference = 'Stop' if ($AdUser.Manager -and ($managerAdAccount = Get-ADUser -Filter "DistinguishedName -eq '$($AdUser.Manager)'" -Properties EmailAddress)) { $managerAdAccount.EmailAddress } } function SendStaleAccountEmail { [OutputType([void])] [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Subject = $PSAdSyncConfiguration.Email.Templates.UnusedAccount.Subject, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FromEmailAddress = $PSAdSyncConfiguration.Email.Templates.UnusedAccount.FromEmailAddress, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FromEmailName = $PSAdSyncConfiguration.Email.Templates.UnusedAccount.FromEmailName, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SmtpServer = $PSAdSyncConfiguration.Email.SmtpServer ) begin { $ErrorActionPreference = 'Stop' } process { try { if (-not $AdUser.Manager) { throw "No manager defined for user: [$($AdUser.name)]. Cannot send email." } if (-not ($managerEmail = GetManagerEmailAddress -AdUser $AdUser)) { throw "Could not find a manager email address for user [$($AdUser.Name)]" } $emailBody = ReadEmailTemplate -Name UnusedSccount $emailBody = $emailBody -f $managerEmail,$AdUser.Name,$PSAdSyncConfiguration.CompanyName $sendParams = @{ To = $managerEmail From = "$FromEmailName <$FromEmailAddress>" Subject = $Subject Body = $emailBody SmtpServer = $SmtpServer } if ($PSCmdlet.ShouldProcess($managerEmail,"Send email about account [$($AdUser.Name)]")) { Send-MailMessage @sendParams } } catch { $PSCmdlet.ThrowTerminatingError($_) } } } function ReadEmailTemplate { [OutputType('string')] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name ) if ($template = Get-ChildItem -Path "$PSScriptRoot\EmailTemplates" -Filter "$Name.txt") { Get-Content -Path $template.FullName -Raw } } function WriteProgressHelper { 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) } # .ExternalHelp PSADSync-Help.xml function Invoke-AdSync { [OutputType([void])] [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CsvFilePath, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter()] [ValidateNotNullOrEmpty()] [hashtable]$FieldValueMap, [Parameter(Mandatory,ParameterSetName = 'CreateNewUsers')] [ValidateNotNullOrEmpty()] [switch]$CreateNewUsers, [Parameter(Mandatory,ParameterSetName = 'CreateNewUsers')] [ValidateNotNullOrEmpty()] [hashtable]$UserMatchMap, [Parameter(Mandatory,ParameterSetName = 'CreateNewUsers')] [ValidateNotNullOrEmpty()] [string]$UsernamePattern, [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 (TestFieldMapIsValid -FieldSyncMap $FieldSyncMap -CsvFilePath $CsvFilePath)) { throw 'Invalid attribute found in FieldSyncMap.' } if (-not (TestFieldMapIsValid -FieldMatchMap $FieldMatchMap -CsvFilePath $CsvFilePath)) { throw 'Invalid attribute found in FieldMatchMap.' } if ($PSBoundParameters.ContainsKey('FieldValueMap')) { if (-not (TestFieldMapIsValid -FieldValueMap $FieldValueMap -CsvFilePath $CsvFilePath)) { throw 'Invalid attribute found in FieldValueMap.' } } $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-AvailableAdUserAttribute for a list of available attributes.' } }) Write-Output '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 -FieldSyncMap $FieldSyncMap)) { throw 'No AD users found' } Write-Output 'Enumerating all CSV users...' if (-not ($csvusers = Get-CompanyCsvUser @getCsvParams)) { throw 'No CSV users found' } $script:totalSteps = @($csvusers).Count $stepCounter = 0 $rowsProcessed = 1 @($csvUsers).foreach({ try { 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)" } WriteProgressHelper -Message $prgMsg -StepNumber ($stepCounter++) $csvUser = $_ if ($adUserMatch = FindUserMatch -CsvUser $csvUser -FieldMatchMap $FieldMatchMap) { Write-Verbose -Message 'Match' $CSVAttemptedMatchIds = $aduserMatch.CSVAttemptedMatchIds $csvIdValue = ($CSVAttemptedMatchIds | % {$csvUser.$_}) -join ',' $csvIdField = $CSVAttemptedMatchIds -join ',' #region FieldValueMap check if ($PSBoundParameters.ContainsKey('FieldValueMap')) { $selectParams = @{ Property = @('*') Exclude = [array]($FieldValueMap.Keys) } @($FieldValueMap.GetEnumerator()).foreach({ $selectParams.Property += @{ Name = $_.Key Expression = $_.Value } }) $csvUser = $csvUser | Select-Object @selectParams } #endregion ## User termination check if ((TestIsUserTerminationEnabled) -and (TestUserTerminated -CsvUser $csvUser)) { if (-not $ReportOnly.IsPresent) { InvokeUserTermination -AdUser $adUserMatch.MatchedAduser } $logAttribs = @{ CSVAttributeName = 'UserTermination' CSVAttributeValue = 'UserTermination' ADAttributeName = 'UserTermination' ADAttributeValue = 'UserTermination' Message = $_.Exception.Message } } else { $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] Message = $null } if (-not $ReportOnly.IsPresent) { $syncParams = @{ CsvUser = $csvUser ActiveDirectoryAttributes = $attribMismatches.ADShouldBe Identity = $adUserMatch.MatchedAduser.samAccountName } Write-Verbose -Message "Running SyncCompanyUser with params: [$($syncParams | Out-String)]" SyncCompanyUser @syncParams } } elseif ($attribMismatches -eq $false) { throw 'Error occurred in FindAttributeMismatch' } 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' Message = $null } } } } else { ## No user match was found if (-not ($csvIds = @(GetCsvIdField -CsvUser $csvUser -FieldMatchMap $FieldMatchMap).where({ $_.Field }))) { Write-Warning -Message 'No CSV ID fields were found.' $csvIdField = "CSV Row: $rowsProcessed" $csvIdValue = "CSV Row: $rowsProcessed" $logAttribs = @{ CSVAttributeName = "CSV Row: $rowsProcessed" CSVAttributeValue = "CSV Row: $rowsProcessed" ADAttributeName = 'NoMatch' ADAttributeValue = 'NoMatch' Message = $null } } elseif ($CreateNewUsers.IsPresent) { $csvIdField = $csvIds.Field -join ',' if (-not $ReportOnly.IsPresent) { $newUserParams = @{ CsvUser = $csvUser UsernamePattern = $UsernamePattern UserMatchMap = $UserMatchMap RandomPassword = $true FieldSyncMap = $FieldSyncMap FieldMatchMap = $FieldMatchMap } New-CompanyAdUser @newUserParams } $logAttribs = @{ CSVAttributeName = 'NewUserCreated' CSVAttributeValue = 'NewUserCreated' ADAttributeName = 'NewUserCreated' ADAttributeValue = 'NewUserCreated' Message = $null } $csvIdValue = ($csvIds | foreach { $csvUser.($_.Field) }) } else { $csvIdField = $csvIds.Field -join ',' $csvIdValue = "CSV Row: $rowsProcessed" $logAttribs = @{ CSVAttributeName = "CSV Row: $rowsProcessed" CSVAttributeValue = "CSV Row: $rowsProcessed" ADAttributeName = 'NoMatch' ADAttributeValue = 'NoMatch' Message = $null } } } } catch { $logAttribs = @{ CSVAttributeName = 'Error' CSVAttributeValue = 'Error' ADAttributeName = 'Error' ADAttributeValue = 'Error' Message = $_.Exception.Message } } finally { WriteLog -CsvIdentifierField $csvIdField -CsvIdentifierValue $csvIdValue -Attributes $logAttribs } $rowsProcessed++ }) } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } |