From 58b13ea3162fbfabd0c2f80cb10dfbcd13879bbf Mon Sep 17 00:00:00 2001 From: iainbrighton Date: Fri, 20 Nov 2015 22:49:36 +0000 Subject: [PATCH] Initial implementation of #44 that also fixes #11 Adds additional user property support and test coverage --- DSCResources/MSFT_xADUser/MSFT_xADUser.psm1 | 1033 ++++++++++++++--- .../MSFT_xADUser/MSFT_xADUser.schema.mof | 48 +- README.md | 71 +- Tests/xADUser.Tests.ps1 | 467 ++++++++ 4 files changed, 1461 insertions(+), 158 deletions(-) create mode 100644 Tests/xADUser.Tests.ps1 diff --git a/DSCResources/MSFT_xADUser/MSFT_xADUser.psm1 b/DSCResources/MSFT_xADUser/MSFT_xADUser.psm1 index 6a763c927..2f2b92278 100644 --- a/DSCResources/MSFT_xADUser/MSFT_xADUser.psm1 +++ b/DSCResources/MSFT_xADUser/MSFT_xADUser.psm1 @@ -1,229 +1,964 @@ -# -# xADUser: DSC resource to create a new Active Directory user. -# +# Localized messages +data LocalizedData +{ + # culture="en-US" + ConvertFrom-StringData @' + RoleNotFoundError = Please ensure that the PowerShell module for role '{0}' is installed. + RetrievingADUserError = Error looking up Active Directory user '{0}' ({0}@{1}). + PasswordParameterConflictError = Parameter '{0}' cannot be set to '{1}' when the '{2}' parameter is specified. + + RetrievingADUser = Retrieving Active Directory user '{0}' ({0}@{1}) ... + CreatingADDomainConnection = Creating connection to Active Directory domain '{0}' ... + CheckingADUserPassword = Checking Active Directory user '{0}' password ... + ADUserIsPresent = Active Directory user '{0}' ({0}@{1}) is present. + ADUserNotPresent = Active Directory user '{0}' ({0}@{1}) was NOT present. + ADUserNotDesiredPropertyState = User '{0}' property is NOT in the desired state. Expected '{1}', actual '{2}'. + + AddingADUser = Adding Active Directory user '{0}'. + RemovingADUser = Removing Active Directory user '{0}'. + UpdatingADUser = Updating Active Directory user '{0}'. + SettingADUserPassword = Setting Active Directory user password. + UpdatingADUserProperty = Updating user property '{0}' with/to '{1}'. + RemovingADUserProperty = Removing user property '{0}' with '{1}'. + MovingADUser = Moving user from '{0}' to '{1}'. + RenamingADUser = Renaming user from '{0}' to '{1}'. +'@ +} + +## Create a property map that maps the DSC resource parameters to the +## Active Directory user attributes. +$adPropertyMap = @( + @{ Parameter = 'CommonName'; ADProperty = 'cn'; } + @{ Parameter = 'UserPrincipalName'; } + @{ Parameter = 'DisplayName'; } + @{ Parameter = 'Path'; ADProperty = 'distinguishedName'; } + @{ Parameter = 'GivenName'; } + @{ Parameter = 'Initials'; } + @{ Parameter = 'Surname'; ADProperty = 'sn'; } + @{ Parameter = 'Description'; } + @{ Parameter = 'StreetAddress'; } + @{ Parameter = 'POBox'; } + @{ Parameter = 'City'; ADProperty = 'l'; } + @{ Parameter = 'State'; ADProperty = 'st'; } + @{ Parameter = 'PostalCode'; } + @{ Parameter = 'Country'; ADProperty = 'c'; } + @{ Parameter = 'Department'; } + @{ Parameter = 'Division'; } + @{ Parameter = 'Company'; } + @{ Parameter = 'Office'; ADProperty = 'physicalDeliveryOfficeName'; } + @{ Parameter = 'JobTitle'; ADProperty = 'title'; } + @{ Parameter = 'EmailAddress'; ADProperty = 'mail'; } + @{ Parameter = 'EmployeeID'; } + @{ Parameter = 'EmployeeNumber'; } + @{ Parameter = 'HomeDirectory'; } + @{ Parameter = 'HomeDrive'; } + @{ Parameter = 'HomePage'; ADProperty = 'wWWHomePage'; } + @{ Parameter = 'ProfilePath'; } + @{ Parameter = 'LogonScript'; ADProperty = 'scriptPath'; } + @{ Parameter = 'Notes'; ADProperty = 'info'; } + @{ Parameter = 'OfficePhone'; ADProperty = 'telephoneNumber'; } + @{ Parameter = 'MobilePhone'; ADProperty = 'mobile'; } + @{ Parameter = 'Fax'; ADProperty = 'facsimileTelephoneNumber'; } + @{ Parameter = 'Pager'; } + @{ Parameter = 'IPPhone'; } + @{ Parameter = 'HomePhone'; } + @{ Parameter = 'Enabled'; } + @{ Parameter = 'Manager'; } + @{ Parameter = 'PasswordNeverExpires'; UseCmdletParameter = $true; } + @{ Parameter = 'CannotChangePassword'; UseCmdletParameter = $true; } +) function Get-TargetResource { [OutputType([System.Collections.Hashtable])] param ( + ## Only used if password is managed. [Parameter(Mandatory)] - [string]$DomainName, - + [System.String] $DomainName, + + # SamAccountName [Parameter(Mandatory)] - [string]$UserName, + [System.String] $UserName, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $Password, - [Parameter(Mandatory)] - [PSCredential]$DomainAdministratorCredential, + [ValidateSet('Present', 'Absent')] + [System.String] $Ensure = 'Present', + + # Common name (CN) + [ValidateNotNull()] + [System.String] $CommonName = $UserName, + + [ValidateNotNull()] + [System.String] $UserPrincipalName, + + [ValidateNotNull()] + [System.String] $DisplayName, + + [ValidateNotNull()] + [System.String] $Path, + + [ValidateNotNull()] + [System.String] $GivenName, + + [ValidateNotNull()] + [System.String] $Initials, + + [ValidateNotNull()] + [System.String] $Surname, + + [ValidateNotNull()] + [System.String] $Description, + + [ValidateNotNull()] + [System.String] $StreetAddress, + + [ValidateNotNull()] + [System.String] $POBox, + + [ValidateNotNull()] + [System.String] $City, + + [ValidateNotNull()] + [System.String] $State, + + [ValidateNotNull()] + [System.String] $PostalCode, + + [ValidateNotNull()] + [System.String] $Country, + + [ValidateNotNull()] + [System.String] $Department, + + [ValidateNotNull()] + [System.String] $Division, + + [ValidateNotNull()] + [System.String] $Company, + + [ValidateNotNull()] + [System.String] $Office, + + [ValidateNotNull()] + [System.String] $JobTitle, + + [ValidateNotNull()] + [System.String] $EmailAddress, + + [ValidateNotNull()] + [System.String] $EmployeeID, + + [ValidateNotNull()] + [System.String] $EmployeeNumber, + + [ValidateNotNull()] + [System.String] $HomeDirectory, + + [ValidateNotNull()] + [System.String] $HomeDrive, + + [ValidateNotNull()] + [System.String] $HomePage, + + [ValidateNotNull()] + [System.String] $ProfilePath, + + [ValidateNotNull()] + [System.String] $LogonScript, + + [ValidateNotNull()] + [System.String] $Notes, + + [ValidateNotNull()] + [System.String] $OfficePhone, + + [ValidateNotNull()] + [System.String] $MobilePhone, + + [ValidateNotNull()] + [System.String] $Fax, + + [ValidateNotNull()] + [System.String] $HomePhone, + + [ValidateNotNull()] + [System.String] $Pager, + + [ValidateNotNull()] + [System.String] $IPPhone, + + ## User's manager specified as a Distinguished Name (DN) + [ValidateNotNull()] + [System.String] $Manager, - [PSCredential]$Password, + [ValidateNotNull()] + [System.Boolean] $Enabled = $true, - [ValidateSet("Present","Absent")] - [string]$Ensure = "Present" + [ValidateNotNull()] + [System.Boolean] $CannotChangePassword, + + [ValidateNotNull()] + [System.Boolean] $PasswordNeverExpires, + + [ValidateNotNull()] + [System.String] $DomainController, + + ## Ideally this should just be called 'Credential' but is here for backwards compatibility + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $DomainAdministratorCredential ) + + Assert-Module -ModuleName 'ActiveDirectory'; try { - Write-Verbose -Message "Checking if the user '$($UserName)' in domain '$($DomainName)' is present ..." - $user = Get-AdUser -Identity $UserName -Credential $DomainAdministratorCredential - Write-Verbose -Message "Found '$($UserName)' in domain '$($DomainName)'." - $Ensure = "Present" + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters; + + $adProperties = @(); + ## Create an array of the AD propertie names to retrieve from the property map + foreach ($property in $adPropertyMap) + { + if ($property.ADProperty) + { + $adProperties += $property.ADProperty; + } + else + { + $adProperties += $property.Parameter; + } + } + + Write-Verbose -Message ($LocalizedData.RetrievingADUser -f $UserName, $DomainName); + $adUser = Get-ADUser @adCommonParameters -Properties $adProperties; + Write-Verbose -Message ($LocalizedData.ADUserIsPresent -f $UserName, $DomainName); + $Ensure = 'Present'; } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { - Write-Verbose -Message "User '$($UserName)' in domain '$($DomainName)' is NOT present." - $Ensure = "Absent" + Write-Verbose -Message ($LocalizedData.ADUserNotPresent -f $UserName, $DomainName); + $Ensure = 'Absent'; } catch { - Write-Error -Message "Error looking up user '$($UserName)' in domain '$($DomainName)'." - throw $_ + Write-Error -Message ($LocalizedData.RetrievingADUserError -f $UserName, $DomainName); + throw $_; } - @{ - DomainName = $DomainName - UserName = $UserName - Ensure = $Ensure + $targetResource = @{ + DomainName = $DomainName; + Password = $Password; + UserName = $UserName; + DistinguishedName = $adUser.DistinguishedName; ## Read-only property + Ensure = $Ensure; + DomainController = $DomainController; } -} -function Set-TargetResource -{ - param - ( - [Parameter(Mandatory)] - [string]$DomainName, - - [Parameter(Mandatory)] - [string]$UserName, - - [Parameter(Mandatory)] - [PSCredential]$DomainAdministratorCredential, - - [PSCredential]$Password, - - [ValidateSet("Present","Absent")] - [string]$Ensure = "Present" - ) - try + ## Retrieve each property from the ADPropertyMap and add to the hashtable + foreach ($property in $adPropertyMap) { - ValidateProperties @PSBoundParameters -Apply - } - catch - { - Write-Error -Message "Error configuring user '$($UserName)' in domain '$($DomainName)'." - throw $_ + if ($property.Parameter -eq 'Path') { + ## The path returned is not the parent container + if (-not [System.String]::IsNullOrEmpty($adUser.DistinguishedName)) + { + $targetResource['Path'] = Get-ADObjectParentDN -DN $adUser.DistinguishedName; + } + } + elseif ($property.ADProperty) + { + ## The AD property name is different to the function parameter to use this + $targetResource[$property.Parameter] = $adUser.($property.ADProperty); + } + else + { + ## The AD property name matches the function parameter + $targetResource[$property.Parameter] = $adUser.($property.Parameter); + } } -} + return $targetResource; + +} #end function Get-TargetResource function Test-TargetResource { [OutputType([System.Boolean])] param ( + ## Only used if password is managed. [Parameter(Mandatory)] - [string]$DomainName, - - [Parameter(Mandatory)] - [string]$UserName, + [System.String] $DomainName, + # SamAccountName [Parameter(Mandatory)] - [PSCredential]$DomainAdministratorCredential, + [System.String] $UserName, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $Password, + + [ValidateSet('Present', 'Absent')] + [System.String] $Ensure = 'Present', + + # Common name (CN) + [ValidateNotNull()] + [System.String] $CommonName = $UserName, + + [ValidateNotNull()] + [System.String] $UserPrincipalName, + + [ValidateNotNull()] + [System.String] $DisplayName, + + [ValidateNotNull()] + [System.String] $Path, + + [ValidateNotNull()] + [System.String] $GivenName, + + [ValidateNotNull()] + [System.String] $Initials, + + [ValidateNotNull()] + [System.String] $Surname, + + [ValidateNotNull()] + [System.String] $Description, + + [ValidateNotNull()] + [System.String] $StreetAddress, + + [ValidateNotNull()] + [System.String] $POBox, + + [ValidateNotNull()] + [System.String] $City, + + [ValidateNotNull()] + [System.String] $State, + + [ValidateNotNull()] + [System.String] $PostalCode, + + [ValidateNotNull()] + [System.String] $Country, + + [ValidateNotNull()] + [System.String] $Department, + + [ValidateNotNull()] + [System.String] $Division, + + [ValidateNotNull()] + [System.String] $Company, + + [ValidateNotNull()] + [System.String] $Office, + + [ValidateNotNull()] + [System.String] $JobTitle, + + [ValidateNotNull()] + [System.String] $EmailAddress, + + [ValidateNotNull()] + [System.String] $EmployeeID, + + [ValidateNotNull()] + [System.String] $EmployeeNumber, + + [ValidateNotNull()] + [System.String] $HomeDirectory, + + [ValidateNotNull()] + [System.String] $HomeDrive, + + [ValidateNotNull()] + [System.String] $HomePage, + + [ValidateNotNull()] + [System.String] $ProfilePath, + + [ValidateNotNull()] + [System.String] $LogonScript, + + [ValidateNotNull()] + [System.String] $Notes, + + [ValidateNotNull()] + [System.String] $OfficePhone, + + [ValidateNotNull()] + [System.String] $MobilePhone, + + [ValidateNotNull()] + [System.String] $Fax, - [PSCredential]$Password, + [ValidateNotNull()] + [System.String] $HomePhone, - [ValidateSet("Present","Absent")] - [string]$Ensure = "Present" + [ValidateNotNull()] + [System.String] $Pager, + + [ValidateNotNull()] + [System.String] $IPPhone, + + ## User's manager specified as a Distinguished Name (DN) + [ValidateNotNull()] + [System.String] $Manager, + + [ValidateNotNull()] + [System.Boolean] $Enabled = $true, + + [ValidateNotNull()] + [System.Boolean] $CannotChangePassword, + + [ValidateNotNull()] + [System.Boolean] $PasswordNeverExpires, + + [ValidateNotNull()] + [System.String] $DomainController, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $DomainAdministratorCredential ) - try + Validate-Parameters @PSBoundParameters; + $targetResource = Get-TargetResource @PSBoundParameters; + $isCompliant = $true; + + if ($Ensure -eq 'Absent') { - $parameters = $PSBoundParameters.Remove("Debug"); - ValidateProperties @PSBoundParameters + if ($targetResource.Ensure -eq 'Present') + { + Write-Verbose -Message ($LocalizedData.ADUserNotDesiredPropertyState -f 'Ensure', $PSBoundParameters.Ensure, $targetResource.Ensure); + $isCompliant = $false; + } } - catch + else { - Write-Error -Message "Error testing user '$($UserName)' in domain '$($DomainName)'." - throw $_ + ## Add common name, ensure and enabled as they may not be explicitly passed and we want to enumerate them + $PSBoundParameters['Name'] = $Name; + $PSBoundParameters['Ensure'] = $Ensure; + $PSBoundParameters['Enabled'] = $Enabled; + + foreach ($parameter in $PSBoundParameters.Keys) + { + if ($parameter -eq 'Password') + { + $testPasswordParams = @{ + Username = $UserName; + Password = $Password; + DomainName = $DomainName; + } + if ($DomainAdministratorCredential) + { + $testPasswordParams['DomainAdministratorCredential'] = $DomainAdministratorCredential; + } + if (-not (Test-Password @testPasswordParams)) + { + Write-Verbose -Message ($LocalizedData.ADUserNotDesiredPropertyState -f 'Password', '', ''); + $isCompliant = $false; + } + } + # Only check properties that are returned by Get-TargetResource + elseif ($targetResource.ContainsKey($parameter)) + { + ## This check is required to be able to explicitly remove values with an empty string, if required + if (([System.String]::IsNullOrEmpty($PSBoundParameters.$parameter)) -and ([System.String]::IsNullOrEmpty($targetResource.$parameter))) + { + # Both values are null/empty and therefore we are compliant + } + elseif ($PSBoundParameters.$parameter -ne $targetResource.$parameter) + { + Write-Verbose -Message ($LocalizedData.ADUserNotDesiredPropertyState -f $parameter, $PSBoundParameters.$parameter, $targetResource.$parameter); + $isCompliant = $false; + } + } + } #end foreach PSBoundParameter } -} -function ValidateProperties + return $isCompliant; + +} #end function Test-TargetResource + +function Set-TargetResource { param ( + ## Only used if password is managed. [Parameter(Mandatory)] - [string]$DomainName, - + [System.String] $DomainName, + + # SamAccountName [Parameter(Mandatory)] - [string]$UserName, + [System.String] $UserName, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $Password, - [Parameter(Mandatory)] - [PSCredential]$DomainAdministratorCredential, + [ValidateSet('Present', 'Absent')] + [System.String] $Ensure = 'Present', + + [ValidateNotNull()] + [System.String] $CommonName = $UserName, + + [ValidateNotNull()] + [System.String] $UserPrincipalName, + + [ValidateNotNull()] + [System.String] $DisplayName, + + [ValidateNotNull()] + [System.String] $Path, + + [ValidateNotNull()] + [System.String] $GivenName, + + [ValidateNotNull()] + [System.String] $Initials, + + [ValidateNotNull()] + [System.String] $Surname, + + [ValidateNotNull()] + [System.String] $Description, - [PSCredential]$Password, + [ValidateNotNull()] + [System.String] $StreetAddress, - [ValidateSet("Present","Absent")] - [string]$Ensure = "Present", + [ValidateNotNull()] + [System.String] $POBox, - [Switch]$Apply - ) + [ValidateNotNull()] + [System.String] $City, - $result = $true - try - { - Write-Verbose -Message "Checking if the user '$($UserName)' in domain '$($DomainName)' is present ..." - $user = Get-AdUser -Identity $UserName -Credential $DomainAdministratorCredential - Write-Verbose -Message "Found '$($UserName)' in domain '$($DomainName)'." + [ValidateNotNull()] + [System.String] $State, + + [ValidateNotNull()] + [System.String] $PostalCode, + + [ValidateNotNull()] + [System.String] $Country, + + [ValidateNotNull()] + [System.String] $Department, + + [ValidateNotNull()] + [System.String] $Division, + + [ValidateNotNull()] + [System.String] $Company, + + [ValidateNotNull()] + [System.String] $Office, + + [ValidateNotNull()] + [System.String] $JobTitle, + + [ValidateNotNull()] + [System.String] $EmailAddress, - if ($Ensure -eq "Absent") - { - if ($Apply) - { - Remove-ADUser -Identity $UserName -Credential $DomainAdministratorCredential -Confirm:$false - return - } - else - { - return $false - } - } + [ValidateNotNull()] + [System.String] $EmployeeID, + + [ValidateNotNull()] + [System.String] $EmployeeNumber, + + [ValidateNotNull()] + [System.String] $HomeDirectory, + + [ValidateNotNull()] + [System.String] $HomeDrive, + + [ValidateNotNull()] + [System.String] $HomePage, - if ($Apply) - { - # We need to enable the account for password validation. - if (!($user.Enabled)) - { - Set-AdUser -Identity $UserName -Enabled $true -Credential $DomainAdministratorCredential - Write-Verbose -Message "Enabled user account '$($UserName)' in domain '$($DomainName)'." - } - } + [ValidateNotNull()] + [System.String] $ProfilePath, - if ($Password) - { - Write-Verbose -Message "Checking if the password specified for user '$($UserName)' is valid ..." - Add-Type -AssemblyName "System.DirectoryServices.AccountManagement" - - Write-Verbose -Message "Creating connection to the domain '$($DomainName)' ..." - $prnContext = new-object System.DirectoryServices.AccountManagement.PrincipalContext( - "Domain", $DomainName, $DomainAdministratorCredential.UserName, ` - $DomainAdministratorCredential.GetNetworkCredential().Password) + [ValidateNotNull()] + [System.String] $LogonScript, + + [ValidateNotNull()] + [System.String] $Notes, + + [ValidateNotNull()] + [System.String] $OfficePhone, + + [ValidateNotNull()] + [System.String] $MobilePhone, + + [ValidateNotNull()] + [System.String] $Fax, + + [ValidateNotNull()] + [System.String] $HomePhone, + + [ValidateNotNull()] + [System.String] $Pager, + + [ValidateNotNull()] + [System.String] $IPPhone, + + ## User's manager specified as a Distinguished Name (DN) + [ValidateNotNull()] + [System.String] $Manager, + + [ValidateNotNull()] + [System.Boolean] $Enabled = $true, + + [ValidateNotNull()] + [System.Boolean] $CannotChangePassword, + + [ValidateNotNull()] + [System.Boolean] $PasswordNeverExpires, + + [ValidateNotNull()] + [System.String] $DomainController, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $DomainAdministratorCredential + ) + + Validate-Parameters @PSBoundParameters; + $targetResource = Get-TargetResource @PSBoundParameters; + + ## Add common name, ensure and enabled as they may not be explicitly passed + $PSBoundParameters['Name'] = $Name; + $PSBoundParameters['Ensure'] = $Ensure; + $PSBoundParameters['Enabled'] = $Enabled; - $result = $prnContext.ValidateCredentials($UserName, $Password.GetNetworkCredential().Password) - if($result) + if ($Ensure -eq 'Present') + { + if ($targetResource.Ensure -eq 'Absent') { + ## User does not exist and needs creating + $newADUserParams = Get-ADCommonParameters @PSBoundParameters -UseNameParameter; + if ($PSBoundParameters.ContainsKey('Path')) { - Write-Verbose -Message "The password for user '$($UserName)' is valid." - return $true + $newADUserParams['Path'] = $Path; } - else + Write-Verbose -Message ($LocalizedData.AddingADUser -f $UserName); + New-ADUser @newADUserParams -SamAccountName $UserName; + ## Now retrieve the newly created user + $targetResource = Get-TargetResource @PSBoundParameters; + } + + $setADUserParams = Get-ADCommonParameters @PSBoundParameters; + $replaceUserProperties = @{}; + $removeUserProperties = @{}; + foreach ($parameter in $PSBoundParameters.Keys) + { + ## Only check/action properties specified/declared parameters that match one of the function's + ## parameters. This will ignore common parameters such as -Verbose etc. + if ($targetResource.ContainsKey($parameter)) { - Write-Verbose -Message "The password for user '$($UserName)' is NOT valid." - if ($Apply) + if ($parameter -eq 'Path' -and ($PSBoundParameters.Path -ne $targetResource.Path)) { - Set-AdAccountPassword -Reset -Identity $UserName -NewPassword $Password.Password -Credential $DomainAdministratorCredential - Write-Verbose -Message "Successfully reset password for user '$($UserName)'." + ## Cannot move users by updating the DistinguishedName property + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters; + ## Using the SamAccountName for identity with Move-ADObject does not work, use the DN instead + $adCommonParameters['Identity'] = $targetResource.DistinguishedName; + Write-Verbose -Message ($LocalizedData.MovingADUser -f $targetResource.Path, $PSBoundParameters.Path); + Move-ADObject @adCommonParameters -TargetPath $PSBoundParameters.Path; } - else + elseif ($parameter -eq 'CommonName' -and ($PSBoundParameters.CommonName -ne $targetResource.CommonName)) { - return $false + ## Cannot rename users by updating the CN property directly + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters; + ## Using the SamAccountName for identity with Rename-ADObject does not work, use the DN instead + $adCommonParameters['Identity'] = $targetResource.DistinguishedName; + Write-Verbose -Message ($LocalizedData.RenamingADUser -f $targetResource.CommonName, $PSBoundParameters.CommonName); + Rename-ADObject @adCommonParameters -NewName $PSBoundParameters.CommonName; } - } + elseif ($parameter -eq 'Password') + { + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters; + Write-Verbose -Message ($LocalizedData.SettingADUserPassword -f $UserName); + Set-ADAccountPassword @adCommonParameters -Reset -NewPassword $Password.Password; + } + elseif ($parameter -eq 'Enabled' -and ($PSBoundParameters.$parameter -ne $targetResource.$parameter)) + { + ## We cannot enable/disable an account with -Add or -Replace parameters, but inform that + ## we will change this as it is out of compliance (it always gets set anyway) + Write-Verbose -Message ($LocalizedData.UpdatingADUserProperty -f $parameter, $PSBoundParameters.$parameter); + } + elseif ($PSBoundParameters.$parameter -ne $targetResource.$parameter) + { + ## Find the associated AD property + $adProperty = $adPropertyMap | Where-Object { $_.Parameter -eq $parameter }; + + ## Are we removing properties + if ([System.String]::IsNullOrEmpty($PSBoundParameters.$parameter)) + { + ## Only remove if the existing value in not null or empty + if (-not ([System.String]::IsNullOrEmpty($targetResource.$parameter))) + { + Write-Verbose -Message ($LocalizedData.RemovingADUserProperty -f $parameter, $PSBoundParameters.$parameter); + if ($adProperty.UseCmdletParameter -eq $true) + { + ## We need to pass the parameter explicitly to Set-ADUser, not via -Remove + $setADUserParams[$adProperty.Parameter] = $PSBoundParameters.$parameter; + } + elseif ([System.String]::IsNullOrEmpty($adProperty.ADProperty)) + { + $removeUserProperties[$adProperty.Parameter] = $targetResource.$parameter; + } + else + { + $removeUserProperties[$adProperty.ADProperty] = $targetResource.$parameter; + } + } + } #end if remove existing value + else + { + ## We are replacing the existing value + Write-Verbose -Message ($LocalizedData.UpdatingADUserProperty -f $parameter, $PSBoundParameters.$parameter); + if ($adProperty.UseCmdletParameter -eq $true) + { + ## We need to pass the parameter explicitly to Set-ADUser, not via -Replace + $setADUserParams[$adProperty.Parameter] = $PSBoundParameters.$parameter; + } + elseif ([System.String]::IsNullOrEmpty($adProperty.ADProperty)) + { + $replaceUserProperties[$adProperty.Parameter] = $PSBoundParameters.$parameter; + } + else + { + $replaceUserProperties[$adProperty.ADProperty] = $PSBoundParameters.$parameter; + } + } #end if replace existing value + } + + } #end if TargetResource parameter + } #end foreach PSBoundParameter + + ## Only pass -Remove and/or -Replace if we have something to set/change + if ($replaceUserProperties.Count -gt 0) + { + $setADUserParams['Replace'] = $replaceUserProperties; } - else - { - Write-Verbose -Message "Found user '$($UserName)' in domain '$($DomainName)'." - return $true + if ($removeUserProperties.Count -gt 0) + { + $setADUserParams['Remove'] = $removeUserProperties; } + + Write-Verbose -Message ($LocalizedData.UpdatingADUser -f $UserName); + Set-ADUser @setADUserParams -Enabled $Enabled; } - catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] + elseif (($Ensure -eq 'Absent') -and ($targetResource.Ensure -eq 'Present')) { - Write-Verbose -Message "User '$($UserName)' in domain '$($DomainName)' is NOT present." - if ($Apply) + ## User exists and needs removing + Write-Verbose ($LocalizedData.RemovingADUser -f $UserName); + $adCommonParameters = Get-ADCommonParameters @PSBoundParameters; + Remove-ADUser @adCommonParameters -Confirm:$false; + } + +} #end function Set-TargetResource + +# Internal function to validate unsupported options/configurations +function Validate-Parameters +{ + [CmdletBinding()] + param + ( + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $Password, + + [ValidateNotNull()] + [System.Boolean] $Enabled = $true, + + [Parameter(ValueFromRemainingArguments)] + $IgnoredArguments + ) + + ## We cannot test/set passwords on disabled AD accounts + if (($PSBoundParameters.ContainsKey('Password')) -and ($Enabled -eq $false)) + { + $throwInvalidArgumentErrorParams = @{ + ErrorId = 'xADUser_DisabledAccountPasswordConflict'; + ErrorMessage = $LocalizedData.PasswordParameterConflictError -f 'Enabled', $false, 'Password'; + } + ThrowInvalidArgumentError @throwInvalidArgumentErrorParams; + } + +} #end function Validate-Parameters + +# Internal function to test the validity of a user's password. +function Test-Password +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [System.String] $DomainName, + + [Parameter(Mandatory)] + [System.String] $UserName, + + [Parameter(Mandatory)] + [System.Management.Automation.PSCredential] $Password, + + [ValidateNotNull()] + [System.Management.Automation.PSCredential] $DomainAdministratorCredential + ) + + Write-Verbose -Message ($LocalizedData.CreatingADDomainConnection -f $DomainName); + Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement'; + + if ($DomainAdministratorCredential) + { + $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( + 'Domain', $DomainName, $DomainAdministratorCredential.UserName, ` + $DomainAdministratorCredential.GetNetworkCredential().Password); + } + else + { + $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Domain', $DomainName, $null, $null); + } + Write-Verbose -Message ($LocalizedData.CheckingADUserPassword -f $UserName); + return $principalContext.ValidateCredentials($UserName, $Password.GetNetworkCredential().Password); + +} #end function Test-Password + +# Internal function to build common parameters for the Active Directory cmdlets +function Get-ADCommonParameters +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [ValidateNotNullOrEmpty()] + [System.String] + $CommonName, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + $DomainAdministratorCredential, + + [ValidateNotNullOrEmpty()] + [System.String] + $DomainController, + + [Parameter(ValueFromRemainingArguments)] + $IgnoredArguments, + + [System.Management.Automation.SwitchParameter] + $UseNameParameter + ) + + ## The Get-ADUser, Set-ADUser and Remove-ADUser cmdlets take an -Identity parameter, but the New-ADUser cmdlet uses the -Name parameter + if ($UseNameParameter) + { + if ($PSBoundParameters.ContainsKey('CommonName')) { - if ($Ensure -ne "Absent") - { - $params = @{ - Name = $UserName - Credential = $DomainAdministratorCredential - Enabled = $true - UserPrincipalName = "$UserName@$DomainName" - PasswordNeverExpires = $true - } - if ($Password) - { - $params.Add( "AccountPassword", $Password.Password ) - } - New-AdUser @params - Write-Verbose -Message "Successfully created user account '$($UserName)' in domain '$($DomainName)'." - } + $adUserParameters = @{ Name = $CommonName; } } else { - return ($Ensure -eq "Absent") + $adUserParameters = @{ Name = $UserName; } } } + else + { + $adUserParameters = @{ Identity = $UserName; } + } + + if ($DomainAdministratorCredential) + { + $adUserParameters['Credential'] = $DomainAdministratorCredential; + } + if ($DomainController) + { + $adUserParameters['Server'] = $DomainController; + } + return $adUserParameters; + +} #end function Get-ADCommonParameters + +# Internal function to assert if the role specific module is installed or not +function Assert-Module +{ + [CmdletBinding()] + param + ( + [System.String] $ModuleName = 'ActiveDirectory' + ) + + if (-not (Get-Module -Name $ModuleName -ListAvailable)) + { + $errorId = 'xADUser_ModuleNotFound'; + $errorMessage = $LocalizedData.RoleNotFoundError -f $moduleName; + ThrowInvalidOperationError -ErrorId $errorId -ErrorMessage $errorMessage; + } +} #end function Assert-Module + +# Internal function to get an Active Directory object's parent Distinguished Name +function Get-ADObjectParentDN +{ + # https://www.uvm.edu/~gcd/2012/07/listing-parent-of-ad-object-in-powershell/ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [System.String] + $DN + ) + + $distinguishedNameParts = $DN -split '(? + + It "Calls 'Move-ADObject' when 'Ensure' is 'Present', the account exists but Path is incorrect" { + $testTargetPath = 'CN=Users,DC=contoso,DC=com'; + Mock Set-ADUser { } + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser['DistinguishedName'] = "CN=$($testPresentParams.UserName),OU=WrongPath,DC=contoso,DC=com"; + return $duffADUser; + } + Mock Move-ADObject -ParameterFilter { $TargetPath -eq $testTargetPath } -MockWith { } + + Set-TargetResource @testPresentParams -Path $testTargetPath -Enabled $true; + + Assert-MockCalled Move-ADObject -ParameterFilter { $TargetPath -eq $testTargetPath } -Scope It; + } + + It "Calls 'Rename-ADObject' when 'Ensure' is 'Present', the account exists but 'CommonName' is incorrect" { + $testCommonName = 'Test Common Name'; + Mock Set-ADUser { } + Mock Get-ADUser { return $fakeADUser; } + Mock Rename-ADObject -ParameterFilter { $NewName -eq $testCommonName } -MockWith { } + + Set-TargetResource @testPresentParams -CommonName $testCommonName -Enabled $true; + + Assert-MockCalled Rename-ADObject -ParameterFilter { $NewName -eq $testCommonName } -Scope It; + } + + It "Calls 'Set-ADAccountPassword' when 'Password' parameter is specified" { + Mock Get-ADUser { return $fakeADUser; } + Mock Set-ADUser { } + Mock Set-ADAccountPassword -ParameterFilter { $NewPassword -eq $testCredential.Password } -MockWith { } + + Set-TargetResource @testPresentParams -Password $testCredential; + + Assert-MockCalled Set-ADAccountPassword -ParameterFilter { $NewPassword -eq $testCredential.Password } -Scope It; + } + + It "Calls 'Set-ADUser' with 'Replace' when existing matching AD property is null" { + $testADPropertyName = 'Description'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = $null; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -Description 'My custom description'; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Set-ADUser' with 'Replace' when existing matching AD property is empty" { + $testADPropertyName = 'Description'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = ''; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -Description 'My custom description'; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Set-ADUser' with 'Remove' when new matching AD property is empty" { + $testADPropertyName = 'Description'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = 'Incorrect parameter value'; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Remove.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -Description ''; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Remove.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Set-ADUser' with 'Replace' when existing mismatched AD property is null" { + $testADPropertyName = 'Title'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = $null; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -JobTitle 'Gaffer'; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Set-ADUser' with 'Replace' when existing mismatched AD property is empty" { + $testADPropertyName = 'Title'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = ''; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -JobTitle 'Gaffer'; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Replace.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Set-ADUser' with 'Remove' when new mismatched AD property is empty" { + $testADPropertyName = 'Title'; + Mock Get-ADUser { + $duffADUser = $fakeADUser.Clone(); + $duffADUser[$testADPropertyName] = 'Incorrect job title'; + return $duffADUser; + } + Mock Set-ADUser -ParameterFilter { $Remove.ContainsKey($testADPropertyName) } -MockWith { } + + Set-TargetResource @testPresentParams -JobTitle ''; + + Assert-MockCalled Set-ADUser -ParameterFilter { $Remove.ContainsKey($testADPropertyName) } -Scope It -Exactly 1; + } + + It "Calls 'Remove-ADUser' when 'Ensure' is 'Absent' and user account exists" { + Mock Get-ADUser { return [PSCustomObject] $fakeADUser; } + Mock Remove-ADUser -ParameterFilter { $Identity.ToString() -eq $testAbsentParams.UserName } -MockWith { } + + Set-TargetResource @testAbsentParams; + + Assert-MockCalled Remove-ADUser -ParameterFilter { $Identity.ToString() -eq $testAbsentParams.UserName } -Scope It; + } + + } #end context Validate Set-TargetResource method + + } #end InModuleScope +}