DSCResources/DSC_xUserResource/DSC_xUserResource.psm1
# User name and password needed for this resource and Write-Verbose Used in helper functions [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCUseVerboseMessageInDSCResource', '')] param () $errorActionPreference = 'Stop' Set-StrictMode -Version 'Latest' $modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' # Import the shared modules Import-Module -Name (Join-Path -Path $modulePath ` -ChildPath (Join-Path -Path 'xPSDesiredStateConfiguration.Common' ` -ChildPath 'xPSDesiredStateConfiguration.Common.psm1')) Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') # Import Localization Strings $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' if (-not (Test-IsNanoServer)) { Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' } <# .SYNOPSIS Retrieves the user with the given username .PARAMETER UserName Indicates the account name for which you want to ensure a specific state. #> function Get-TargetResource { [OutputType([System.Collections.Hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName ) if (Test-IsNanoServer) { Get-TargetResourceOnNanoServer @PSBoundParameters } else { Get-TargetResourceOnFullSKU @PSBoundParameters } } <# .SYNOPSIS Creates, modifies, or deletes a user. .PARAMETER UserName Indicates the account name for which you want to ensure a specific state. .PARAMETER Ensure Specified if the user account is present or absent. .PARAMETER FullName The full name of the user account. .PARAMETER Description Indicates the description you want to use for the user account. .PARAMETER Password Indicates the password you want to use for this account. .PARAMETER Disabled Indicates if the account is enabled. Set this property to $true to ensure that this account is disabled, and set it to $false to ensure that it is enabled. Defaults to $false. .PARAMETER PasswordNeverExpires Indicates if the password will expire. To ensure that the password for this account will never expire, set this property to $true, and set it to $false if the password will expire. .PARAMETER PasswordChangeRequired Indicates if the user must change the password at the next sign in. Set this property to $true if the user must change the password. .PARAMETER PasswordChangeNotAllowed Indicates if the user can change the password. Set this property to $true to ensure that the user cannot change the password, and set it to $false to allow the user to change the password. .NOTES If Ensure is set to 'Present' then the password parameter is required. #> function Set-TargetResource { # Should process is called in a helper functions but not directly in Set-TargetResource [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) if (Test-IsNanoServer) { Set-TargetResourceOnNanoServer @PSBoundParameters } else { Set-TargetResourceOnFullSKU @PSBoundParameters } } <# .SYNOPSIS Tests if a user is in the desired state. .PARAMETER UserName Indicates the account name for which you want to ensure a specific state. .PARAMETER Ensure Specified if the user account is present or absent. .PARAMETER FullName The full name of the user account. .PARAMETER Description Indicates the description you want to use for the user account. .PARAMETER Password Indicates the password you want to use for this account. .PARAMETER Disabled Indicates if the account is enabled. Set this property to $true to ensure that this account is disabled, and set it to $false to ensure that it is enabled. Defaults to $false. .PARAMETER PasswordNeverExpires Indicates if the password will expire. To ensure that the password for this account will never expire, set this property to $true, and set it to $false if the password will expire. .PARAMETER PasswordChangeRequired Indicates if the user must change the password at the next sign in. Set this property to $true if the user must change the password. .PARAMETER PasswordChangeNotAllowed Indicates if the user can change the password. Set this property to $true to ensure that the user cannot change the password, and set it to $false to allow the user to change the password. #> function Test-TargetResource { [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) if (Test-IsNanoServer) { Test-TargetResourceOnNanoServer @PSBoundParameters } else { Test-TargetResourceOnFullSKU @PSBoundParameters } } <# .SYNOPSIS Retrieves the user with the given username when on a full server .PARAMETER UserName The name of the user to retrieve. #> function Get-TargetResourceOnFullSKU { [OutputType([System.Collections.Hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName ) Set-StrictMode -Version Latest Assert-UserNameValid -UserName $UserName # Try to find a user by a name $principalContext = New-Object ` -TypeName System.DirectoryServices.AccountManagement.PrincipalContext ` -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) try { Write-Verbose -Message 'Starting Get-TargetResource on FullSKU' $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) if ($null -ne $user) { # The user is found. Return all user properties and Ensure='Present'. $returnValue = @{ UserName = $user.Name Ensure = 'Present' FullName = $user.DisplayName Description = $user.Description Disabled = -not $user.Enabled PasswordNeverExpires = $user.PasswordNeverExpires PasswordChangeRequired = $null PasswordChangeNotAllowed = $user.UserCannotChangePassword } return $returnValue } # The user is not found. Return Ensure = Absent. return @{ UserName = $UserName Ensure = 'Absent' } } catch { New-ConnectionException -ErrorId 'MultipleMatches' -ErrorMessage ($script:localizedData.MultipleMatches + $_) } finally { if ($null -ne $user) { $user.Dispose() } $principalContext.Dispose() } } <# .SYNOPSIS Creates, modifies, or deletes a user when on a full server. .PARAMETER UserName The name of the user to create, modify, or delete. .PARAMETER Ensure Specifies whether the user should exist or not. By default this is set to Present .PARAMETER FullName The (optional) full name or display name of the user. If not provided this value will remain blank. .PARAMETER Description Optional description for the user. .PARAMETER Password The desired password for the user. .PARAMETER Disabled Specifies whether the user should be disabled or not. By default this is set to $false .PARAMETER PasswordNeverExpires Specifies whether the password should ever expire or not. By default this is set to $false .PARAMETER PasswordChangeRequired Specifies whether the user must reset their password or not. By default this is set to $false .PARAMETER PasswordChangeNotAllowed Specifies whether the user is allowed to change their password or not. By default this is set to $false .NOTES If Ensure is set to 'Present' then the Password parameter is required. #> function Set-TargetResourceOnFullSKU { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) Set-StrictMode -Version Latest Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName) Assert-UserNameValid -UserName $UserName # Try to find a user by name. $principalContext = New-Object ` -TypeName System.DirectoryServices.AccountManagement.PrincipalContext ` -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) try { try { $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) } catch { New-InvalidOperationException -Message ($script:localizedData.MultipleMatches + $_) } if ($Ensure -eq 'Present') { $whatIfShouldProcess = $true $userExists = $false $saveChanges = $false if ($null -eq $user) { # A user does not exist. Check WhatIf for adding a user $whatIfShouldProcess = $pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.AddOperation) } else { # A user exists $userExists = $true # Check WhatIf for setting a user $whatIfShouldProcess = $pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.SetOperation) } if ($whatIfShouldProcess) { if (-not $userExists) { # The user with the provided name does not exist so add a new user $user = New-Object ` -TypeName System.DirectoryServices.AccountManagement.UserPrincipal ` -ArgumentList $principalContext $user.Name = $UserName $saveChanges = $true } # Set user properties. if ($PSBoundParameters.ContainsKey('FullName') -and ((-not $userExists) -or ($FullName -ne $user.DisplayName))) { $user.DisplayName = $FullName $saveChanges = $true } else { if (-not $userExists) { # For a newly created user, set the DisplayName property to an empty string since by default DisplayName is set to user's name $user.DisplayName = [System.String]::Empty } } if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description))) { $user.Description = $Description $saveChanges = $true } # Set the password regardless of the state of the user if ($PSBoundParameters.ContainsKey('Password')) { $user.SetPassword($Password.GetNetworkCredential().Password) $saveChanges = $true } if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled))) { $user.Enabled = -not $Disabled $saveChanges = $true } if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $user.PasswordNeverExpires))) { $user.PasswordNeverExpires = $PasswordNeverExpires $saveChanges = $true } if ($PSBoundParameters.ContainsKey('PasswordChangeRequired')) { if ($PasswordChangeRequired) { # Expire the password which will force the user to change the password at the next logon $user.ExpirePasswordNow() $saveChanges = $true } } if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($PasswordChangeNotAllowed -ne $user.UserCannotChangePassword))) { $user.UserCannotChangePassword = $PasswordChangeNotAllowed $saveChanges = $true } if ($saveChanges) { $user.Save() # Send an operation success verbose message if ($userExists) { Write-Verbose -Message ($script:localizedData.UserUpdated -f $UserName) } else { Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName) } } else { Write-Verbose -Message ($script:localizedData.NoConfigurationRequired -f $UserName) } } } else { # Ensure is set to 'Absent' if ($null -ne $user) { # The user exists if ($pscmdlet.ShouldProcess($script:localizedData.UserWithName -f $UserName, $script:localizedData.RemoveOperation)) { # Remove the user $user.Delete() } Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName) } else { Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName) } } } catch { New-InvalidOperationException -Message $_ } finally { if ($null -ne $user) { $user.Dispose() } $principalContext.Dispose() } Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName) } <# .SYNOPSIS Tests if a user is in the desired state when on a full server. .PARAMETER UserName The name of the user to test the state of. .PARAMETER Ensure Specifies whether the user should exist or not. By default this is set to Present .PARAMETER FullName The full name/display name that the user should have. If not provided, this value will not be tested. .PARAMETER Description The description that the user should have. If not provided, this value will not be tested. .PARAMETER Password The password the user should have. .PARAMETER Disabled Specifies whether the user account should be disabled or not. .PARAMETER PasswordNeverExpires Specifies whether the password should ever expire or not. .PARAMETER PasswordChangeRequired Not used in Test-TargetResource as there is no easy way to test this value. .PARAMETER PasswordChangeNotAllowed Specifies whether the user should be allowed to change their password or not. #> function Test-TargetResourceOnFullSKU { [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) Set-StrictMode -Version Latest Assert-UserNameValid -UserName $UserName # Try to find a user by a name $principalContext = New-Object System.DirectoryServices.AccountManagement.PrincipalContext ` -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Machine) try { $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) if ($null -eq $user) { # A user with the provided name does not exist Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName) if ($Ensure -eq 'Absent') { return $true } else { return $false } } # A user with the provided name exists Write-Verbose -Message ($script:localizedData.UserExists -f $UserName) # Validate separate properties if ($Ensure -eq 'Absent') { # The Ensure property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') return $false } if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.DisplayName) { # The FullName property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.DisplayName) return $false } if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description) { # The Description property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description) return $false } # Password if ($PSBoundParameters.ContainsKey('Password')) { if (-not $principalContext.ValidateCredentials($UserName, $Password.GetNetworkCredential().Password)) { # The Password property does not match Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password') return $false } } if ($PSBoundParameters.ContainsKey('Disabled') -and $Disabled -eq $user.Enabled) { # The Disabled property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled) return $false } if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $user.PasswordNeverExpires) { # The PasswordNeverExpires property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $user.PasswordNeverExpires) return $false } if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne $user.UserCannotChangePassword) { # The PasswordChangeNotAllowed property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, $user.UserCannotChangePassword) return $false } } catch { New-ConnectionException -ErrorId 'ConnectionError' -ErrorMessage ($script:localizedData.ConnectionError + $_) } finally { if ($null -ne $user) { $user.Dispose() } $principalContext.Dispose() } # All properties match Write-Verbose -Message ($script:localizedData.AllUserPropertisMatch -f 'User', $UserName) return $true } <# .SYNOPSIS Retrieves the user with the given username when on Nano Server. .PARAMETER UserName The name of the user to retrieve. #> function Get-TargetResourceOnNanoServer { [OutputType([System.Collections.Hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName ) Set-StrictMode -Version Latest Assert-UserNameValid -UserName $UserName # Try to find a user by a name try { Write-Verbose -Message 'Starting Get-TargetResource on NanoServer' [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop } catch [System.Exception] { if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) { # The user is not found return @{ UserName = $UserName Ensure = 'Absent' } } New-InvalidOperationException -ErrorRecord $_ } # The user is found. Return all user properties and Ensure = 'Present'. $returnValue = @{ UserName = $user.Name Ensure = 'Present' FullName = $user.FullName Description = $user.Description Disabled = -not $user.Enabled PasswordChangeRequired = $null PasswordChangeNotAllowed = -not $user.UserMayChangePassword } if ($user.PasswordExpires) { $returnValue.Add('PasswordNeverExpires', $false) } else { $returnValue.Add('PasswordNeverExpires', $true) } return $returnValue } <# .SYNOPSIS Creates, modifies, or deletes a user when on Nano Server. .PARAMETER UserName The name of the user to create, modify, or delete. .PARAMETER Ensure Specifies whether the user should exist or not. By default this is set to Present .PARAMETER FullName The (optional) full name or display name of the user. If not provided this value will remain blank. .PARAMETER Description Optional description for the user. .PARAMETER Password The desired password for the user. .PARAMETER Disabled Specifies whether the user should be disabled or not. By default this is set to $false .PARAMETER PasswordNeverExpires Specifies whether the password should ever expire or not. By default this is set to $false .PARAMETER PasswordChangeRequired Specifies whether the user must reset their password or not. By default this is set to $false .PARAMETER PasswordChangeNotAllowed Specifies whether the user is allowed to change their password or not. By default this is set to $false .NOTES If Ensure is set to 'Present' then the Password parameter is required. #> function Set-TargetResourceOnNanoServer { param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) Set-StrictMode -Version Latest Write-Verbose -Message ($script:localizedData.ConfigurationStarted -f $UserName) Assert-UserNameValid -UserName $UserName # Try to find a user by a name. $userExists = $false try { [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop $userExists = $true } catch [System.Exception] { if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) { # The user is not found. Write-Verbose -Message ($script:localizedData.UserDoesNotExist -f $UserName) } else { New-InvalidOperationException -ErrorRecord $_ } } if ($Ensure -eq 'Present') { # Ensure is set to 'Present' if (-not $userExists) { # The user with the provided name does not exist so add a new user New-LocalUser -Name $UserName -NoPassword Write-Verbose -Message ($script:localizedData.UserCreated -f $UserName) } # Set user properties if ($PSBoundParameters.ContainsKey('FullName')) { if (-not $userExists -or $FullName -ne $user.FullName) { if ($null -eq $FullName) { Set-LocalUser -Name $UserName -FullName ([System.String]::Empty) } else { Set-LocalUser -Name $UserName -FullName $FullName } } } else { if (-not $userExists) { # For a newly created user, set the DisplayName property to an empty string since by default DisplayName is set to user's name. Set-LocalUser -Name $UserName -FullName ([System.String]::Empty) } } if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description))) { if ($null -eq $Description) { Set-LocalUser -Name $UserName -Description ([System.String]::Empty) } else { Set-LocalUser -Name $UserName -Description $Description } } # Set the password regardless of the state of the user if ($PSBoundParameters.ContainsKey('Password')) { Set-LocalUser -Name $UserName -Password $Password.Password } if ($PSBoundParameters.ContainsKey('Disabled') -and ((-not $userExists) -or ($Disabled -eq $user.Enabled))) { if ($Disabled) { Disable-LocalUser -Name $UserName } else { Enable-LocalUser -Name $UserName } } $existingUserPasswordNeverExpires = (($userExists) -and ($null -eq $user.PasswordExpires)) if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and ((-not $userExists) -or ($PasswordNeverExpires -ne $existingUserPasswordNeverExpires))) { Set-LocalUser -Name $UserName -PasswordNeverExpires:$passwordNeverExpires } if ($PSBoundParameters.ContainsKey('PasswordChangeRequired') -and ($PasswordChangeRequired)) { Set-LocalUser -Name $UserName -AccountExpires ([System.DateTime]::Now) } # NOTE: The parameter name and the property name have opposite meaning. [System.Boolean] $expected = -not $PasswordChangeNotAllowed $actual = $expected if ($userExists) { $actual = $user.UserMayChangePassword } if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and ((-not $userExists) -or ($expected -ne $actual))) { Set-LocalUser -Name $UserName -UserMayChangePassword $expected } } else { # Ensure is set to 'Absent' if ($userExists) { # The user exists Remove-LocalUser -Name $UserName Write-Verbose -Message ($script:localizedData.UserRemoved -f $UserName) } else { Write-Verbose -Message ($script:localizedData.NoConfigurationRequiredUserDoesNotExist -f $UserName) } } Write-Verbose -Message ($script:localizedData.ConfigurationCompleted -f $UserName) } <# .SYNOPSIS Tests if a user is in the desired state when on Nano Server. .PARAMETER UserName The name of the user to test the state of. .PARAMETER Ensure Specifies whether the user should exist or not. By default this is set to Present .PARAMETER FullName The full name/display name that the user should have. If not provided, this value will not be tested. .PARAMETER Description The description that the user should have. If not provided, this value will not be tested. .PARAMETER Password The password the user should have. .PARAMETER Disabled Specifies whether the user account should be disabled or not. .PARAMETER PasswordNeverExpires Specifies whether the password should ever expire or not. .PARAMETER PasswordChangeRequired Not used in Test-TargetResource as there is no easy way to test this value. .PARAMETER PasswordChangeNotAllowed Specifies whether the user should be allowed to change their password or not. #> function Test-TargetResourceOnNanoServer { [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.String] $FullName, [Parameter()] [System.String] $Description, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Password, [Parameter()] [System.Boolean] $Disabled, [Parameter()] [System.Boolean] $PasswordNeverExpires, [Parameter()] [System.Boolean] $PasswordChangeRequired, [Parameter()] [System.Boolean] $PasswordChangeNotAllowed ) Set-StrictMode -Version Latest Assert-UserNameValid -UserName $UserName # Try to find a user by a name try { [Microsoft.PowerShell.Commands.LocalUser] $user = Get-LocalUser -Name $UserName -ErrorAction Stop } catch [System.Exception] { if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) { # The user is not found if ($Ensure -eq 'Absent') { return $true } else { return $false } } New-InvalidOperationException -ErrorRecord $_ } # A user with the provided name exists Write-Verbose -Message ($script:localizedData.UserExists -f $UserName) # Validate separate properties if ($Ensure -eq 'Absent') { # The Ensure property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Ensure', 'Absent', 'Present') return $false } if ($PSBoundParameters.ContainsKey('FullName') -and $FullName -ne $user.FullName) { # The FullName property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'FullName', $FullName, $user.FullName) return $false } if ($PSBoundParameters.ContainsKey('Description') -and $Description -ne $user.Description) { # The Description property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Description', $Description, $user.Description) return $false } if ($PSBoundParameters.ContainsKey('Password')) { if (-not (Test-CredentialsValidOnNanoServer -UserName $UserName -Password $Password.Password)) { # The Password property does not match Write-Verbose -Message ($script:localizedData.PasswordPropertyMismatch -f 'Password') return $false } } if ($PSBoundParameters.ContainsKey('Disabled') -and ($Disabled -eq $user.Enabled)) { # The Disabled property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'Disabled', $Disabled, $user.Enabled) return $false } $existingUserPasswordNeverExpires = ($null -eq $user.PasswordExpires) if ($PSBoundParameters.ContainsKey('PasswordNeverExpires') -and $PasswordNeverExpires -ne $existingUserPasswordNeverExpires) { # The PasswordNeverExpires property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordNeverExpires', $PasswordNeverExpires, $existingUserPasswordNeverExpires) return $false } if ($PSBoundParameters.ContainsKey('PasswordChangeNotAllowed') -and $PasswordChangeNotAllowed -ne (-not $user.UserMayChangePassword)) { # The PasswordChangeNotAllowed property does not match Write-Verbose -Message ($script:localizedData.PropertyMismatch -f 'PasswordChangeNotAllowed', $PasswordChangeNotAllowed, (-not $user.UserMayChangePassword)) return $false } # All properties match. Return $true. Write-Verbose -Message ($script:localizedData.AllUserPropertisMatch -f 'User', $UserName) return $true } <# .SYNOPSIS Checks that the username does not contain invalid characters. .PARAMETER UserName The username to validate. #> function Assert-UserNameValid { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName ) # Check if the name consists of only periods and/or white spaces $wrongName = $true for ($i = 0; $i -lt $UserName.Length; $i++) { if (-not [System.Char]::IsWhiteSpace($UserName, $i) -and $UserName[$i] -ne '.') { $wrongName = $false break } } $invalidChars = @('\', '/', '"', '[', ']', ':', '|', '<', '>', '+', '=', ';', ',', '?', '*', '@') if ($wrongName) { New-InvalidArgumentException ` -Message ($script:localizedData.InvalidUserName -f $UserName, [System.String]::Join(' ', $invalidChars)) ` -ArgumentName 'UserName' } if ($UserName.IndexOfAny($invalidChars) -ne -1) { New-InvalidArgumentException ` -Message ($script:localizedData.InvalidUserName -f $UserName, [System.String]::Join(' ', $invalidChars)) ` -ArgumentName 'UserName' } } <# .SYNOPSIS Creates a new Connection error record and throws it. .PARAMETER ErrorId The ID for the error record to be thrown. .PARAMETER ErrorMessage Message to be included in the error record to be thrown. #> function New-ConnectionException { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorId, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $ErrorMessage ) $errorCategory = [System.Management.Automation.ErrorCategory]::ConnectionError $exception = New-Object ` -TypeName System.ArgumentException ` -ArgumentList $ErrorMessage $errorRecord = New-Object ` -TypeName System.Management.Automation.ErrorRecord ` -ArgumentList @($exception, $ErrorId, $errorCategory, $null) throw $errorRecord } <# .SYNOPSIS Tests the local user's credentials on the local machine. .PARAMETER UserName The username to validate the credentials of. .PARAMETER Password The password of the given user. #> function Test-CredentialsValidOnNanoServer { [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $UserName, [Parameter()] [ValidateNotNullOrEmpty()] [System.Security.SecureString] $Password ) $source = @' [Flags] private enum LogonType { Logon32LogonInteractive = 2, Logon32LogonNetwork, Logon32LogonBatch, Logon32LogonService, Logon32LogonUnlock, Logon32LogonNetworkCleartext, Logon32LogonNewCredentials } [Flags] private enum LogonProvider { Logon32ProviderDefault = 0, Logon32ProviderWinnt35, Logon32ProviderWinnt40, Logon32ProviderWinnt50 } [DllImport("api-ms-win-security-logon-l1-1-1.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern Boolean LogonUser( String lpszUserName, String lpszDomain, IntPtr lpszPassword, LogonType dwLogonType, LogonProvider dwLogonProvider, out IntPtr phToken ); [DllImport("api-ms-win-core-handle-l1-1-0.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] internal static extern bool CloseHandle(IntPtr handle); public static bool ValidateCredentials(string username, SecureString password) { IntPtr tokenHandle = IntPtr.Zero; IntPtr unmanagedPassword = IntPtr.Zero; unmanagedPassword = SecureStringMarshal.SecureStringToCoTaskMemUnicode(password); try { return LogonUser( username, null, unmanagedPassword, LogonType.Logon32LogonInteractive, LogonProvider.Logon32ProviderDefault, out tokenHandle); } catch { return false; } finally { if (tokenHandle != IntPtr.Zero) { CloseHandle(tokenHandle); } if (unmanagedPassword != IntPtr.Zero) { Marshal.ZeroFreeCoTaskMemUnicode(unmanagedPassword); } unmanagedPassword = IntPtr.Zero; } } '@ Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource ` -Name CredentialsValidationTool -MemberDefinition $source -Using System.Security -ReferencedAssemblies System.Security.SecureString.dll | Out-Null return [Microsoft.Windows.DesiredStateConfiguration.NanoServer.UserResource.CredentialsValidationTool]::ValidateCredentials($UserName, $Password) } Export-ModuleMember -Function *-TargetResource |