diff --git a/DscResources/MSFT_UserResource/MSFT_UserResource.psm1 b/DscResources/MSFT_UserResource/MSFT_UserResource.psm1 new file mode 100644 index 0000000..426e60b --- /dev/null +++ b/DscResources/MSFT_UserResource/MSFT_UserResource.psm1 @@ -0,0 +1,1333 @@ +# User name and password needed for this resource and Write-Verbose Used in helper functions +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCUseVerboseMessageInDSCResource', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonResourceHelper.psm1') + +# Localized messages for Write-Verbose statements in this resource +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_UserResource' + +if (-not (Test-IsNanoServer)) +{ + Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' +} + +<# + .SYNOPSIS + Retrieves the user with the given username + + .PARAMETER UserName + The name of the user to retrieve. +#> +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 + 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-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, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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 + 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-TargetResource +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $UserName, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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 + { + $user = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($principalContext, $UserName) + 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 = [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 ($user -ne $null) + { + # 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 ($script:localizedData.MultipleMatches + $_) + } + 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, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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 ($FullName -eq $null) + { + Set-LocalUser -Name $UserName -FullName ([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 ([String]::Empty) + } + } + + if ($PSBoundParameters.ContainsKey('Description') -and ((-not $userExists) -or ($Description -ne $user.Description))) + { + if ($null -eq $Description) + { + Set-LocalUser -Name $UserName -Description ([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 ([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, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.String] + $FullName, + + [System.String] + $Description, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Password, + + [System.Boolean] + $Disabled, + + [System.Boolean] + $PasswordNeverExpires, + + [System.Boolean] + $PasswordChangeRequired, + + [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 [Char]::IsWhiteSpace($UserName, $i) -and $UserName[$i] -ne '.') + { + $wrongName = $false + break + } + } + + $invalidChars = @('\','/','"','[',']',':','|','<','>','+','=',';',',','?','*','@') + + if ($wrongName) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidUserName -f $UserName, [String]::Join(' ', $invalidChars)) ` + -ArgumentName 'UserName' + } + + if ($UserName.IndexOfAny($invalidChars) -ne -1) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidUserName -f $UserName, [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, + + [ValidateNotNullOrEmpty()] + [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 diff --git a/DscResources/MSFT_UserResource/MSFT_UserResource.schema.mof b/DscResources/MSFT_UserResource/MSFT_UserResource.schema.mof new file mode 100644 index 0000000..bd27563 --- /dev/null +++ b/DscResources/MSFT_UserResource/MSFT_UserResource.schema.mof @@ -0,0 +1,13 @@ +[ClassVersion("1.0.0"), FriendlyName("User")] +class MSFT_UserResource : OMI_BaseResource +{ + [Key,Description("The name of the User to Create/Modify/Delete")] String UserName; + [Write,Description("An enumerated value that describes if the user is expected to exist on the machine"),ValueMap{"Present", "Absent"},Values{"Present", "Absent"}] String Ensure; + [Write,Description("The display name of the user")] String FullName; + [Write,Description("A description for the user")] String Description; + [Write,Description("The password for the user"),EmbeddedInstance("MSFT_Credential")] String Password; + [Write,Description("Value used to disable/enable a user account")] Boolean Disabled; + [Write,Description("Value used to set whether a user's password expires or not")] Boolean PasswordNeverExpires; + [Write,Description("Value used to require a user to change their password")] Boolean PasswordChangeRequired; + [Write,Description("Value used to set whether a user can/cannot change their password")] Boolean PasswordChangeNotAllowed; +}; diff --git a/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.schema.mfl b/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.schema.mfl new file mode 100644 index 0000000..5a284f6 Binary files /dev/null and b/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.schema.mfl differ diff --git a/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.strings.psd1 b/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.strings.psd1 new file mode 100644 index 0000000..48de5c3 --- /dev/null +++ b/DscResources/MSFT_UserResource/en-US/MSFT_UserResource.strings.psd1 @@ -0,0 +1,23 @@ +# Localized resources for User + +ConvertFrom-StringData @' + UserWithName = User: {0} + RemoveOperation = Remove + AddOperation = Add + SetOperation = Set + ConfigurationStarted = Configuration of user {0} started. + ConfigurationCompleted = Configuration of user {0} completed successfully. + UserCreated = User {0} created successfully. + UserUpdated = User {0} properties updated successfully. + UserRemoved = User {0} removed successfully. + NoConfigurationRequired = User {0} exists on this node with the desired properties. No action required. + NoConfigurationRequiredUserDoesNotExist = User {0} does not exist on this node. No action required. + InvalidUserName = The name {0} cannot be used. Names may not consist entirely of periods and/or spaces, or contain these characters: {1} + UserExists = A user with the name {0} exists. + UserDoesNotExist = A user with the name {0} does not exist. + PropertyMismatch = The value of the {0} property is expected to be {1} but it is {2}. + PasswordPropertyMismatch = The value of the {0} property does not match. + AllUserPropertisMatch = All {0} {1} properties match. + ConnectionError = There could be a possible connection error while trying to use the System.DirectoryServices API's. + MultipleMatches = There could be a possible multiple matches exception while trying to use the System.DirectoryServices API's. +'@ diff --git a/Examples/Sample_User_CreateUser.ps1 b/Examples/Sample_User_CreateUser.ps1 new file mode 100644 index 0000000..b2aae55 --- /dev/null +++ b/Examples/Sample_User_CreateUser.ps1 @@ -0,0 +1,16 @@ +Configuration UserExample +{ + param ( + [System.Management.Automation.PSCredential] + $PasswordCredential + ) + + Import-DscResource -ModuleName PSDscResources + + User UserExample + { + Ensure = 'Present' # To ensure the user account does not exist, set Ensure to "Absent" + UserName = 'SomeUserName' + Password = $PasswordCredential # This needs to be a credential object + } +} diff --git a/Examples/Sample_User_Generic.ps1 b/Examples/Sample_User_Generic.ps1 new file mode 100644 index 0000000..111dd5a --- /dev/null +++ b/Examples/Sample_User_Generic.ps1 @@ -0,0 +1,68 @@ +param +( + [Parameter(Mandatory)] + [System.String] + $ConfigurationName +) + +<# + Create a custom configuration by passing in whatever + values you need. $Password is the only param that is + required since it must be a PSCredential object. + If you want to create a user with minimal attributes, + every param except username can be deleted since they + are optional. +#> + +Configuration $ConfigurationName +{ + param + ( + [System.String] + $UserName = 'Test UserName', + + [System.String] + $Description = 'Test Description', + + [System.String] + $FullName = 'Test Full Name', + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory)] + [System.Management.Automation.PSCredential] + $Password, + + [System.Boolean] + $Disabled = $false, + + [System.Boolean] + $PasswordNeverExpires = $false, + + [System.Boolean] + $PasswordChangeRequired = $false, + + [System.Boolean] + $PasswordChangeNotAllowed = $false + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node Localhost { + + User UserResource1 + { + UserName = $UserName + Ensure = $Ensure + FullName = $FullName + Description = $Description + Password = $Password + Disabled = $Disabled + PasswordNeverExpires = $PasswordNeverExpires + PasswordChangeRequired = $PasswordChangeRequired + PasswordChangeNotAllowed = $PasswordChangeNotAllowed + } + } +} diff --git a/Tests/Integration/MSFT_UserResource.Integration.Tests.ps1 b/Tests/Integration/MSFT_UserResource.Integration.Tests.ps1 new file mode 100644 index 0000000..131f0ea --- /dev/null +++ b/Tests/Integration/MSFT_UserResource.Integration.Tests.ps1 @@ -0,0 +1,200 @@ +<# + To run these tests, the currently logged on user must have rights to create a user. + These integration tests cover creating a brand new user, updating values + of a user that already exists, and deleting a user that exists. +#> + +# Suppressing this rule since we need to create a plaintext password to test this resource +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonTestHelper.psm1') ` + -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'PSDscResources' ` + -DscResourceName 'MSFT_UserResource' ` + -TestType 'Integration' + +try { + + $configFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_UserResource.config.ps1' + + + Describe 'UserResource Integration Tests' { + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = '*' + PSDscAllowPlainTextPassword = $true + } + @{ + NodeName = 'localhost' + } + ) + } + + Context 'Should create a new user' { + $configurationName = 'MSFT_User_NewUser' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'NewUser.log' + + $testUserName = 'TestUserName12345' + $testUserPassword = 'StrongOne7.' + $testDescription = 'Test Description' + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential = New-Object PSCredential ($testUserName, $secureTestPassword) + + try + { + It 'Should compile without throwing' { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -UserName $testUserName ` + -Password $testCredential ` + -Description $testDescription ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData ` + -ErrorAction Stop + Start-DscConfiguration -Path $configurationPath -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction Stop + $currentConfig.UserName | Should Be $testUserName + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.Description | Should Be $TestDescription + $currentConfig.Disabled | Should Be $false + $currentConfig.PasswordChangeRequired | Should Be $null + } + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + Context 'Should update an existing user' { + $configurationName = 'MSFT_User_UpdateUser' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'UpdateUser.log' + + $testUserName = 'TestUserName12345' + $testUserPassword = 'StrongOne7.' + $testDescription = 'New Test Description' + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential = New-Object PSCredential ($testUserName, $secureTestPassword) + + try + { + It 'Should compile without throwing' { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -UserName $testUserName ` + -Password $testCredential ` + -Description $testDescription ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData ` + -ErrorAction Stop + Start-DscConfiguration -Path $configurationPath -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction Stop + $currentConfig.UserName | Should Be $testUserName + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.Description | Should Be $TestDescription + $currentConfig.Disabled | Should Be $false + $currentConfig.PasswordChangeRequired | Should Be $null + } + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + Context 'Should delete an existing user' { + $configurationName = 'MSFT_User_DeleteUser' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'DeleteUser.log' + + $testUserName = 'TestUserName12345' + $testUserPassword = 'StrongOne7.' + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential = New-Object PSCredential ($testUserName, $secureTestPassword) + + try + { + It 'Should compile without throwing' { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -UserName $testUserName ` + -Password $testCredential ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData ` + -Ensure 'Absent' ` + -ErrorAction Stop + Start-DscConfiguration -Path $configurationPath -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction Stop + $currentConfig.UserName | Should Be $testUserName + $currentConfig.Ensure | Should Be 'Absent' + } + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} + + diff --git a/Tests/Integration/MSFT_UserResource.config.ps1 b/Tests/Integration/MSFT_UserResource.config.ps1 new file mode 100644 index 0000000..60b4bed --- /dev/null +++ b/Tests/Integration/MSFT_UserResource.config.ps1 @@ -0,0 +1,46 @@ + +# Integration Test Config Template Version 1.0.0 +param +( + [Parameter(Mandatory)] + [System.String] + $ConfigurationName +) + + +Configuration $ConfigurationName +{ + param + ( + [System.String] + $UserName = 'Test UserName', + + [System.String] + $Description = 'Test Description', + + [System.String] + $FullName = 'Test Full Name', + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory)] + [System.Management.Automation.PSCredential] + $Password + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node Localhost { + + User UserResource1 + { + UserName = $UserName + Ensure = $Ensure + FullName = $FullName + Description = $Description + Password = $Password + } + } +} diff --git a/Tests/TestHelpers/MSFT_UserResource.TestHelper.psm1 b/Tests/TestHelpers/MSFT_UserResource.TestHelper.psm1 new file mode 100644 index 0000000..1ed7d5a --- /dev/null +++ b/Tests/TestHelpers/MSFT_UserResource.TestHelper.psm1 @@ -0,0 +1,468 @@ +Import-Module "$PSScriptRoot\..\..\DSCResources\CommonResourceHelper.psm1" -Force + +<# + .SYNOPSIS + Tests if a scope represents the current machine. + + .PARAMETER Scope + The scope to test. +#> +function Test-IsLocalMachine +{ + [OutputType([System.Boolean])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Scope + ) + + Set-StrictMode -Version latest + + if ($scope -eq '.') + { + return $true + } + + if ($scope -eq $env:COMPUTERNAME) + { + return $true + } + + if ($scope -eq 'localhost') + { + return $true + } + + if ($scope.Contains('.')) + { + if ($scope -eq '127.0.0.1') + { + return $true + } + + # Determine if we have an ip address that matches an ip address on one of the network adapters. + # NOTE: This is likely overkill; consider removing it. + $networkAdapters = @(Get-CimInstance Win32_NetworkAdapterConfiguration) + foreach ($networkAdapter in $networkAdapters) + { + if ($null -ne $networkAdapter.IPAddress) + { + foreach ($address in $networkAdapter.IPAddress) + { + if ($address -eq $scope) + { + return $true + } + } + } + } + } + + return $false +} + +<# + .SYNOPSIS + Creates a user account. + + .DESCRIPTION + This function creates a user on the local or remote machine. + + .PARAMETER Credential + The credential containing the username and password to use to create the account. + + .PARAMETER Description + The optional description to set on the user account. + + .PARAMETER ComputerName + The optional name of the computer to update. Omit to create a user on the local machine. + + .NOTES + For remote machines, the currently logged on user must have rights to create a user. +#> +function New-User +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [System.String] + $Description, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + if (Test-IsNanoServer) + { + New-UserOnNanoServer @PSBoundParameters + } + else + { + New-UserOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Creates a user account on a full server. + + .DESCRIPTION + This function creates a user on the local or remote machine running a full server. + + .PARAMETER Credential + The credential containing the username and password to use to create the account. + + .PARAMETER Description + The optional description to set on the user account. + + .PARAMETER ComputerName + The optional name of the computer to update. Omit to create a user on the local machine. + + .NOTES + For remote machines, the currently logged on user must have rights to create a user. +#> +function New-UserOnFullSKU +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [System.String] + $Description, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + Set-StrictMode -Version Latest + + $userName = $Credential.UserName + $password = $Credential.GetNetworkCredential().Password + + # Remove user if it already exists. + Remove-User $userName $ComputerName + + $adComputerEntry = [ADSI] "WinNT://$ComputerName" + $adUserEntry = $adComputerEntry.Create('User', $userName) + $null = $adUserEntry.SetPassword($password) + + if ($PSBoundParameters.ContainsKey('Description')) + { + $null = $adUserEntry.Put('Description', $Description) + } + + $null = $adUserEntry.SetInfo() +} + +<# + .SYNOPSIS + Creates a user account on a Nano server. + + .DESCRIPTION + This function creates a user on the local machine running a Nano server. + + .PARAMETER Credential + The credential containing the username and password to use to create the account. + + .PARAMETER Description + The optional description to set on the user account. + + .PARAMETER ComputerName + This parameter should not be used on NanoServer. +#> +function New-UserOnNanoServer +{ + + param ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.CredentialAttribute()] + $Credential, + + [System.String] + $Description, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + Set-StrictMode -Version Latest + + if ($PSBoundParameters.ContainsKey('ComputerName')) + { + if (-not (Test-IsLocalMachine -Scope $ComputerName)) + { + throw 'Do not specify the ComputerName arguments when running on NanoServer unless it is local machine.' + } + } + + $userName = $Credential.UserName + $securePassword = $Credential.GetNetworkCredential().SecurePassword + + # Remove user if it already exists. + Remove-LocalUser -Name $userName -ErrorAction SilentlyContinue + + New-LocalUser -Name $userName -Password $securePassword + + if ($PSBoundParameters.ContainsKey('Description')) + { + Set-LocalUser -Name $userName -Description $Description + } +} + +<# + .SYNOPSIS + Removes a user account. + + .DESCRIPTION + This function removes a local user from the local or remote machine. + + .PARAMETER UserName + The name of the user to remove. + + .PARAMETER ComputerName + The optional name of the computer to update. Omit to remove the user on the local machine. + + .NOTES + For remote machines, the currently logged on user must have rights to remove a user. +#> +function Remove-User +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + if (Test-IsNanoServer) + { + Remove-UserOnNanoServer @PSBoundParameters + } + else + { + Remove-UserOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Removes a user account on a full server. + + .DESCRIPTION + This function removes a local user from the local or remote machine running a full server. + + .PARAMETER UserName + The name of the user to remove. + + .PARAMETER ComputerName + The optional name of the computer to update. Omit to remove the user on the local machine. + + .NOTES + For remote machines, the currently logged on user must have rights to remove a user. +#> +function Remove-UserOnFullSKU +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + Set-StrictMode -Version Latest + + $adComputerEntry = [ADSI] "WinNT://$ComputerName" + + if ($adComputerEntry.Children | Where-Object Path -like "WinNT://*$ComputerName/$UserName") + { + $null = $adComputerEntry.Delete('user', $UserName) + } +} + +<# + .SYNOPSIS + Removes a local user account on a Nano server. + + .DESCRIPTION + This function removes a local user from the local machine running a Nano Server. + + .PARAMETER UserName + The name of the user to remove. + + .PARAMETER ComputerName + This parameter should not be used on NanoServer. +#> +function Remove-UserOnNanoServer +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + Set-StrictMode -Version Latest + + if ($PSBoundParameters.ContainsKey('ComputerName')) + { + if (-not (Test-IsLocalMachine -Scope $ComputerName)) + { + throw 'Do not specify the ComputerName arguments when running on NanoServer unless it is local machine.' + } + } + + Remove-LocalUser -Name $UserName +} + +<# + .SYNOPSIS + Determines if a user exists.. + + .DESCRIPTION + This function determines if a user exists on a local or remote machine running. + + .PARAMETER UserName + The name of the user to test. + + .PARAMETER ComputerName + The optional name of the computer to update. +#> +function Test-User +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + if (Test-IsNanoServer) + { + Test-UserOnNanoServer @PSBoundParameters + } + else + { + Test-UserOnFullSKU @PSBoundParameters + } +} + +<# + .SYNOPSIS + Determines if a user exists on a full server. + + .DESCRIPTION + This function determines if a user exists on a local or remote machine running a full server. + + .PARAMETER UserName + The name of the user to test. + + .PARAMETER ComputerName + The optional name of the computer to update. +#> +function Test-UserOnFullSKU +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + Set-StrictMode -Version Latest + + $adComputerEntry = [ADSI] "WinNT://$ComputerName" + if ($adComputerEntry.Children | Where-Object Path -like "WinNT://*$ComputerName/$UserName") + { + return $true + } + + return $false +} + +<# + .SYNOPSIS + Determines if a user exists on a Nano server. + + .DESCRIPTION + This function determines if a user exists on a local or remote machine running a Nano server. + + .PARAMETER UserName + The name of the user to test. + + .PARAMETER ComputerName + This parameter should not be used on NanoServer. +#> +function Test-UserOnNanoServer +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [System.String] + $ComputerName = $env:COMPUTERNAME + ) + + if ($PSBoundParameters.ContainsKey('ComputerName')) + { + if (-not (Test-IsLocalMachine -Scope $ComputerName)) + { + throw 'Do not specify the ComputerName arguments when running on NanoServer unless it is local machine.' + } + } + + # Try to find a group by its name. + try + { + $null = Get-LocalUser -Name $UserName -ErrorAction Stop + return $true + } + catch [System.Exception] + { + if ($_.CategoryInfo.ToString().Contains('UserNotFoundException')) + { + # A user with the provided name does not exist. + return $false + } + throw $_.Exception + } + finally + { + Remove-LocalUser -Name $UserName + } + + return $false +} + +Export-ModuleMember -Function ` + New-User, ` + Remove-User, ` + Test-IsLocalMachine, ` + Test-User diff --git a/Tests/Unit/MSFT_UserResource.Tests.ps1 b/Tests/Unit/MSFT_UserResource.Tests.ps1 new file mode 100644 index 0000000..53dca49 --- /dev/null +++ b/Tests/Unit/MSFT_UserResource.Tests.ps1 @@ -0,0 +1,448 @@ +# To run these tests, the currently logged on user must have rights to create a user +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonTestHelper.psm1') ` + -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DSCResourceModuleName 'PSDscResources' ` + -DSCResourceName 'MSFT_UserResource' ` + -TestType Unit + +try { + + Import-Module -Name (Join-Path -Path $PSScriptRoot ` + -ChildPath 'MSFT_UserResource.TestHelper.psm1') ` + -Force + + InModuleScope 'MSFT_UserResource' { + # Used to skip the Nano server tests for the time being since they are not working on AppVeyor + + $script:skipMe = $true + + $existingUserName = 'TestUserName12345' + $existingUserPassword = 'StrongOne7.' + $existingDescription = 'Some Description' + $existingSecurePassword = ConvertTo-SecureString $existingUserPassword -AsPlainText -Force + $existingTestCredential = New-Object PSCredential ($existingUserName, $existingSecurePassword) + + New-User -Credential $existingTestCredential -Description $existingDescription + + $newUserName1 = 'NewTestUserName12345' + $newUserPassword1 = 'NewStrongOne123.' + $newFullName1 = 'Fullname1' + $newUserDescription1 = 'New Description1' + $newSecurePassword1 = ConvertTo-SecureString $newUserPassword1 -AsPlainText -Force + $newCredential1 = New-Object PSCredential ($newUserName1, $newSecurePassword1) + + $newUserName2 = 'newUser1234' + $newPassword2 = 'ThisIsAStrongPassword543!' + $newFullName2 = 'Fullname2' + $newUserDescription2 = 'New Description2' + $newSecurePassword2 = ConvertTo-SecureString $newPassword2 -AsPlainText -Force + $newCredential2 = New-Object PSCredential ($newUserName2, $newSecurePassword2) + + try { + + Describe 'UserResource/Get-TargetResource' { + + Context 'Tests on FullSKU' { + Mock -CommandName Test-IsNanoServer -MockWith { return $false } + + It 'Should return the user as Present' { + $getTargetResourceResult = Get-TargetResource $existingUserName + + $getTargetResourceResult['UserName'] | Should Be $existingUserName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $existingDescription + $getTargetResourceResult['PasswordChangeRequired'] | Should Be $null + } + + It 'Should return the user as Absent' { + $getTargetResourceResult = Get-TargetResource 'NotAUserName' + + $getTargetResourceResult['UserName'] | Should Be 'NotAUserName' + $getTargetResourceResult['Ensure'] | Should Be 'Absent' + } + } + + Context 'Tests on Nano Server' { + Mock -CommandName Test-IsNanoServer -MockWith { return $true } + + It 'Should return the user as Present on Nano Server' -Skip:$script:skipMe { + $getTargetResourceResult = Get-TargetResource $existingUserName + + $getTargetResourceResult['UserName'] | Should Be $existingUserName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $existingDescription + $getTargetResourceResult['PasswordChangeRequired'] | Should Be $null + } + + It 'Should return the user as Absent' -Skip:$script:skipMe { + $getTargetResourceResult = Get-TargetResource 'NotAUserName' + + $getTargetResourceResult['UserName'] | Should Be 'NotAUserName' + $getTargetResourceResult['Ensure'] | Should Be 'Absent' + } + } + } + + Describe 'UserResource/Set-TargetResource' { + Context 'Tests on FullSKU' { + Mock -CommandName Test-IsNanoServer -MockWith { return $false } + + try + { + New-User -Credential $newCredential1 -Description $newUserDescription1 + + It 'Should remove the user' { + Test-User -UserName $newUserName1 | Should Be $true + Set-TargetResource -UserName $newUserName1 -Ensure 'Absent' + Test-User -UserName $newUserName1 | Should Be $false + } + + It 'Should add the new user' { + Set-TargetResource -UserName $newUserName2 -Password $newCredential2 -Ensure 'Present' + Test-User -UserName $newUserName2 | Should Be $true + } + + It 'Should update the user' { + $disabled = $false + $passwordNeverExpires = $true + $passwordChangeRequired = $false + $passwordChangeNotAllowed = $true + + Set-TargetResource -UserName $newUserName2 ` + -Password $newCredential2 ` + -Ensure 'Present' ` + -FullName $newFullName1 ` + -Description $newUserDescription1 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeRequired $passwordChangeRequired ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + + Test-User -UserName $newUserName2 | Should Be $true + $testTargetResourceResult1 = + Test-TargetResource -UserName $newUserName2 ` + -Password $newCredential2 ` + -Ensure 'Present' ` + -FullName $newFullName1 ` + -Description $newUserDescription1 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + $testTargetResourceResult1 | Should Be $true + } + It 'Should update the user again with different values' { + $disabled = $false + $passwordNeverExpires = $false + $passwordChangeRequired = $true + $passwordChangeNotAllowed = $false + + Set-TargetResource -UserName $newUserName2 ` + -Password $newCredential1 ` + -Ensure 'Present' ` + -FullName $newFullName2 ` + -Description $newUserDescription2 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeRequired $passwordChangeRequired ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + + Test-User -UserName $newUserName2 | Should Be $true + $testTargetResourceResult2 = + Test-TargetResource -UserName $newUserName2 ` + -Password $newCredential1 ` + -Ensure 'Present' ` + -FullName $newFullName2 ` + -Description $newUserDescription2 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + $testTargetResourceResult2 | Should Be $true + } + } + finally + { + Remove-User -UserName $newUserName1 + Remove-User -UserName $newUserName2 + } + } + + Context 'Tests on Nano Server' { + Mock -CommandName Test-IsNanoServer -MockWith { return $true } + Mock -CommandName Test-CredentialsValidOnNanoServer { return $true } + + try + { + New-User -Credential $newCredential1 -Description $newUserDescription1 + + It 'Should remove the user' -Skip:$script:skipMe { + Test-User -UserName $newUserName1 | Should Be $true + Set-TargetResource -UserName $newUserName1 -Ensure 'Absent' + Test-User -UserName $newUserName1 | Should Be $false + } + + It 'Should add the new user' -Skip:$script:skipMe { + Set-TargetResource -UserName $newUserName2 -Password $newCredential2 -Ensure 'Present' + Test-User -UserName $newUserName2 | Should Be $true + } + + It 'Should update the user' -Skip:$script:skipMe { + $disabled = $false + $passwordNeverExpires = $true + $passwordChangeRequired = $false + $passwordChangeNotAllowed = $true + + Set-TargetResource -UserName $newUserName2 ` + -Password $newCredential2 ` + -Ensure 'Present' ` + -FullName $newFullName1 ` + -Description $newUserDescription1 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeRequired $passwordChangeRequired ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + + Test-User -UserName $newUserName2 | Should Be $true + $testTargetResourceResult1 = + Test-TargetResource -UserName $newUserName2 ` + -Password $newCredential2 ` + -Ensure 'Present' ` + -FullName $newFullName1 ` + -Description $newUserDescription1 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + $testTargetResourceResult1 | Should Be $true + } + It 'Should update the user again with different values' -Skip:$script:skipMe { + $disabled = $false + $passwordNeverExpires = $false + $passwordChangeRequired = $true + $passwordChangeNotAllowed = $false + + Set-TargetResource -UserName $newUserName2 ` + -Password $newCredential1 ` + -Ensure 'Present' ` + -FullName $newFullName2 ` + -Description $newUserDescription2 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeRequired $passwordChangeRequired ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + + Test-User -UserName $newUserName2 | Should Be $true + $testTargetResourceResult2 = + Test-TargetResource -UserName $newUserName2 ` + -Password $newCredential1 ` + -Ensure 'Present' ` + -FullName $newFullName2 ` + -Description $newUserDescription2 ` + -Disabled $disabled ` + -PasswordNeverExpires $passwordNeverExpires ` + -PasswordChangeNotAllowed $passwordChangeNotAllowed + $testTargetResourceResult2 | Should Be $true + } + } + finally + { + Remove-User -UserName $newUserName1 + Remove-User -UserName $newUserName2 + } + } + } + + Describe 'UserResource/Test-TargetResource' { + Context 'Tests on FullSKU' { + Mock -CommandName Test-IsNanoServer -MockWith { return $false } + $absentUserName = 'AbsentUserUserName123456789' + + It 'Should return true when user Present and correct values' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Description $existingDescription ` + -Password $existingTestCredential ` + -Disabled $false ` + -PasswordNeverExpires $false ` + -PasswordChangeNotAllowed $false + $testTargetResourceResult | Should Be $true + } + + It 'Should return true when user Absent and Ensure = Absent' { + $testTargetResourceResult = Test-TargetResource -UserName $absentUserName ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $true + } + + It 'Should return false when user Absent and Ensure = Present' { + $testTargetResourceResult = Test-TargetResource -UserName $absentUserName ` + -Ensure 'Present' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when user Present and Ensure = Absent' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when Password is wrong' { + $badPassword = 'WrongPassword' + $secureBadPassword = ConvertTo-SecureString $badPassword -AsPlainText -Force + $badTestCredential = New-Object PSCredential ($existingUserName, $secureBadPassword) + + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Password $badTestCredential + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when user Present and wrong Description' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Description 'Wrong description' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when FullName is incorrect' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -FullName 'Wrong FullName' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when Disabled is incorrect' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Disabled $true + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when PasswordNeverExpires is incorrect' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -PasswordNeverExpires $true + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when PasswordChangeNotAllowed is incorrect' { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -PasswordChangeNotAllowed $true + $testTargetResourceResult | Should Be $false + } + } + + Context 'Tests on Nano Server' { + Mock -CommandName Test-IsNanoServer -MockWith { return $true } + + $absentUserName = 'AbsentUserUserName123456789' + + It 'Should return true when user Present and correct values' -Skip:$script:skipMe { + Mock -CommandName Test-CredentialsValidOnNanoServer { return $true } + + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Description $existingDescription ` + -Password $existingTestCredential ` + -Disabled $false ` + -PasswordNeverExpires $false ` + -PasswordChangeNotAllowed $false + $testTargetResourceResult | Should Be $true + } + + It 'Should return true when user Absent and Ensure = Absent' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $absentUserName ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $true + } + + It 'Should return false when user Absent and Ensure = Present' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $absentUserName ` + -Ensure 'Present' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when user Present and Ensure = Absent' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when Password is wrong' -Skip:$script:skipMe { + Mock -CommandName Test-CredentialsValidOnNanoServer { return $false } + + $badPassword = 'WrongPassword' + $secureBadPassword = ConvertTo-SecureString $badPassword -AsPlainText -Force + $badTestCredential = New-Object PSCredential ($existingUserName, $secureBadPassword) + + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Password $badTestCredential + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when user Present and wrong Description' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Description 'Wrong description' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when FullName is incorrect' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -FullName 'Wrong FullName' + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when Disabled is incorrect' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -Disabled $true + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when PasswordNeverExpires is incorrect' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -PasswordNeverExpires $true + $testTargetResourceResult | Should Be $false + } + + It 'Should return false when PasswordChangeNotAllowed is incorrect' -Skip:$script:skipMe { + $testTargetResourceResult = Test-TargetResource -UserName $existingUserName ` + -PasswordChangeNotAllowed $true + $testTargetResourceResult | Should Be $false + } + } + } + + Describe 'UserResource/Assert-UserNameValid' { + It 'Should not throw when username contains all valid chars' { + { Assert-UserNameValid -UserName 'abc123456!f_t-l098s' } | Should Not Throw + } + + It 'Should throw InvalidArgumentError when username contains only whitespace and dots' { + $invalidName = ' . .. . ' + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorId = 'UserNameHasOnlyWhiteSpacesAndDots' + $errorMessage = "The name $invalidName cannot be used." + $exception = New-Object System.ArgumentException $errorMessage; + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null + { Assert-UserNameValid -UserName $invalidName } | Should Throw $errorRecord + } + + It 'Should throw InvalidArgumentError when username contains an invalid char' { + $invalidName = 'user|name' + $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $errorId = 'UserNameHasInvalidCharachter' + $errorMessage = "The name $invalidName cannot be used." + $exception = New-Object System.ArgumentException $errorMessage; + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null + { Assert-UserNameValid -UserName $invalidName } | Should Throw $errorRecord + } + } + } + finally + { + Remove-User -UserName $existingUserName + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} +