PSADSync.psm1
Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' function GetPsAdSyncConfiguration { [OutputType('hashtable')] [CmdletBinding()] param () 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: [$_]" } } } } 'countryCode' { ## Load once only if (-not (Get-Variable -Name 'countryCodes' -Scope Script -ErrorAction Ignore)) { $script:countryCodes = Get-AvailableCountryCodes } ## ie. match on United States or just US if (-not ($code = @($script:countryCodes).where({ $_.activeDirectoryName -eq $AttributeValue -or $_.alpha2 -eq $AttributeValue}))) { throw "Country code for name [$($AttributeValue)] could not be found." } $code.Numeric } default { ## Remove any special characters $AttributeValue } } } else { ## If $AttributeValue is null, return an emptry string to prevent any references to the value from failing '' } } function CleanAdAccountName { [OutputType('string')] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$AccountName ) $AccountName -replace "'" } 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 Confirm = $false } 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 'Enabled -eq $true' -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) } 'LastNameFirstTwoFirstNameChars' { '{0}{1}' -f $CsvUser.($FieldMap.LastName), ($CsvUser.($FieldMap.FirstName)).SubString(0, 2) } 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 } } # .ExternalHelp PSADSync-Help.xml function Get-AvailableCountryCodes { [OutputType('pscustomobject')] [CmdletBinding()] param () $ErrorActionPreference = 'Stop' $countryCodes = Import-PowerShellDataFile -Path "$PSScriptRoot\CountryCodeMap.psd1" $countryCodes.Countries } 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([Microsoft.ActiveDirectory.Management.ADUser])] [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()] [ValidateNotNullOrEmpty()] [string]$Path = (GetPsAdSyncConfiguration).NewUserCreation.Path, [Parameter()] [ValidateNotNullOrEmpty()] [hashtable]$FieldValueMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldSyncMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$FieldMatchMap, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [hashtable]$UserMatchMap, [Parameter()] [ValidateNotNullOrEmpty()] [string]$UsernamePattern = (GetPsAdSyncConfiguration).NewUserCreation.AccountNamePattern ) $userName = CleanAdAccountName(NewUserName -CsvUser $CsvUser -Pattern $UsernamePattern -FieldMap $UserMatchMap) $firstName = $CsvUser.($UserMatchMap.FirstName) $lastName = $CsvUser.($UserMatchMap.LastName) $newAdUserParams = @{ Name = $userName samAccountName = $userName DisplayName = "$firstName $lastName" PassThru = $true GivenName = $firstName Surname = $lastName Enabled = $true Path = $Path } if ($RandomPassword.IsPresent) { $pw = NewRandomPassword } else { $pw = $Password } $secPw = ConvertTo-SecureString -String $pw -AsPlainText -Force $otherAttribs = @{} $FieldSyncMap.GetEnumerator().foreach({ if ($_.Value -is 'string') { $adAttribName = $_.Value } else { $adAttribName = EvaluateFieldCondition -Condition $_.Value -Type 'CSV' } if ($_.Key -is 'string') { $key = $_.Key } else { $key = EvaluateFieldCondition -Condition $_.Key -Type 'CSV' } if ($FieldValueMap -and $FieldValueMap.ContainsKey($key)) { $adAttribValue = EvaluateFieldCondition -Condition $FieldValueMap.$key -Type 'CSV' } else { $adAttribValue = $CsvUser.$key } $otherAttribs.$adAttribName = $adAttribValue }) $FieldMatchMap.GetEnumerator().foreach({ if ($_.Value -is 'string') { $adAttribName = $_.Value } else { $adAttribName = EvaluateFieldCondition -Condition $_.Value -CsvUser $CsvUser } if ($_.Key -is 'string') { $key = $_.Key } else { $key = EvaluateFieldCondition -Condition $_.Key -CsvUser $CsvUser } $adAttribValue = $CsvUser.$key $otherAttribs.$adAttribName = $adAttribValue }) $newAdUserParams.OtherAttributes = $otherAttribs if (Get-AdUser -Filter "samAccountName -eq '$userName'") { throw "The user to be created [$($userName)] already exists." } else { if ($PSCmdlet.ShouldProcess("User: [$($userName)] AD attribs: [$($newAdUserParams | Out-String; $newAdUserParams.OtherAttributes | Out-String)]", 'New AD User')) { if ($newUser = New-ADUser @newAdUserParams) { Set-ADAccountPassword -Identity $newUser.DistinguishedName -Reset -NewPassword $secPw $newUser | Add-Member -MemberType NoteProperty -Name 'Password' -Force -Value $pw -PassThru } } } } 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 = @() ## TODO: Why is Select-Object necessary here? [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 = EvaluateFieldCondition -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 EvaluateFieldCondition { [OutputType('string')] [CmdletBinding(DefaultParameterSetName = 'CSVUser')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [scriptblock]$Condition, [Parameter(Mandatory, ParameterSetName = 'CSVUser')] [ValidateNotNullOrEmpty()] [pscustomobject]$CsvUser, [Parameter(Mandatory, ParameterSetName = 'ADUser')] [ValidateNotNullOrEmpty()] [object]$AdUser ) if ($PSBoundParameters.ContainsKey('CsvUser')) { $replace = '$CsvUser' } elseif ($PSBoundParameters.ContainsKey('AdUser')) { $replace = 'ADUser' } $fieldScript = $Condition.ToString() -replace '\$_', $replace & ([scriptblock]::Create($fieldScript)) } 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 = EvaluateFieldCondition -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 { <# .Synopsis Generates one or more complex passwords designed to fulfill the requirements for Active Directory .DESCRIPTION Generates one or more complex passwords designed to fulfill the requirements for Active Directory .EXAMPLE New-SWRandomPassword C&3SX6Kn Will generate one password with a length between 8 and 12 chars. .EXAMPLE New-SWRandomPassword -MinPasswordLength 8 -MaxPasswordLength 12 -Count 4 7d&5cnaB !Bh776T"Fw 9"C"RxKcY %mtM7#9LQ9h Will generate four passwords, each with a length of between 8 and 12 chars. .EXAMPLE New-SWRandomPassword -InputStrings abc, ABC, 123 -PasswordLength 4 3ABa Generates a password with a length of 4 containing atleast one char from each InputString .EXAMPLE New-SWRandomPassword -InputStrings abc, ABC, 123 -PasswordLength 4 -FirstChar abcdefghijkmnpqrstuvwxyzABCEFGHJKLMNPQRSTUVWXYZ 3ABa Generates a password with a length of 4 containing atleast one char from each InputString that will start with a letter from the string specified with the parameter FirstChar .OUTPUTS [String] .NOTES Written by Simon Wåhlin, blog.simonw.se I take no responsibility for any issues caused by this script. .FUNCTIONALITY Generates random passwords .LINK http://blog.simonw.se/powershell-generating-random-password-for-active-directory/ #> [CmdletBinding(DefaultParameterSetName='RandomLength', ConfirmImpact='None')] [OutputType([String])] Param ( # Specifies minimum password length [Parameter(Mandatory=$false, ParameterSetName='RandomLength')] [ValidateScript({$_ -gt 0})] [Alias('Min')] [int]$MinPasswordLength = 12, # Specifies maximum password length [Parameter(Mandatory=$false, ParameterSetName='RandomLength')] [ValidateScript({ if($_ -ge $MinPasswordLength){$true} else{Throw 'Max value cannot be lesser than min value.'}})] [Alias('Max')] [int]$MaxPasswordLength = 15, # Specifies a fixed password length [Parameter(Mandatory=$false, ParameterSetName='FixedLength')] [ValidateRange(1, 2147483647)] [int]$PasswordLength = 8, # Specifies an array of strings containing charactergroups from which the password will be generated. # At least one char from each group (string) will be used. [String[]]$InputStrings = @('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '23456789', '!"#%&'), # Specifies a string containing a character group from which the first character in the password will be generated. # Useful for systems which requires first char in password to be alphabetic. [String] $FirstChar, # Specifies number of passwords to generate. [ValidateRange(1, 2147483647)] [int]$Count = 1 ) Begin { Function Get-Seed{ # Generate a seed for randomization $RandomBytes = New-Object -TypeName 'System.Byte[]' 4 $Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider' $Random.GetBytes($RandomBytes) [BitConverter]::ToUInt32($RandomBytes, 0) } } Process { For($iteration = 1; $iteration -le $Count; $iteration++){ $Password = @{} # Create char arrays containing groups of possible chars [char[][]]$CharGroups = $InputStrings # Create char array containing all chars $AllChars = $CharGroups | ForEach-Object {[Char[]]$_} # Set password length if($PSCmdlet.ParameterSetName -eq 'RandomLength') { if($MinPasswordLength -eq $MaxPasswordLength) { # If password length is set, use set length $PasswordLength = $MinPasswordLength } else { # Otherwise randomize password length $PasswordLength = ((Get-Seed) % ($MaxPasswordLength + 1 - $MinPasswordLength)) + $MinPasswordLength } } # If FirstChar is defined, randomize first char in password from that string. if($PSBoundParameters.ContainsKey('FirstChar')){ $Password.Add(0, $FirstChar[((Get-Seed) % $FirstChar.Length)]) } # Randomize one char from each group Foreach($Group in $CharGroups) { if($Password.Count -lt $PasswordLength) { $Index = Get-Seed While ($Password.ContainsKey($Index)){ $Index = Get-Seed } $Password.Add($Index, $Group[((Get-Seed) % $Group.Count)]) } } # Fill out with chars from $AllChars for($i=$Password.Count; $i -lt $PasswordLength; $i++) { $Index = Get-Seed While ($Password.ContainsKey($Index)){ $Index = Get-Seed } $Password.Add($Index, $AllChars[((Get-Seed) % $AllChars.Count)]) } Write-Output -InputObject $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value)) } } } function InvokeUserTermination { [OutputType('void')] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$AdUser, [Parameter()] [ValidateNotNullOrEmpty()] [scriptblock]$UserTerminationAction ) switch ((GetPsAdSyncConfiguration).UserTermination.Action) { 'Disable' { if ($PSCmdlet.ShouldProcess("AD User [$($AdUser.Name)]", 'Disable')) { Disable-AdAccount -Identity $AdUser.samAccountName -Confirm:$false } } 'Custom' { if (-not $PSBoundParameters.ContainsKey('UserTerminationAction')) { throw 'Custom user termination action chosen in configuration but no custom action was specified.' } & $ } default { throw "Unrecognized user termination action: [$_]" } } } function TestUserTerminated { [OutputType([bool])] [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser ) $csvField = (GetPsAdSyncConfiguration).UserTermination.FieldValueSettings.CsvField $csvValue = (GetPsAdSyncConfiguration).UserTermination.FieldValueSettings.CsvValue if ($CsvUser.$csvField -in $csvValue) { $true } else { $false } } function TestIsUserTerminationEnabled { [OutputType('bool')] [CmdletBinding()] param () if ((GetPsAdSyncConfiguration).UserTermination.Enabled) { $true } else { $false } } function TestIsUserCreationEnabled { [OutputType('bool')] [CmdletBinding()] param () if ((GetPsAdSyncConfiguration).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 = "$PSScriptRoot\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 = (GetPsAdSyncConfiguration).Email.Templates.UnusedAccount.Subject, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FromEmailAddress = (GetPsAdSyncConfiguration).Email.Templates.UnusedAccount.FromEmailAddress, [Parameter()] [ValidateNotNullOrEmpty()] [string]$FromEmailName = (GetPsAdSyncConfiguration).Email.Templates.UnusedAccount.FromEmailName, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SmtpServer = (GetPsAdSyncConfiguration).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, (GetPsAdSyncConfiguration).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) } function TestShouldCreateNewUser { [OutputType('bool')] [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [object]$CsvUser ) if ((TestIsUserTerminationEnabled) -and (TestUserTerminated -CsvUser $CsvUser)) { $false } else { if ($csvfield = (GetPsAdSyncConfiguration).NewUserCreation.Exclude.FieldValueSettings.CsvField) { $csvValue = (GetPsAdSyncConfiguration).NewUserCreation.Exclude.FieldValueSettings.CsvValue if ($CsvUser.$csvField -in $csvValue) { $false } else { $true } } else { $true } } } # .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()] [hashtable]$UserMatchMap, [Parameter(Mandatory, ParameterSetName = 'CreateNewUsers')] [ValidateNotNullOrEmpty()] [ValidateSet('FirstInitialLastName', 'FirstNameLastName', 'FirstNameDotLastName', 'LastNameFirstTwoFirstNameChars')] [string]$UsernamePattern, [Parameter()] [ValidateNotNullOrEmpty()] [scriptblock]$UserTerminationAction, [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 { ## account for the CSV header row $csvRow = $rowsProcessed + 1 $logEntry = $true 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) { $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) { $termParams = @{ AdUser = $adUserMatch.MatchedAduser } if ($PSBoundParameters.ContainsKey('UserTerminationAction')) { $termParams.UserTerminationAction = $UserTerminationAction } InvokeUserTermination @termParams } $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) { $logEntry = $false $attribMismatches | foreach { $logAttribs = @{ CSVAttributeName = 'AttributeChange - {0}' -f [string]($_.CSVField.Keys) CSVAttributeValue = [string]($_.CSVField.Values) ADAttributeName = 'AttributeChange - {0}' -f [string]($_.ActiveDirectoryAttribute.Keys) ADAttributeValue = [string]($_.ActiveDirectoryAttribute.Values) Message = $null } WriteLog -CsvIdentifierField $csvIdField -CsvIdentifierValue $csvIdValue -Attributes $logAttribs } 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: $csvRow" $csvIdValue = "CSV Row: $csvRow" $logAttribs = @{ CSVAttributeName = "CSV Row: $csvRow" CSVAttributeValue = "CSV Row: $csvRow" ADAttributeName = 'NoMatch' ADAttributeValue = 'NoMatch' Message = $null } } elseif ($PSBoundParameters.ContainsKey('UserMatchMap') -and (TestShouldCreateNewUser -CsvUser $csvUser)) { $csvIdField = $csvIds.Field -join ',' if (-not $ReportOnly.IsPresent) { $newUserParams = @{ CsvUser = $csvUser UsernamePattern = $UsernamePattern UserMatchMap = $UserMatchMap RandomPassword = $true FieldSyncMap = $FieldSyncMap FieldMatchMap = $FieldMatchMap } if ($PSBoundParameters.ContainsKey('FieldValueMap')) { $newUserParams.FieldValueMap = $FieldValueMap } $newAdUser = New-CompanyAdUser @newUserParams } $logAttribs = @{ CSVAttributeName = 'NewUserCreated' CSVAttributeValue = 'NewUserCreated' ADAttributeName = 'NewUserCreated' ADAttributeValue = 'NewUserCreated' Message = "UserName: [$($newAdUser.Name)] - Password: [$($newAdUser.Password)]" } $csvIdValue = ($csvIds | foreach { $csvUser.($_.Field) }) } else { $csvIdField = $csvIds.Field -join ',' $csvIdValue = "CSV Row: $csvRow" $logAttribs = @{ CSVAttributeName = "CSV Row: $csvRow" CSVAttributeValue = "CSV Row: $csvRow" ADAttributeName = 'NoMatch' ADAttributeValue = 'NoMatch' Message = $null } } } } catch { $csvIdField = "CSV Row: $csvRow" $csvIdValue = "CSV Row: $csvRow" $logAttribs = @{ CSVAttributeName = 'Error' CSVAttributeValue = 'Error' ADAttributeName = 'Error' ADAttributeValue = 'Error' Message = $_.Exception.Message } } finally { if ($logEntry) { WriteLog -CsvIdentifierField $csvIdField -CsvIdentifierValue $csvIdValue -Attributes $logAttribs } $rowsProcessed++ } }) } catch { Write-Error -Message "Function: $($MyInvocation.MyCommand.Name) Error: $($_.Exception.Message)" } } } |